|
1 | 1 | # Computing Derived Data |
2 | 2 |
|
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. |
4 | 4 |
|
5 | 5 | ### Motivation for Memoized Selectors |
6 | 6 |
|
@@ -54,7 +54,7 @@ We would like to replace `getVisibleTodos` with a memoized selector that recalcu |
54 | 54 |
|
55 | 55 | 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. |
56 | 56 |
|
57 | | -Let's define a memoized selector named `getVisibleTodos` to replace the non-memoized version above: |
| 57 | +Let’s define a memoized selector named `getVisibleTodos` to replace the non-memoized version above: |
58 | 58 |
|
59 | 59 | #### `selectors/index.js` |
60 | 60 |
|
@@ -130,6 +130,202 @@ const VisibleTodoList = connect( |
130 | 130 | export default VisibleTodoList |
131 | 131 | ``` |
132 | 132 |
|
| 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 | + |
133 | 329 | ## Next Steps |
134 | 330 |
|
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