Skip to content

Commit 7414831

Browse files
committed
Merge remote-tracking branch 'mrwolfz/issue-1262' into v7-hooks-alpha
2 parents 756ae49 + 622545d commit 7414831

File tree

6 files changed

+540
-137
lines changed

6 files changed

+540
-137
lines changed

docs/api/hooks.md

+337
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
---
2+
id: hooks
3+
title: Hooks
4+
sidebar_label: Hooks
5+
hide_title: true
6+
---
7+
8+
# Hooks
9+
10+
React's new ["hooks" APIs](https://reactjs.org/docs/hooks-intro.html) give function components the ability to use local component state, execute side effects, and more.
11+
12+
React Redux now offers a set of hook APIs as an alternative to the existing `connect()` Higher Order Component. These APIs allow you to subscribe to the Redux store and dispatch actions, without having to wrap your components in `connect()`.
13+
14+
> **Note**: The hook APIs listed in this page are **still experimental and in alpha!** We encourage you to try them out in your applications and give feedback, but be aware that they may be changed before a final release, including potential renaming or removal.
15+
16+
## Using Hooks in a React Redux App
17+
18+
As with `connect()`, you should start by wrapping your entire application in a `<Provider>` component to make the store available throughout the component tree:
19+
20+
```jsx
21+
const store = createStore(rootReducer)
22+
23+
ReactDOM.render(
24+
<Provider store={store}>
25+
<App />
26+
</Provider>,
27+
document.getElementById('root')
28+
)
29+
```
30+
31+
From there, you may import any of the listed React Redux hooks APIs and use them within your function components.
32+
33+
## `useSelector()`
34+
35+
```js
36+
const result : any = useSelector(selector : Function, deps : any[])
37+
```
38+
39+
Allows you to extract data from the Redux store state, using a selector function.
40+
41+
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.
42+
43+
However, there are some differences between the selectors passed to `useSelector()` and a `mapState` function:
44+
45+
- 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.
46+
- 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.
47+
- 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.
48+
- 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.
49+
50+
> **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.
51+
52+
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.
53+
54+
#### Examples
55+
56+
Basic usage:
57+
58+
```jsx
59+
import React from 'react'
60+
import { useSelector } from 'react-redux'
61+
62+
export const CounterComponent = () => {
63+
const counter = useSelector(state => state.counter)
64+
return <div>{counter}</div>
65+
}
66+
```
67+
68+
Using props to determine what to extract:
69+
70+
```jsx
71+
import React from 'react'
72+
import { useSelector } from 'react-redux'
73+
74+
export const TodoListItem = props => (
75+
const todo = useSelector(state => state.todos[props.id], [props.id])
76+
77+
return <div>{todo.text}</div>
78+
}
79+
```
80+
81+
## `useActions()`
82+
83+
```js
84+
const boundAC = useActions(actionCreator : Function, deps : any[])
85+
86+
const boundACsObject = useActions(actionCreators : Object<string, Function>, deps : any[])
87+
88+
const boundACsArray = useActions(actionCreators : Function[], deps : any[])
89+
```
90+
91+
Allows you to prepare bound action creators that will dispatch actions to the Redux store when called.
92+
93+
This is conceptually similar to the [`mapDispatchToProps` argument to `connect`](../using-react-redux/connect-dispatching-actions-with-mapDispatchToProps.md). The action creators that are passed in will be bound using the Redux [`bindActionCreators()` utility](https://redux.js.org/api/bindactioncreators), and the bound functions will be returned.
94+
95+
However, there are some differences between the arguments passed to `useActions()` and the `mapDispatch` argument to `connect()`:
96+
97+
- `mapDispatch` may be either a function or an object. `useActions()` accepts a single action creator, an object full of action creators, or an array of action creators, and the return value will be the same form.
98+
- `mapDispatch` is normally used once when the component is instantiated, unless it is a function with the `(dispatch, ownProps)` signature, which causes it to be called any time the props have changed. The action creators passed to `useActions()` will be re-bound (and thus have new function references) whenever the values passed in the `deps` array change. If no `deps` array is provided, the functions will be re-bound every time the component re-renders.
99+
100+
> **Note**: There are potential edge cases with using the object argument form and declaring the object inline. See the [Usage Warnings](#usage-warnings) section of this page for further details.
101+
102+
You may call `useActions()` multiple times in a single component.
103+
104+
#### Examples
105+
106+
```jsx
107+
import React from 'react'
108+
import { useActions } from 'react-redux'
109+
110+
const increaseCounter = amount => ({
111+
type: 'increase-counter',
112+
amount
113+
})
114+
115+
export const CounterComponent = ({ value }) => {
116+
// supports passing an object of action creators
117+
const { increaseCounterByOne, increaseCounterByTwo } = useActions(
118+
{
119+
increaseCounterByOne: () => increaseCounter(1),
120+
increaseCounterByTwo: () => increaseCounter(2)
121+
},
122+
[]
123+
)
124+
125+
// supports passing an array/tuple of action creators
126+
const [increaseCounterByThree, increaseCounterByFour] = useActions(
127+
[() => increaseCounter(3), () => increaseCounter(4)],
128+
[]
129+
)
130+
131+
// supports passing a single action creator
132+
const increaseCounterBy5 = useActions(() => increaseCounter(5), [])
133+
134+
// passes through any arguments to the callback
135+
const increaseCounterByX = useActions(x => increaseCounter(x), [])
136+
137+
return (
138+
<div>
139+
<span>{value}</span>
140+
<button onClick={increaseCounterByOne}>Increase counter by 1</button>
141+
</div>
142+
)
143+
}
144+
```
145+
146+
## `useRedux()`
147+
148+
```js
149+
const [selectedValue, boundACs] = useRedux(selector, actionCreators)
150+
```
151+
152+
This hook allows you to both extract values from the Redux store state and bind action creators in a single call. This is conceptually equivalent to the [`connect()` function](./connect.md) accepting both a `mapState` and a `mapDispatch` argument.
153+
154+
`useRedux()` is simply a wrapper for `useSelector()` and `useActions()`, and `useRedux()` passes its arguments directly to them. The return value is an array containing the results of `useSelector()` and `useActions()`, respectively.
155+
156+
Note that `useRedux()` currently does _not_ allow you to specify a dependency array for the `actionCreators` parameter, so they will be re-created every time the component renders. If you need consistent function references, consider using `useActions()` with a dependency array instead.
157+
158+
#### Examples
159+
160+
```jsx
161+
import React from 'react'
162+
import { useRedux } from 'react-redux'
163+
164+
export const CounterComponent = () => {
165+
const [counter, { inc1, inc }] = useRedux(state => state.counter, {
166+
inc1: () => ({ type: 'inc1' }),
167+
inc: amount => ({ type: 'inc', amount })
168+
})
169+
170+
return (
171+
<>
172+
<div>{counter}</div>
173+
<button onClick={inc1}>Increment by 1</button>
174+
<button onClick={() => inc(5)}>Increment by 5</button>
175+
</>
176+
)
177+
}
178+
```
179+
180+
## `useDispatch()`
181+
182+
```js
183+
const dispatch = useDispatch()
184+
```
185+
186+
This hook returns a reference to the `dispatch` function from the Redux store. You may use it to dispatch actions as needed.
187+
188+
#### Examples
189+
190+
```jsx
191+
import React, { useCallback } from 'react'
192+
import { useDispatch } from 'react-redux'
193+
194+
export const CounterComponent = ({ value }) => {
195+
const dispatch = useDispatch()
196+
const increaseCounter = useCallback(
197+
() => dispatch({ type: 'increase-counter' }),
198+
[]
199+
)
200+
201+
return (
202+
<div>
203+
<span>{value}</span>
204+
<button onClick={increaseCounter}>Increase counter</button>
205+
</div>
206+
)
207+
}
208+
```
209+
210+
## `useStore()`
211+
212+
```js
213+
const store = useStore()
214+
```
215+
216+
This hook returns a reference to the same Redux store that was passed in to the `<Provider>` component.
217+
218+
This hook should probably not be used frequently. Prefer `useSelector()` and `useActions()` as your primary choices. However, this may be useful for less common scenarios that do require access to the store, such as replacing reducers.
219+
220+
#### Examples
221+
222+
```jsx
223+
import React from 'react'
224+
import { useStore } from 'react-redux'
225+
226+
export const CounterComponent = ({ value }) => {
227+
const store = useStore()
228+
229+
// EXAMPLE ONLY! Do not do this in a real app.
230+
// The component will not automatically update if the store state changes
231+
return <div>{store.getState()}</div>
232+
}
233+
```
234+
235+
## Usage Warnings
236+
237+
### Stale Props and "Zombie Children"
238+
239+
One of the most difficult aspects of React Redux's implementation is ensuring that if your `mapStateToProps` function is defined as `(state, ownProps)`, it will be called with the "latest" props every time. Up through version 4, there were recurring bugs reported involving edge case situations, such as errors thrown from a `mapState` function for a list item whose data had just been deleted.
240+
241+
Starting with version 5, React Redux has attempted to guarantee that consistency with `ownProps`. In version 7, that is implemented using a custom `Subscription` class internally in `connect()`, which forms a nested hierarchy. This ensures that connected components lower in the tree will only receive store update notifications once the nearest connected ancestor has been updated. However, this relies on each `connect()` instance overriding part of the internal React context, supplying its own unique `Subscription` instance to form that nesting, and rendering the `<ReactReduxContext.Provider>` with that new context value.
242+
243+
With hooks, there is no way to render a context provider, which means there's also no nested hierarchy of subscriptions. Because of this, the "stale props" and "zombie child" issues may potentially re-occur in an app that relies on using hooks instead of `connect()`.
244+
245+
Specifically, "stale props" means any case where:
246+
247+
- a selector function relies on this component's props to extract data
248+
- a parent component _would_ re-render and pass down new props as a result of an action
249+
- but this component's selector function executes before this component has had a chance to re-render with those new props
250+
251+
Depending on what props were used and what the current store state is, this _may_ result in incorrect data being returned from the selector, or even an error being thrown.
252+
253+
"Zombie child" refers specifically to the case where:
254+
255+
- Multiple nested connected components are mounted in a first pass, causing a child component to subscribe to the store before its parent
256+
- An action is dispatched that deletes data from the store, such as a todo item
257+
- The parent component _would_ stop rendering that child as a result
258+
- 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.
259+
260+
Some possible options for avoiding these problems with `useSelector()`:
261+
262+
- Don't rely on props in your selector function for extracting data
263+
- 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`.
264+
- 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.
265+
266+
> **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).
267+
268+
### Action Object Hoisting
269+
270+
Many developers are used to [using the "object shorthand" form of `mapDispatch`](../using-react-redux/connect-dispatching-actions-with-mapDispatchToProps.md#defining-mapdispatchtoprops-as-an-object) by passing multiple action creators as an inline object argument to `connect()`:
271+
272+
```js
273+
export default connect(
274+
mapState,
275+
{ addTodo, toggleTodo }
276+
)(TodoList)
277+
```
278+
279+
However, this pattern can be problematic when calling `useActions()`. Specifically, the combination of importing action creators by name individually, defining the actions object as an inline argument, _and_ attempting to destructure the results, can lead to hoisting problems that cause errors.
280+
281+
This example shows the problematic pattern:
282+
283+
```js
284+
import { addTodo, toggleTodo } from './todos'
285+
286+
const { addTodo, toggleTodo } = useActions({
287+
addTodo,
288+
toggleTodo
289+
})
290+
```
291+
292+
Due to hoisting, the `addTodo` and `toggleTodo` imports are not used, but instead the declared variables from the const are used in the actions object.
293+
294+
Some options for avoiding this problem:
295+
296+
- Don't destructure the result of `useActions()`. Instead, keep it as a single object (`const actions = useActions()`) and reference them like `actions.addTodo`
297+
- Define the action creators object outside the function component, either by hand (`const actionCreators = {addTodo, toggleTodo}`), or by using the "named imports as an object" syntax (`import * as todoActions from "./todoActions"`).
298+
- Try using the single function or array forms of `useActions()`
299+
300+
> **Note**: for more details on this problem, see [this comment and following in issue #1179](https://github.com/reduxjs/react-redux/issues/1179#issuecomment-482473235), as well as [this codesandbox that demonstrates the issue](https://codesandbox.io/s/7yjn3m9n96).
301+
302+
### Performance
303+
304+
As mentioned earlier, `useSelector()` will do basic shallow comparisons of return values when running the selector function after an action is dispatched. However, unlike `connect()`, `useSelector()` does not do anything to prevent your own function component from completing a re-render if the derived state has changed.
305+
306+
If further performance optimizations are necessary, you may consider either wrapping your function component in `React.memo(MyFunctionComponent)`, or using `useMemo()` to memoize the render output of your component:
307+
308+
```jsx
309+
// Option 1: use React.memo() to keep the component from re-rendering
310+
311+
const CounterComponent = props => {
312+
const counter = useSelector(state => state.counter)
313+
return (
314+
<div>
315+
{props.name}: {counter}
316+
</div>
317+
)
318+
}
319+
320+
export const MemoizedCounterComponent = React.memo(CounterComponent)
321+
322+
// Option 2: let the component re-render, but memoize output
323+
324+
export const CounterComponent = props => {
325+
const counter = useSelector(state => state.counter)
326+
327+
const renderedChildren = useMemo(() => {
328+
return (
329+
<div>
330+
{props.name}: {counter}
331+
</div>
332+
)
333+
}, [props.name, counter])
334+
335+
return renderedChildren
336+
}
337+
```

src/components/connectAdvanced.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export default function connectAdvanced(
253253
const lastChildProps = useRef()
254254
const lastWrapperProps = useRef(wrapperProps)
255255
const childPropsFromStoreUpdate = useRef()
256+
const renderIsScheduled = useRef(false)
256257

257258
const actualChildProps = usePureOnlyMemo(() => {
258259
// Tricky logic here:
@@ -282,6 +283,7 @@ export default function connectAdvanced(
282283
// We want to capture the wrapper props and child props we used for later comparisons
283284
lastWrapperProps.current = wrapperProps
284285
lastChildProps.current = actualChildProps
286+
renderIsScheduled.current = false
285287

286288
// If the render was from a store update, clear out that reference and cascade the subscriber update
287289
if (childPropsFromStoreUpdate.current) {
@@ -328,14 +330,17 @@ export default function connectAdvanced(
328330

329331
// If the child props haven't changed, nothing to do here - cascade the subscription update
330332
if (newChildProps === lastChildProps.current) {
331-
notifyNestedSubs()
333+
if (!renderIsScheduled.current) {
334+
notifyNestedSubs()
335+
}
332336
} else {
333337
// Save references to the new child props. Note that we track the "child props from store update"
334338
// as a ref instead of a useState/useReducer because we need a way to determine if that value has
335339
// been processed. If this went into useState/useReducer, we couldn't clear out the value without
336340
// forcing another re-render, which we don't want.
337341
lastChildProps.current = newChildProps
338342
childPropsFromStoreUpdate.current = newChildProps
343+
renderIsScheduled.current = true
339344

340345
// If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
341346
forceComponentUpdateDispatch({

0 commit comments

Comments
 (0)