-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
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
Changes from all commits
55893c8
007c713
6392e85
3bc6be6
152d58a
9a606ee
aca189f
ccd2f6c
418abf5
aa64357
f2275b1
a436f1b
b5cfd82
1efe2e3
27bd414
0711717
39476da
4b15066
3987892
0f9fd70
d658b8b
aed74c5
dc9a992
364c77f
5e205b1
518234a
63fe88c
ee5cc12
665435d
061d21f
2da7cb5
3d934fb
cc94275
c5b7602
b6e5c9e
0eb54f1
8b467ee
3bc8af1
a510322
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) |
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 | ||
|
||
**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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Also, thanks for taking the time to review this!) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the "Normalizing data" pages :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 😄
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Interesting related discussion at choojs/choo#252 (comment). In particular:
|
||
- 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) |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 : {}, | ||
} | ||
} | ||
``` |
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. |
There was a problem hiding this comment.
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: