Skip to content

Commit 445183a

Browse files
committed
Merge pull request reduxjs#1516 from ellbee/update_computing_derived_data
Update ComputingDerivedData.md
2 parents 9a9349b + a982b6f commit 445183a

File tree

1 file changed

+199
-3
lines changed

1 file changed

+199
-3
lines changed

docs/recipes/ComputingDerivedData.md

Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Computing Derived Data
22

3-
[Reselect](https://github.com/faassen/reselect.git) is a simple library for creating memoized, composable **selector** functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.
3+
[Reselect](https://github.com/reactjs/reselect) is a simple library for creating memoized, composable **selector** functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.
44

55
### Motivation for Memoized Selectors
66

@@ -54,7 +54,7 @@ We would like to replace `getVisibleTodos` with a memoized selector that recalcu
5454

5555
Reselect provides a function `createSelector` for creating memoized selectors. `createSelector` takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is mutated in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.
5656

57-
Let's define a memoized selector named `getVisibleTodos` to replace the non-memoized version above:
57+
Lets define a memoized selector named `getVisibleTodos` to replace the non-memoized version above:
5858

5959
#### `selectors/index.js`
6060

@@ -130,6 +130,202 @@ const VisibleTodoList = connect(
130130
export default VisibleTodoList
131131
```
132132

133+
### Accessing React Props in Selectors
134+
135+
> This section introduces an hypothetical extension to our app that allows it to support multiple Todo Lists. Please note that a full implementation of this extension requires changes to the reducers, components, actions etc. that aren’t directly relevant to the topics discussed and have been omitted for brevity.
136+
137+
So far we have only seen selectors receive the Redux store state as an argument, but a selector can receive props too.
138+
139+
Here is an `App` component that renders three `VisibleTodoList` components, each of which has a `listId` prop:
140+
141+
#### `components/App.js`
142+
143+
```js
144+
import React from 'react'
145+
import Footer from './Footer'
146+
import AddTodo from '../containers/AddTodo'
147+
import VisibleTodoList from '../containers/VisibleTodoList'
148+
149+
const App = () => (
150+
<div>
151+
<VisibleTodoList listId="1" />
152+
<VisibleTodoList listId="2" />
153+
<VisibleTodoList listId="3" />
154+
</div>
155+
)
156+
```
157+
158+
Each `VisibleTodoList` container should select a different slice of the state depending on the value of the `listId` prop, so let’s modify `getVisibilityFilter` and `getTodos` to accept a props argument:
159+
160+
#### `selectors/todoSelectors.js`
161+
162+
```js
163+
import { createSelector } from 'reselect'
164+
165+
const getVisibilityFilter = (state, props) =>
166+
state.todoLists[props.listId].visibilityFilter
167+
168+
const getTodos = (state, props) =>
169+
state.todoLists[props.listId].todos
170+
171+
const getVisibleTodos = createSelector(
172+
[ getVisibilityFilter, getTodos ],
173+
(visibilityFilter, todos) => {
174+
switch (visibilityFilter) {
175+
case 'SHOW_COMPLETED':
176+
return todos.filter(todo => todo.completed)
177+
case 'SHOW_ACTIVE':
178+
return todos.filter(todo => !todo.completed)
179+
default:
180+
return todos
181+
}
182+
}
183+
)
184+
185+
export default getVisibleTodos
186+
```
187+
188+
`props` can be passed to `getVisibleTodos` from `mapStateToProps`:
189+
190+
```js
191+
const mapStateToProps = (state, props) => {
192+
return {
193+
todos: getVisibleTodos(state, props)
194+
}
195+
}
196+
```
197+
198+
So now `getVisibleTodos` has access to `props`, and everything seems to be working fine.
199+
200+
**But there is a problem!**
201+
202+
Using the `getVisibleTodos` selector with multiple instances of the `visibleTodoList` container will not correctly memoize:
203+
204+
#### `containers/VisibleTodoList.js`
205+
206+
```js
207+
import { connect } from 'react-redux'
208+
import { toggleTodo } from '../actions'
209+
import TodoList from '../components/TodoList'
210+
import { getVisibleTodos } from '../selectors'
211+
212+
const mapStateToProps = (state, props) => {
213+
return {
214+
// WARNING: THE FOLLOWING SELECTOR DOES NOT CORRECTLY MEMOIZE
215+
todos: getVisibleTodos(state, props)
216+
}
217+
}
218+
219+
const mapDispatchToProps = (dispatch) => {
220+
return {
221+
onTodoClick: (id) => {
222+
dispatch(toggleTodo(id))
223+
}
224+
}
225+
}
226+
227+
const VisibleTodoList = connect(
228+
mapStateToProps,
229+
mapDispatchToProps
230+
)(TodoList)
231+
232+
export default VisibleTodoList
233+
```
234+
235+
A selector created with `createSelector` only returns the cached value when its set of arguments is the same as its previous set of arguments. If we alternate between rendering `<VisibleTodoList listId="1" />` and `<VisibleTodoList listId="2" />`, the shared selector will alternate between receiving `{listId: 1}` and `{listId: 2}` as its `props` argument. This will cause the arguments to be different on each call, so the selector will always recompute instead of returning the cached value. We’ll see how to overcome this limitation in the next section.
236+
237+
### Sharing Selectors Across Multiple Components
238+
239+
> The examples in this section require React Redux v4.3.0 or greater
240+
241+
In order to share a selector across multiple `VisibleTodoList` components **and** retain memoization, each instance of the component needs its own private copy of the selector.
242+
243+
Let’s create a function named `makeGetVisibleTodos` that returns a new copy of the `getVisibleTodos` selector each time it is called:
244+
245+
#### `selectors/todoSelectors.js`
246+
247+
```js
248+
import { createSelector } from 'reselect'
249+
250+
const getVisibilityFilter = (state, props) =>
251+
state.todoLists[props.listId].visibilityFilter
252+
253+
const getTodos = (state, props) =>
254+
state.todoLists[props.listId].todos
255+
256+
const makeGetVisibleTodos = () => {
257+
return createSelector(
258+
[ getVisibilityFilter, getTodos ],
259+
(visibilityFilter, todos) => {
260+
switch (visibilityFilter) {
261+
case 'SHOW_COMPLETED':
262+
return todos.filter(todo => todo.completed)
263+
case 'SHOW_ACTIVE':
264+
return todos.filter(todo => !todo.completed)
265+
default:
266+
return todos
267+
}
268+
}
269+
)
270+
}
271+
272+
export default makeGetVisibleTodos
273+
```
274+
275+
We also need a way to give each instance of a container access to its own private selector. The `mapStateToProps` argument of `connect` can help with this.
276+
277+
**If the `mapStateToProps` argument supplied to `connect` returns a function instead of an object, it will be used to create an individual `mapStateToProps` function for each instance of the container.**
278+
279+
In the example below `makeMapStateToProps` creates a new `getVisibleTodos` selector, and returns a `mapStateToProps` function that has exclusive access to the new selector:
280+
281+
```js
282+
const makeMapStateToProps = () => {
283+
const getVisibleTodos = makeGetVisibleTodos()
284+
const mapStateToProps = (state, props) => {
285+
return {
286+
todos: getVisibleTodos(state, props)
287+
}
288+
}
289+
return mapStateToProps
290+
}
291+
```
292+
293+
If we pass `makeMapStateToProps` to `connect`, each instance of the `VisibleTodosList` container will get its own `mapStateToProps` function with a private `getVisibleTodos` selector. Memoization will now work correctly regardless of the render order of the `VisibleTodoList` containers.
294+
295+
#### `containers/VisibleTodoList.js`
296+
297+
```js
298+
import { connect } from 'react-redux'
299+
import { toggleTodo } from '../actions'
300+
import TodoList from '../components/TodoList'
301+
import { makeGetVisibleTodos } from '../selectors'
302+
303+
const makeMapStateToProps= () => {
304+
const getVisibleTodos = makeGetVisibleTodos()
305+
const mapStateToProps = (state, props) => {
306+
return {
307+
todos: getVisibleTodos(state, props)
308+
}
309+
}
310+
return mapStateToProps
311+
}
312+
313+
const mapDispatchToProps = (dispatch) => {
314+
return {
315+
onTodoClick: (id) => {
316+
dispatch(toggleTodo(id))
317+
}
318+
}
319+
}
320+
321+
const VisibleTodoList = connect(
322+
makeMapStateToProps,
323+
mapDispatchToProps
324+
)(TodoList)
325+
326+
export default VisibleTodoList
327+
```
328+
133329
## Next Steps
134330

135-
Check out the [official documentation](https://github.com/rackt/reselect) of Reselect as well as its [FAQ](https://github.com/rackt/reselect#faq). Most Redux projects start using Reselect when they have performance problems because of too many derived computations and wasted re-renders, so make sure you are familiar with it before you build something big. It can also be useful to study [its source code](https://github.com/rackt/reselect/blob/master/src/index.js) so you don’t think it’s magic.
331+
Check out the [official documentation](https://github.com/reactjs/reselect) of Reselect as well as its [FAQ](https://github.com/reactjs/reselect#faq). Most Redux projects start using Reselect when they have performance problems because of too many derived computations and wasted re-renders, so make sure you are familiar with it before you build something big. It can also be useful to study [its source code](https://github.com/reactjs/reselect/blob/master/src/index.js) so you don’t think it’s magic.

0 commit comments

Comments
 (0)