Skip to content

Commit d110b84

Browse files
markeriksontimdorr
authored andcommitted
Structuring reducers page (#1930)
* Add an initial empty "Structuring Reducers" recipe. * Add initial intro text * Add "Basic Reducer Structure" section * Add initial "Basic State Shape" and "Splitting Reducer Logic" sections Note that a _lot_ of this content duplicates the existing "Reducers" doc page, which bothers me, but we'll work that out later. * Define terms and various function concepts * Add reducer refactoring examples * Add unfinished "Immutable Data" topic, reducer terms, and mutation note * Split "Structuring Reducers" into multiple pages * Add Prerequisite Concepts content * Really emphasize the prerequisites * Update StructuringReducers.md * Add example state shape and note about refactoring example * Add additional prerequisite links * Expand reducer refactoring example * Add more prerequisite links, and tweak example identifiers * Rework reducer terminology * Initial draft of "Using combineReducers" * Tweak comments for key naming * Add a bit more explanation of object key naming * Add initial draft for "Beyond combineReducers" * Tweak wording in a few spots * More prereq links, and another note on combineReducers * First draft of "Normalizing State Shape" * Add additional notes on normalization usage * Tweak wording and fix typos * Clarify reduceReducers example * Tweak wording * Fixes a typo * Add "Reusing Reducer Logic" page * Add "Immutable Update Patterns" page * Fix wrong invocation of toggleTodo which should be editTodo * Add "Managing Normalized Data" page * Tweak phrasing in "Managing Normalizing Data" * Add "Initializing State" * Tweak section headings * Address review comments for "Structuring Reducers" Adjust size of "Prerequisite Concepts" link Clarify wording in "Mutation" note Fix behavior in basic ES5 counter reducer example Clarify use of top-level object Add description of domain state, app state, and UI state Remove emphasis on "business" and "utility" functions Rework "Refactoring Reducers" example to cut down on repetition Remove duplication between "Using combineReducers" and "Initializing State" Remove uses of "obviously" Add note on use of reduceReducers Replace "O(n)" and "linear scan" with "a single loop" Clarify use of Lodash merge Emphasize need to update all levels of nesting Highlight common mistakes in updating
1 parent 5502940 commit d110b84

12 files changed

+1759
-0
lines changed

docs/recipes/StructuringReducers.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Structuring Reducers
2+
3+
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.
4+
5+
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.
6+
7+
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.
8+
9+
10+
### Prerequisite Concepts for Writing Reducers
11+
12+
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.
13+
14+
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:
15+
16+
#### [Prerequisite Concepts](./reducers/00-PrerequisiteConcepts.md)
17+
18+
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.
19+
20+
21+
22+
### Reducer Concepts and Techniques
23+
24+
- [Basic Reducer Structure](./reducers/01-BasicReducerStructure.md)
25+
- [Splitting Reducer Logic](./reducers/02-SplittingReducerLogic.md)
26+
- [Refactoring Reducers Example](./reducers/03-RefactoringReducersExample.md)
27+
- [Using `combineReducers`](./reducers/04-UsingCombineReducers.md)
28+
- [Beyond `combineReducers`](./reducers/05-BeyondCombineReducers.md)
29+
- [Normalizing State Shape](./reducers/06-NormalizingStateShape.md)
30+
- [Updating Normalized Data](./reducers/07-UpdatingNormalizedData.md)
31+
- [Reusing Reducer Logic](./reducers/08-ReusingReducerLogic.md)
32+
- [Immutable Update Patterns](./reducers/09-ImmutableUpdatePatterns.md)
33+
- [Initializing State](./reducers/10-InitializingState.md)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Prerequisite Reducer Concepts
2+
3+
4+
As described in [Reducers](../../basics/Reducers.md), a Redux reducer function:
5+
6+
- 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)
7+
- 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.
8+
9+
>##### Note on immutability, side effects, and mutation
10+
> Mutation is discouraged because it generally breaks time-travel debugging, and React Redux's `connect` function:
11+
> - 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**.
12+
> - 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**.
13+
>
14+
> Other side effects like generating unique IDs or timestamps in a reducer also make the code unpredictable and harder to debug and test.
15+
16+
17+
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:
18+
19+
#### Redux Reducer Basics
20+
21+
**Key concepts**:
22+
23+
- Thinking in terms of state and state shape
24+
- Delegating update responsibility by slice of state (*reducer composition*)
25+
- Higher order reducers
26+
- Defining reducer initial state
27+
28+
**Reading list**:
29+
30+
- [Redux Docs: Reducers](../../basics/Reducers.md)
31+
- [Redux Docs: Reducing Boilerplate](../ReducingBoilerplate.md)
32+
- [Redux Docs: Implementing Undo History](../ImplementingUndoHistory.md)
33+
- [Redux Docs: `combineReducers`](../../api/combineReducers.md)
34+
- [The Power of Higher-Order Reducers](http://slides.com/omnidan/hor#/)
35+
- [Stack Overflow: Store initial state and `combineReducers`](http://stackoverflow.com/questions/33749759/read-stores-initial-state-in-redux-reducer)
36+
- [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)
37+
38+
39+
#### Pure Functions and Side Effects
40+
41+
**Key Concepts**:
42+
43+
- Side effects
44+
- Pure functions
45+
- How to think in terms of combining functions
46+
47+
**Reading List**:
48+
49+
- [The Little Idea of Functional Programming](http://jaysoo.ca/2016/01/13/functional-programming-little-ideas/)
50+
- [Understanding Programmatic Side Effects](http://web24studios.com/2015/10/understanding-programmatic-side-effects/)
51+
- [Learning Functional Programming in Javascript](https://youtu.be/e-5obm1G_FY)
52+
- [An Introduction to Reasonably Pure Functional Programming](https://www.sitepoint.com/an-introduction-to-reasonably-pure-functional-programming/)
53+
54+
55+
56+
#### Immutable Data Management
57+
58+
**Key Concepts**:
59+
60+
- Mutability vs immutability
61+
- Immutably updating objects and arrays safely
62+
- Avoiding functions and statements that mutate state
63+
64+
**Reading List**:
65+
66+
- [Pros and Cons of Using Immutability With React](http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/)
67+
- [Javascript and Immutability](http://t4d.io/javascript-and-immutability/)
68+
- [Immutable Data using ES6 and Beyond](http://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/)
69+
- [Immutable Data from Scratch](https://ryanfunduk.com/articles/immutable-data-from-scratch/)
70+
- [Redux Docs: Using the Object Spread Operator](../UsingObjectSpreadOperator.md)
71+
72+
73+
#### Normalizing Data
74+
75+
**Key Concepts**:
76+
77+
- Database structure and organization
78+
- Splitting relational/nested data up into separate tables
79+
- Storing a single definition for a given item
80+
- Referring to items by IDs
81+
- Using objects keyed by item IDs as lookup tables, and arrays of IDs to track ordering
82+
- Associating items in relationships
83+
84+
85+
**Reading List**:
86+
87+
- [Database Normalization in Simple English](http://www.essentialsql.com/get-ready-to-learn-sql-database-normalization-explained-in-simple-english/)
88+
- [Idiomatic Redux: Normalizing the State Shape](https://egghead.io/lessons/javascript-redux-normalizing-the-state-shape)
89+
- [Normalizr Documentation](https://github.com/paularmstrong/normalizr)
90+
- [Redux Without Profanity: Normalizr](https://tonyhb.gitbooks.io/redux-without-profanity/content/normalizer.html)
91+
- [Querying a Redux Store](https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f)
92+
- [Wikipedia: Associative Entity](https://en.wikipedia.org/wiki/Associative_entity)
93+
- [Database Design: Many-to-Many](http://www.tomjewett.com/dbdesign/dbdesign.php?page=manymany.php)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Basic Reducer Structure and State Shape
2+
3+
## Basic Reducer Structure
4+
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:
5+
6+
- 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.
7+
- It needs to look at the previous state and the dispatched action, and determine what kind of work needs to be done
8+
- Assuming actual changes need to occur, it needs to create new objects and arrays with the updated data and return those
9+
- If no changes are needed, it should return the existing state as-is.
10+
11+
The simplest possible approach to writing reducer logic is to put everything into a single function declaration, like this:
12+
13+
```js
14+
function counter(state, action) {
15+
if (typeof state === 'undefined') {
16+
state = 0; // If state is undefined, initialize it with a default value
17+
}
18+
19+
if (action.type === 'INCREMENT') {
20+
return state + 1;
21+
}
22+
else if (action.type === 'DECREMENT') {
23+
return state - 1;
24+
}
25+
else {
26+
return state; // In case an action is passed in we don't understand
27+
}
28+
}
29+
```
30+
31+
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.
32+
33+
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:
34+
35+
```js
36+
function counter(state = 0, action) {
37+
switch (action.type) {
38+
case 'INCREMENT':
39+
return state + 1;
40+
case 'DECREMENT':
41+
return state - 1;
42+
default:
43+
return state;
44+
}
45+
}
46+
```
47+
48+
This is the basic structure that a typical Redux reducer function uses.
49+
50+
## Basic State Shape
51+
52+
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.
53+
54+
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:
55+
56+
```js
57+
{
58+
visibilityFilter: 'SHOW_ALL',
59+
todos: [
60+
{
61+
text: 'Consider using Redux',
62+
completed: true,
63+
},
64+
{
65+
text: 'Keep all state in a single tree',
66+
completed: false
67+
}
68+
]
69+
}
70+
```
71+
72+
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.
73+
74+
Most applications deal with multiple types of data, which can be broadly divided into three categories:
75+
76+
- _Domain data_: data that the application needs to show, use, or modify (such as "all of the Todos retrieved from the server")
77+
- _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")
78+
- _UI state_: data that represents how the UI is currently displayed (such as "The EditTodo modal dialog is currently open")
79+
80+
81+
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.
82+
83+
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.
84+
85+
A typical app's state shape might look roughly like:
86+
87+
```js
88+
{
89+
domainData1 : {},
90+
domainData2 : {},
91+
appState1 : {},
92+
appState2 : {},
93+
ui : {
94+
uiState1 : {},
95+
uiState2 : {},
96+
}
97+
}
98+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Splitting Up Reducer Logic
2+
3+
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.
4+
5+
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.
6+
7+
These new functions would typically fall into one of three categories:
8+
9+
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)
10+
2. Functions for handling a specific update case, which often need parameters other than the typical `(state, action)` pair
11+
3. Functions which handle *all* updates for a given slice of state. These functions do generally have the typical `(state, action)` parameter signature
12+
13+
14+
For clarity, these terms will be used to distinguish between different types of functions and different use cases:
15+
16+
- ***reducer***: any function with the signature `(state, action) -> newState` (ie, any function that *could* be used as an argument to `Array.reduce`)
17+
- ***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.
18+
- ***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`
19+
- ***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.
20+
- ***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`
21+
22+
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).
23+
24+
25+
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.

0 commit comments

Comments
 (0)