Skip to content

Commit 0ea274a

Browse files
MrWolfZmarkerikson
authored andcommitted
adjust the hooks docs with more details about useSelector and add some more examples (#1267)
* adjust the hooks docs with more details about the inner workings of useSelector and add some more examples * update hooks docs for removal of `useSelector` deps; add examples for usage with memoizing selectors; removed code example for custom `useActions` hook to not second-guess our own API design decision; some other minor docs tweaks * in the hooks doc code examples change nr to num or number * Update useSelector equality info and add hooks recipes
1 parent 7669175 commit 0ea274a

File tree

2 files changed

+213
-41
lines changed

2 files changed

+213
-41
lines changed

docs/api/batch.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ React's `unstable_batchedUpdate()` API allows any React updates in an event loop
1616
Since React-Redux needs to work in both ReactDOM and React Native environments, we've taken care of importing this API from the correct renderer at build time for our own use. We also now re-export this function publicly ourselves, renamed to `batch()`. You can use it to ensure that multiple actions dispatched outside of React only result in a single render update, like this:
1717

1818
```js
19-
import { batch } from "react-redux";
19+
import { batch } from 'react-redux'
2020

2121
function myThunk() {
22-
return (dispatch, getState) => {
23-
// should only result in one combined re-render, not two
24-
batch(() => {
25-
dispatch(increment());
26-
dispatch(increment());
27-
})
28-
}
22+
return (dispatch, getState) => {
23+
// should only result in one combined re-render, not two
24+
batch(() => {
25+
dispatch(increment())
26+
dispatch(increment())
27+
})
28+
}
2929
}
3030
```
3131

3232
## References
3333

34-
- [`unstable_batchedUpdate()` API from React](https://github.com/facebook/react/commit/b41883fc708cd24d77dcaa767cde814b50b457fe)
34+
- [`unstable_batchedUpdate()` API from React](https://github.com/facebook/react/commit/b41883fc708cd24d77dcaa767cde814b50b457fe)

docs/api/hooks.md

Lines changed: 204 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ React Redux now offers a set of hook APIs as an alternative to the existing `con
1515
1616
These hooks were first added in v7.1.0.
1717

18-
This page reflects the latest alpha, which is currently **v7.1.0-alpha.4**.
18+
This page reflects the latest alpha, which is currently **v7.1.0-alpha.5**.
1919

2020
## Using Hooks in a React Redux App
2121

@@ -37,25 +37,56 @@ From there, you may import any of the listed React Redux hooks APIs and use them
3737
## `useSelector()`
3838

3939
```js
40-
const result : any = useSelector(selector : Function, deps : any[])
40+
const result : any = useSelector(selector : Function, equalityFn? : Function)
4141
```
4242

4343
Allows you to extract data from the Redux store state, using a selector function.
4444

45+
> **Note**: The selector function should be [pure](https://en.wikipedia.org/wiki/Pure_function) since it is potentially executed multiple times and at arbitrary points in time.
46+
4547
The selector is approximately equivalent to the [`mapStateToProps` argument to `connect`](../using-react-redux/connect-extracting-data-with-mapStateToProps.md) conceptually. The selector will be called with the entire Redux store state as its only argument. The selector will be run whenever the function component renders. `useSelector()` will also subscribe to the Redux store, and run your selector whenever an action is dispatched.
4648

4749
However, there are some differences between the selectors passed to `useSelector()` and a `mapState` function:
4850

4951
- The selector may return any value as a result, not just an object. The return value of the selector will be used as the return value of the `useSelector()` hook.
50-
- The selector function used will be based on the `deps` array. If no deps array is provided, the latest passed-in selector function will be used when the component renders, and also when any actions are dispatched before the next render. If a deps array is provided, the last saved selector will be used, and that selector will be overwritten whenever the deps array contents have changed.
5152
- When an action is dispatched, `useSelector()` will do a shallow comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, they component will not re-render.
52-
- The selector function does _not_ receive an `ownProps` argument. If you wish to use props within the selector function to determine what values to extract, you should call the React [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) or [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hooks yourself to create a version of the selector that will be re-created whenever the props it depends on change.
53+
- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples below) or by using a curried selector.
54+
- Extra care must be taken when using memoizing selectors (see examples below for more details).
55+
- `useSelector()` uses strict `===` reference equality checks by default, not shallow equality (see the following section for more details).
5356

5457
> **Note**: There are potential edge cases with using props in selectors that may cause errors. See the [Usage Warnings](#usage-warnings) section of this page for further details.
5558
5659
You may call `useSelector()` multiple times within a single function component. Each call to `useSelector()` creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple `useSelector()`s in the same component to return new values _should_ only result in a single re-render.
5760

58-
#### Examples
61+
### Equality Comparisons and Updates
62+
63+
When the function component renders, the provided selector function will be called and its result will be returned
64+
from the `useSelector()` hook. (A cached result may be returned if the selector has been run and hasn't changed.)
65+
66+
However, when an action is dispatched to the Redux store, `useSelector()` only forces a re-render if the selector result
67+
appears to be different than the last result. As of v7.1.0-alpha.5, the default comparison is a strict `===` reference
68+
comparison. This is different than `connect()`, which uses shallow equality checks on the results of `mapState` calls
69+
to determine if re-rendering is needed. This has several implications on how you should use `useSelector()`.
70+
71+
With `mapState`, all individual fields were returned in a combined object. It didn't matter if the return object was
72+
a new reference or not - `connect()` just compared the individual fields. With `useSelector()`, returning a new object
73+
every time will _always_ force a re-render by default. If you want to retrieve multiple values from the store, you can:
74+
75+
- Call `useSelector()` multiple times, with each call returning a single field value
76+
- Use Reselect or a similar library to create a memoized selector that returns multiple values in one object, but
77+
only returns a new object when one of the values has changed.
78+
- Use the `shallowEqual` function from React-Redux as the `equalityFn` argument to `useSelector()`, like:
79+
80+
```js
81+
import { shallowEqual, useSelector } from 'react-redux'
82+
83+
// later
84+
const selectedData = useSelector(selectorReturningObject, shallowEqual)
85+
```
86+
87+
The optional comparison function also enables using something like Lodash's `_.isEqual()` or Immutable.js's comparison capabilities.
88+
89+
### `useSelector` Examples
5990

6091
Basic usage:
6192

@@ -69,19 +100,119 @@ export const CounterComponent = () => {
69100
}
70101
```
71102

72-
Using props to determine what to extract:
103+
Using props via closure to determine what to extract:
73104

74105
```jsx
75106
import React from 'react'
76107
import { useSelector } from 'react-redux'
77108

78109
export const TodoListItem = props => {
79-
const todo = useSelector(state => state.todos[props.id], [props.id])
80-
110+
const todo = useSelector(state => state.todos[props.id])
81111
return <div>{todo.text}</div>
82112
}
83113
```
84114

115+
#### Using memoizing selectors
116+
117+
When using `useSelector` with an inline selector as shown above, a new instance of the selector is created whenever the component is rendered. This works as long as the selector does not maintain any state. However, memoizing selectors (e.g. created via `createSelector` from `reselect`) do have internal state, and therefore care must be taken when using them. Below you can find typical usage scenarios for memoizing selectors.
118+
119+
When the selector does only depend on the state, simply ensure that it is declared outside of the component so that the same selector instance is used for each render:
120+
121+
```jsx
122+
import React from 'react'
123+
import { useSelector } from 'react-redux'
124+
import { createSelector } from 'reselect'
125+
126+
const selectNumOfDoneTodos = createSelector(
127+
state => state.todos,
128+
todos => todos.filter(todo => todo.isDone).length
129+
)
130+
131+
export const DoneTodosCounter = () => {
132+
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
133+
return <div>{NumOfDoneTodos}</div>
134+
}
135+
136+
export const App = () => {
137+
return (
138+
<>
139+
<span>Number of done todos:</span>
140+
<DoneTodosCounter />
141+
</>
142+
)
143+
}
144+
```
145+
146+
The same is true if the selector depends on the component's props, but will only ever be used in a single instance of a single component:
147+
148+
```jsx
149+
import React from 'react'
150+
import { useSelector } from 'react-redux'
151+
import { createSelector } from 'reselect'
152+
153+
const selectNumOfTodosWithIsDoneValue = createSelector(
154+
state => state.todos,
155+
(_, isDone) => isDone,
156+
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
157+
)
158+
159+
export const TodoCounterForIsDoneValue = ({ isDone }) => {
160+
const NumOfTodosWithIsDoneValue = useSelector(state =>
161+
selectNumOfTodosWithIsDoneValue(state, isDone)
162+
)
163+
164+
return <div>{NumOfTodosWithIsDoneValue}</div>
165+
}
166+
167+
export const App = () => {
168+
return (
169+
<>
170+
<span>Number of done todos:</span>
171+
<TodoCounterForIsDoneValue isDone={true} />
172+
</>
173+
)
174+
}
175+
```
176+
177+
However, when the selector is used in multiple component instances and depends on the component's props, you need to ensure that each component instance gets its own selector instance (see [here](https://github.com/reduxjs/reselect#accessing-react-props-in-selectors) for a more thourough explanation of why this is necessary):
178+
179+
```jsx
180+
import React, { useMemo } from 'react'
181+
import { useSelector } from 'react-redux'
182+
import { createSelector } from 'reselect'
183+
184+
const makeNumOfTodosWithIsDoneSelector = () =>
185+
createSelector(
186+
state => state.todos,
187+
(_, isDone) => isDone,
188+
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
189+
)
190+
191+
export const TodoCounterForIsDoneValue = ({ isDone }) => {
192+
const selectNumOfTodosWithIsDone = useMemo(
193+
makeNumOfTodosWithIsDoneSelector,
194+
[]
195+
)
196+
197+
const NumOfTodosWithIsDoneValue = useSelector(state =>
198+
selectNumOfTodosWithIsDoneValue(state, isDone)
199+
)
200+
201+
return <div>{NumOfTodosWithIsDoneValue}</div>
202+
}
203+
204+
export const App = () => {
205+
return (
206+
<>
207+
<span>Number of done todos:</span>
208+
<TodoCounterForIsDoneValue isDone={true} />
209+
<span>Number of unfinished todos:</span>
210+
<TodoCounterForIsDoneValue isDone={false} />
211+
</>
212+
)
213+
}
214+
```
215+
85216
## Removed: `useActions()`
86217

87218
This hook was removed in `v7.1.0-alpha.4`, based on [Dan Abramov's suggestion](https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930).
@@ -93,25 +224,6 @@ and manually call `dispatch(someActionCreator())` in callbacks and effects as ne
93224
[`bindActionCreators`](https://redux.js.org/api/bindactioncreators) function in your own code to bind action creators,
94225
or "manually" bind them like `const boundAddTodo = (text) => dispatch(addTodo(text))`.
95226

96-
If you still wish to use the `useActions()` hook, you may copy and paste this implementation into your own app:
97-
98-
```js
99-
import { bindActionCreators } from 'redux'
100-
import { useDispatch } from 'react-redux'
101-
import { useMemo } from 'react'
102-
103-
export function useActions(actions, deps) {
104-
const dispatch = useDispatch()
105-
return useMemo(() => {
106-
if (Array.isArray(actions)) {
107-
return actions.map(a => bindActionCreators(a, dispatch))
108-
}
109-
110-
return bindActionCreators(actions, dispatch)
111-
}, deps)
112-
}
113-
```
114-
115227
## Removed: `useRedux()`
116228

117229
This hook was removed in `v7.1.0-alpha.3`, on the grounds that it didn't provide any real benefit.
@@ -128,24 +240,48 @@ This hook returns a reference to the `dispatch` function from the Redux store. Y
128240

129241
#### Examples
130242

243+
```jsx
244+
import React from 'react'
245+
import { useDispatch } from 'react-redux'
246+
247+
export const CounterComponent = ({ value }) => {
248+
const dispatch = useDispatch()
249+
250+
return (
251+
<div>
252+
<span>{value}</span>
253+
<button onClick={() => dispatch({ type: 'increment-counter' })}>
254+
Increment counter
255+
</button>
256+
</div>
257+
)
258+
}
259+
```
260+
261+
When passing a callback using `dispatch` to a child component, it is recommended to memoize it with `useCallback`, since otherwise child components may render unnecessarily due to the changed reference.
262+
131263
```jsx
132264
import React, { useCallback } from 'react'
133265
import { useDispatch } from 'react-redux'
134266

135267
export const CounterComponent = ({ value }) => {
136268
const dispatch = useDispatch()
137-
const increaseCounter = useCallback(
138-
() => dispatch({ type: 'increase-counter' }),
269+
const incrementCounter = useCallback(
270+
() => dispatch({ type: 'increment-counter' }),
139271
[]
140272
)
141273

142274
return (
143275
<div>
144276
<span>{value}</span>
145-
<button onClick={increaseCounter}>Increase counter</button>
277+
<MyIncrementButton onIncrement={incrementCounter} />
146278
</div>
147279
)
148280
}
281+
282+
export const MyIncrementButton = React.memo(({ onIncrement }) => (
283+
<button onClick={onIncrement}>Increment counter</button>
284+
))
149285
```
150286

151287
## `useStore()`
@@ -198,11 +334,13 @@ Depending on what props were used and what the current store state is, this _may
198334
- The parent component _would_ stop rendering that child as a result
199335
- However, because the child subscribed first, its subscription runs before the parent stops rendering it. When it reads a value from the store based on props, that data no longer exists, and if the extraction logic is not careful, this may result in an error being thrown.
200336

201-
Some possible options for avoiding these problems with `useSelector()`:
337+
`useSelector()` tries to deal with this by catching all errors that are thrown when the selector is executed due to a store update (but not when it is executed during rendering). When an error occurs, the component will be forced to render, at which point the selector is executed again. This works as long as the selector is a pure function and you do not depend on the selector throwing errors.
338+
339+
If you prefer to deal with this issue yourself, here are some possible options for avoiding these problems altogether with `useSelector()`:
202340

203341
- Don't rely on props in your selector function for extracting data
204342
- In cases where you do rely on props in your selector function _and_ those props may change over time, _or_ the data you're extracting may be based on items that can be deleted, try writing the selector functions defensively. Don't just reach straight into `state.todos[props.id].name` - read `state.todos[props.id]` first, and verify that it exists before trying to read `todo.name`.
205-
- Because connected components add the necessary `Subscription` to the context provider, putting a connected component in the tree just above the components with potential data issues may keep those issues from occurring.
343+
- Because `connect` adds the necessary `Subscription` to the context provider, wrapping the component with `connect` (without any arguments, i.e. `connect()(MyComponent)` will keep those issues from occurring.
206344

207345
> **Note**: For a longer description of this issue, see [this chat log that describes the problems in more detail](https://gist.github.com/markerikson/faac6ae4aca7b82a058e13216a7888ec), as well as [issue #1179](https://github.com/reduxjs/react-redux/issues/1179).
208346
@@ -242,3 +380,37 @@ export const CounterComponent = props => {
242380
return renderedChildren
243381
}
244382
```
383+
384+
## Hooks Recipes
385+
386+
We've pared down our hooks API from the original alpha release, focusing on a more minimal set of API primitives.
387+
However, you may still wish to use some of the approaches we tried in your own apps. These examples should be ready
388+
to copy and paste into your own codebase.
389+
390+
### Recipe: `useActions()`
391+
392+
```js
393+
import { bindActionCreators } from 'redux'
394+
import { useDispatch } from 'react-redux'
395+
import { useMemo } from 'react'
396+
397+
export function useActions(actions, deps) {
398+
const dispatch = useDispatch()
399+
return useMemo(() => {
400+
if (Array.isArray(actions)) {
401+
return actions.map(a => bindActionCreators(a, dispatch))
402+
}
403+
return bindActionCreators(actions, dispatch)
404+
}, deps)
405+
}
406+
```
407+
408+
### Recipe: `useShallowEqualSelector()`
409+
410+
```js
411+
import { shallowEqual } from 'react-redux'
412+
413+
export function useShallowEqualSelector(selector) {
414+
return useSelector(selector, shallowEqual)
415+
}
416+
```

0 commit comments

Comments
 (0)