Skip to content

Structuring reducers page #1930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Sep 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
55893c8
Add an initial empty "Structuring Reducers" recipe.
markerikson Jul 4, 2016
007c713
Add initial intro text
markerikson Jul 4, 2016
6392e85
Add "Basic Reducer Structure" section
markerikson Jul 4, 2016
3bc6be6
Add initial "Basic State Shape" and "Splitting Reducer Logic" sections
markerikson Jul 4, 2016
152d58a
Define terms and various function concepts
markerikson Jul 4, 2016
9a606ee
Add reducer refactoring examples
markerikson Jul 4, 2016
aca189f
Add unfinished "Immutable Data" topic, reducer terms, and mutation note
markerikson Jul 4, 2016
ccd2f6c
Split "Structuring Reducers" into multiple pages
markerikson Jul 6, 2016
418abf5
Add Prerequisite Concepts content
markerikson Jul 6, 2016
aa64357
Really emphasize the prerequisites
stevensacks Jul 6, 2016
f2275b1
Update StructuringReducers.md
stevensacks Jul 6, 2016
a436f1b
Merge pull request #2 from stevensacks/patch-1
markerikson Jul 6, 2016
b5cfd82
Add example state shape and note about refactoring example
markerikson Jul 7, 2016
1efe2e3
Add additional prerequisite links
markerikson Jul 14, 2016
27bd414
Expand reducer refactoring example
markerikson Jul 14, 2016
0711717
Add more prerequisite links, and tweak example identifiers
markerikson Jul 23, 2016
39476da
Rework reducer terminology
markerikson Jul 23, 2016
4b15066
Initial draft of "Using combineReducers"
markerikson Jul 23, 2016
3987892
Tweak comments for key naming
markerikson Jul 23, 2016
0f9fd70
Add a bit more explanation of object key naming
markerikson Jul 23, 2016
d658b8b
Add initial draft for "Beyond combineReducers"
markerikson Jul 24, 2016
aed74c5
Tweak wording in a few spots
markerikson Jul 24, 2016
dc9a992
More prereq links, and another note on combineReducers
markerikson Jul 24, 2016
364c77f
First draft of "Normalizing State Shape"
markerikson Jul 24, 2016
5e205b1
Add additional notes on normalization usage
markerikson Jul 25, 2016
518234a
Tweak wording and fix typos
markerikson Jul 25, 2016
63fe88c
Clarify reduceReducers example
markerikson Jul 25, 2016
ee5cc12
Tweak wording
markerikson Jul 25, 2016
665435d
Fixes a typo
epeicher Jul 25, 2016
061d21f
Merge pull request #3 from epeicher/patch-1
markerikson Jul 25, 2016
2da7cb5
Add "Reusing Reducer Logic" page
markerikson Aug 14, 2016
3d934fb
Add "Immutable Update Patterns" page
markerikson Aug 14, 2016
cc94275
Fix wrong invocation of toggleTodo which should be editTodo
pgilad Aug 26, 2016
c5b7602
Merge pull request #4 from pgilad/patch-1
markerikson Aug 26, 2016
b6e5c9e
Add "Managing Normalized Data" page
markerikson Sep 4, 2016
0eb54f1
Tweak phrasing in "Managing Normalizing Data"
markerikson Sep 4, 2016
8b467ee
Add "Initializing State"
markerikson Sep 4, 2016
3bc8af1
Tweak section headings
markerikson Sep 4, 2016
a510322
Address review comments for "Structuring Reducers"
markerikson Sep 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/recipes/StructuringReducers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Structuring Reducers

At its core, Redux is really a fairly simple design pattern: all your "write" logic goes into a single function, and the only way to run that logic is to give Redux a plain object that describes something that has happened. The Redux store calls that write logic function and passes in the current state tree and the descriptive object, the write logic function returns some new state tree, and the Redux store notifies any subscribers that the state tree has changed.

Redux puts some basic constraints on how that write logic function should work. As described in [Reducers](../basics/Reducers.md), it has to have a signature of `(previousState, action) => newState`, is known as a ***reducer function***, and must be *pure* and predictable.

Beyond that, Redux does not really care how you actually structure your logic inside that reducer function, as long as it obeys those basic rules. This is both a source of freedom and a source of confusion. However, there are a number of common patterns that are widely used when writing reducers, as well as a number of related topics and concepts to be aware of. As an application grows, these patterns play a crucial role in managing reducer code complexity, handling real-world data, and optimizing UI performance.


### Prerequisite Concepts for Writing Reducers

Some of these concepts are already described elsewhere in the Redux documentation. Others are generic and applicable outside of Redux itself, and there are numerous existing articles that cover these concepts in detail. These concepts and techniques form the foundation of writing solid Redux reducer logic.

It is vital that these Prerequisite Concepts are **thoroughly understood** before moving on to more advanced and Redux-specific techniques. A recommended reading list is available at:

#### [Prerequisite Concepts](./reducers/00-PrerequisiteConcepts.md)

It's also important to note that some of these suggestions may or may not be directly applicable based on architectural decisions in a specific application. For example, an application using Immutable.js Maps to store data would likely have its reducer logic structured at least somewhat differently than an application using plain Javascript objects. This documentation primarily assumes use of plain Javascript objects, but many of the principles would still apply if using other tools.



### Reducer Concepts and Techniques

- [Basic Reducer Structure](./reducers/01-BasicReducerStructure.md)
- [Splitting Reducer Logic](./reducers/02-SplittingReducerLogic.md)
- [Refactoring Reducers Example](./reducers/03-RefactoringReducersExample.md)
- [Using `combineReducers`](./reducers/04-UsingCombineReducers.md)
- [Beyond `combineReducers`](./reducers/05-BeyondCombineReducers.md)
- [Normalizing State Shape](./reducers/06-NormalizingStateShape.md)
- [Updating Normalized Data](./reducers/07-UpdatingNormalizedData.md)
- [Reusing Reducer Logic](./reducers/08-ReusingReducerLogic.md)
- [Immutable Update Patterns](./reducers/09-ImmutableUpdatePatterns.md)
- [Initializing State](./reducers/10-InitializingState.md)
93 changes: 93 additions & 0 deletions docs/recipes/reducers/00-PrerequisiteConcepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Prerequisite Reducer Concepts


As described in [Reducers](../../basics/Reducers.md), a Redux reducer function:

- Should have a signature of `(previousState, action) => newState`, similar to the type of function you would pass to [`Array.prototype.reduce(reducer, ?initialValue)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)
- Should be "pure", which means it does not mutate its arguments, perform side effects like API calls or modifying values outside of the function, or call non-pure functions like `Date.now()` or `Math.random()`. This also means that updates should be done in an ***"immutable"*** fashion, which means **always returning new objects with the updated data**, rather than directly modifying the original state tree in-place.

>##### Note on immutability, side effects, and mutation
> Mutation is discouraged because it generally breaks time-travel debugging, and React Redux's `connect` function:
> - For time traveling, the Redux DevTools expect that replaying recorded actions would output a state value, but not change anything else. **Side effects like mutation or asynchronous behavior will cause time travel to alter behavior between steps, breaking the application**.
> - For React Redux, `connect` checks to see if the props returned from a `mapStateToProps` function have changed in order to determine if a component needs to update. To improve performance, `connect` takes some shortcuts that rely on the state being immutable, and uses shallow reference equality checks to detect changes. This means that **changes made to objects and arrays by direct mutation will not be detected, and components will not re-render**.
>
> Other side effects like generating unique IDs or timestamps in a reducer also make the code unpredictable and harder to debug and test.


Because of these rules, it's important that the following core concepts are fully understood before moving on to other specific techniques for organizing Redux reducers:

#### Redux Reducer Basics

**Key concepts**:

- Thinking in terms of state and state shape
- Delegating update responsibility by slice of state (*reducer composition*)
- Higher order reducers
- Defining reducer initial state

**Reading list**:

- [Redux Docs: Reducers](../../basics/Reducers.md)
- [Redux Docs: Reducing Boilerplate](../ReducingBoilerplate.md)
- [Redux Docs: Implementing Undo History](../ImplementingUndoHistory.md)
- [Redux Docs: `combineReducers`](../../api/combineReducers.md)
- [The Power of Higher-Order Reducers](http://slides.com/omnidan/hor#/)
- [Stack Overflow: Store initial state and `combineReducers`](http://stackoverflow.com/questions/33749759/read-stores-initial-state-in-redux-reducer)
- [Stack Overflow: State key names and `combineReducers`](http://stackoverflow.com/questions/35667775/state-in-redux-react-app-has-a-property-with-the-name-of-the-reducer)


#### Pure Functions and Side Effects

**Key Concepts**:

- Side effects
- Pure functions
- How to think in terms of combining functions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this reads slightly better as:

Key Concepts:

  • Side effects
  • Pure functions
  • How to think in terms of combining functions


**Reading List**:

- [The Little Idea of Functional Programming](http://jaysoo.ca/2016/01/13/functional-programming-little-ideas/)
- [Understanding Programmatic Side Effects](http://web24studios.com/2015/10/understanding-programmatic-side-effects/)
- [Learning Functional Programming in Javascript](https://youtu.be/e-5obm1G_FY)
- [An Introduction to Reasonably Pure Functional Programming](https://www.sitepoint.com/an-introduction-to-reasonably-pure-functional-programming/)



#### Immutable Data Management

**Key Concepts**:

- Mutability vs immutability
- Immutably updating objects and arrays safely
- Avoiding functions and statements that mutate state

**Reading List**:

- [Pros and Cons of Using Immutability With React](http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/)
- [Javascript and Immutability](http://t4d.io/javascript-and-immutability/)
- [Immutable Data using ES6 and Beyond](http://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/)
- [Immutable Data from Scratch](https://ryanfunduk.com/articles/immutable-data-from-scratch/)
- [Redux Docs: Using the Object Spread Operator](../UsingObjectSpreadOperator.md)


#### Normalizing Data

**Key Concepts**:

- Database structure and organization
- Splitting relational/nested data up into separate tables
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it it clear what "tables" refers to here. Am I right that this item means that the reader should understand database normalization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, since that is the suggested structure for organizing nested/relational data in a Redux store.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, thanks for taking the time to review this!)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem! Databases haven't been mentioned yet in this doc is all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the "Normalizing data" pages :)

Copy link
Contributor

@ellbee ellbee Sep 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I mean that from the point of view of someone reading this doc for the first time there has been no mention of databases by the time they get to this mention of tables. I was just wondering if people are going to be like "huh, what's a JavaScript table?". I mean, they won't but they might not jump to database tables immediately either. I dunno, the more I talk about it the less important it seems 😄

Am I right that this item means that the reader should understand database normalization?

I asked this because I wasn't clear if you were saying that a reader would have to specifically understand database normalization to be able to read the Normalizing Data section. Having read that section now, I am not sure they do. In fact, I am not sure that a reader has to understand any of the concepts listed as prerequisites for Normalizing Data prior to reading it, they are thoroughly explained in the section itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my first attempt at writing a couple of these sections, I found myself totally trying to explain concepts like "immutable data" from the ground up, and realized I was totally wasting effort re-explaining stuff that was better written and explained elsewhere. I did do some explanation of normalization concepts and terms in those pages, but didn't go fully in depth.

Any suggestions for better handling the "prereqs" aspect? Some way to better tie it in to the rest of the writing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked this because I wasn't clear if you were saying that a reader would have to specifically understand database normalization to be able to read the Normalizing Data section.

Interesting related discussion at choojs/choo#252 (comment). In particular:

At first blush, the idea of "importing" relations or graph structures, wholesale, kind of turns my stomach. Either would bring with it a whole mound of concepts dwarfing all the other fundamental primitives in choo combined. "If you wish to make a choo app from scratch, you must first understand relational theory?"

- Storing a single definition for a given item
- Referring to items by IDs
- Using objects keyed by item IDs as lookup tables, and arrays of IDs to track ordering
- Associating items in relationships


**Reading List**:

- [Database Normalization in Simple English](http://www.essentialsql.com/get-ready-to-learn-sql-database-normalization-explained-in-simple-english/)
- [Idiomatic Redux: Normalizing the State Shape](https://egghead.io/lessons/javascript-redux-normalizing-the-state-shape)
- [Normalizr Documentation](https://github.com/paularmstrong/normalizr)
- [Redux Without Profanity: Normalizr](https://tonyhb.gitbooks.io/redux-without-profanity/content/normalizer.html)
- [Querying a Redux Store](https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f)
- [Wikipedia: Associative Entity](https://en.wikipedia.org/wiki/Associative_entity)
- [Database Design: Many-to-Many](http://www.tomjewett.com/dbdesign/dbdesign.php?page=manymany.php)
98 changes: 98 additions & 0 deletions docs/recipes/reducers/01-BasicReducerStructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Basic Reducer Structure and State Shape

## Basic Reducer Structure
First and foremost, it's important to understand that your entire application really only has **one single reducer function**: the function that you've passed into `createStore` as the first argument. That one single reducer function ultimately needs to do several things:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really important point, and you've made it very clear here 👍


- The first time the reducer is called, the `state` value will be `undefined`. The reducer needs to handle this case by supplying a default state value before handling the incoming action.
- It needs to look at the previous state and the dispatched action, and determine what kind of work needs to be done
- Assuming actual changes need to occur, it needs to create new objects and arrays with the updated data and return those
- If no changes are needed, it should return the existing state as-is.

The simplest possible approach to writing reducer logic is to put everything into a single function declaration, like this:

```js
function counter(state, action) {
if (typeof state === 'undefined') {
state = 0; // If state is undefined, initialize it with a default value
}

if (action.type === 'INCREMENT') {
return state + 1;
}
else if (action.type === 'DECREMENT') {
return state - 1;
}
else {
return state; // In case an action is passed in we don't understand
}
}
```

Notice that this simple function fulfills all the basic requirements. It returns a default value if none exists, initializing the store; it determines what sort of update needs to be done based on the type of the action, and returns new values; and it returns the previous state if no work needs to be done.

There are some simple tweaks that can be made to this reducer. First, repeated `if`/`else` statements quickly grow tiresome, so it's very common to use `switch` statements instead. Second, we can use ES6's default parameter values to handle the initial "no existing data" case. With those changes, the reducer would look like:

```js
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
```

This is the basic structure that a typical Redux reducer function uses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I like the narrative progression here, and the refactoring of the basic reducer structure, I wonder if it would be better to have only the latter code example. That way, a hurried reader doesn't just grab the first example and use it without reading on. If there's only one code snippet, that's what people will grab.

The advantage of the first snippet is that it doesn't use ES6 features, but I'm not what our target is, so that may or may not be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs certainly use ES6 just about everywhere, but given that this is intended as a "tutorial" / "how-to" section, the progression here is definitely deliberate. I would hope that someone reading this would be looking more to read and learn than "grab a snippet and go".


## Basic State Shape

Redux encourages you to think about your application in terms of the data you need to manage. The data at any given point in time is the "*state*" of your application, and the structure and organization of that state is typically referred to as its "*shape*". The shape of your state plays a major role in how you structure your reducer logic.

A Redux state usually has a plain Javascript object as the top of the state tree. (It is certainly possible to have another type of data instead, such as a single number, an array, or a specialized data structure, but most libraries assume that the top-level value is a plain object.) The most common way to organize data within that top-level object is to further divide data into sub-trees, where each top-level key represents some "domain" or "slice" of related data. For example, a basic Todo app's state might look like:

```js
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
```

In this example, `todos` and `visibilityFilter` are both top-level keys in the state, and each represents a "slice" of data for some particular concept.

Most applications deal with multiple types of data, which can be broadly divided into three categories:

- _Domain data_: data that the application needs to show, use, or modify (such as "all of the Todos retrieved from the server")
- _App state_: data that is specific to the application's behavior (such as "Todo #5 is currently selected", or "there is a request in progress to fetch Todos")
- _UI state_: data that represents how the UI is currently displayed (such as "The EditTodo modal dialog is currently open")


Because the store represents the core of your application, you should **define your state shape in terms of your domain data and app state, not your UI component tree**. As an example, a shape of `state.leftPane.todoList.todos` would be a bad idea, because the idea of "todos" is central to the whole application, not just a single part of the UI. The `todos` slice should be at the top of the state tree instead.

There will *rarely* be a 1-to-1 correspondence between your UI tree and your state shape. The exception to that might be if you are explicitly tracking various aspects of UI data in your Redux store as well, but even then the shape of the UI data and the shape of the domain data would likely be different.

A typical app's state shape might look roughly like:

```js
{
domainData1 : {},
domainData2 : {},
appState1 : {},
appState2 : {},
ui : {
uiState1 : {},
uiState2 : {},
}
}
```
25 changes: 25 additions & 0 deletions docs/recipes/reducers/02-SplittingReducerLogic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Splitting Up Reducer Logic

For any meaningful application, putting *all* your update logic into a single reducer function is quickly going to become unmaintainable. While there's no single rule for how long a function should be, it's generally agreed that functions should be relatively short and ideally only do one specific thing. Because of this, it's good programming practice to take pieces of code that are very long or do many different things, and break them into smaller pieces that are easier to understand.

Since a Redux reducer is *just* a function, the same concept applies. You can split some of your reducer logic out into another function, and call that new function from the parent function.

These new functions would typically fall into one of three categories:

1. Small utility functions containing some reusable chunk of logic that is needed in multiple places (which may or may not be actually related to the specific business logic)
2. Functions for handling a specific update case, which often need parameters other than the typical `(state, action)` pair
3. Functions which handle *all* updates for a given slice of state. These functions do generally have the typical `(state, action)` parameter signature


For clarity, these terms will be used to distinguish between different types of functions and different use cases:

- ***reducer***: any function with the signature `(state, action) -> newState` (ie, any function that *could* be used as an argument to `Array.reduce`)
- ***root reducer***: the reducer function that is actually passed as the first argument to `createStore`. This is the only part of the reducer logic that _must_ have the `(state, action) -> newState` signature.
- ***slice reducer***: a reducer that is being used to handle updates to one specific slice of the state tree, usually done by passing it to `combineReducers`
- ***case function***: a function that is being used to handle the update logic for a specific action. This may actually be a reducer function, or it may require other parameters to do its work properly.
- ***higher-order reducer***: a function that takes a reducer function as an argument, and/or returns a new reducer function as a result (such as `combineReducers`, or `redux-undo`

The term "*sub-reducer*" has also been used in various discussions to mean any function that is not the root reducer, although the term is not very precise. Some people may also refer to some functions as "*business logic*" (functions that relate to application-specific behavior) or "*utility functions*" (generic functions that are not application-specific).


Breaking down a complex process into smaller, more understandable parts is usually described with the term ***[functional decomposition](http://stackoverflow.com/questions/947874/what-is-functional-decomposition)***. This term and concept can be applied generically to any code. However, in Redux it is *very* common to structure reducer logic using approach #3, where update logic is delegated to other functions based on slice of state. Redux refers to this concept as ***reducer composition***, and it is by far the most widely-used approach to structuring reducer logic. In fact, it's so common that Redux includes a utility function called [`combineReducers()`](../../api/combineReducers.md), which specifically abstracts the process of delegating work to other reducer functions based on slices of state. However, it's important to note that it is not the *only* pattern that can be used. In fact, it's entirely possible to use all three approaches for splitting up logic into functions, and usually a good idea as well. The [Refactoring Reducers](./03-RefactoringReducers.md) section shows some examples of this in action.
Loading