译 文 : How to use useReducer in React Hooks for performance optimization
Welcome to Github address star! React Hook has been around for a while, and for more details on how to use it and the pain points it addresses, check out Dan’s two articles, useEffect’s complete guide and writing resilient components.
This article mainly introduces 6 different ways to use useReducer in React Hooks
preface
The React Hooks API has been officially released in React V16.8. This blog is mainly about the various usage examples of useReducer. Before you read, make sure you’ve read the Official React Hooks guide.
UseReducer hooks belong to the official extension hooks:
UseState is another alternative. It accepts (state, action) => newState and returns a method of dispatch paired with the current state. (If you’re familiar with Redux, you’ll quickly understand how it works.)
Although useReducer is an extended hook and useState is a basic hook, useState actually executes a useReducer as well. This means that useReducer is more native and you can use useReducer anywhere you use useState. Reducer is so powerful that there are various use cases.
This article then introduces a few representative use cases. Each example represents a specific use case and has associated code.
Use Case 1: Minimal (simple) pattern
You can see the code for this simple example. The rest of the following is an extension of this counting example.
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action'); }};Copy the code
First, we define the initialState and reducer to be initialized. Note that state is just a number, not an object. Developers familiar with Redux may be confused, but it is appropriate in hooks. In addition, an action is just a plain string.
Here is a component that uses useReducer.
const Example01 = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
Copy the code
When the user clicks a button, it dispatches an action to update the count, and the page displays the updated count. You can define as many actions as you want in the Reducer, but this mode has its limitations.
Here is the complete code:
import React, { useReducer } from 'react';
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action'); }}; const Example01 = () => { const [count, dispatch] = useReducer(reducer, initialState);return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
export default Example01;
Copy the code
Use Case 2: Action is an object
This example will be familiar to users of Redux. We used a state object and an Action object.
const initialState = {
count1: 0,
count2: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case 'increment1':
return { ...state, count1: state.count1 + 1 };
case 'decrement1':
return { ...state, count1: state.count1 - 1 };
case 'set1':
return { ...state, count1: action.count };
case 'increment2':
return { ...state, count2: state.count2 + 1 };
case 'decrement2':
return { ...state, count2: state.count2 - 1 };
case 'set2':
return { ...state, count2: action.count };
default:
throw new Error('Unexpected action'); }};Copy the code
There are two numbers stored in state. We can use complex objects to represent state, as long as the reducer is well organized (see combineReducers in React-Redux). Also, since action is an object, in addition to the type value, you can add other properties like action.count to it. Reducer is a little messy in this example, but that doesn’t prevent us from using it like this:
const Example02 = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<div>
{state.count1}
<button onClick={() => dispatch({ type: 'increment1' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>
<button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>
</div>
<div>
{state.count2}
<button onClick={() => dispatch({ type: 'increment2' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>
<button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>
</div>
</>
);
};
Copy the code
Notice that there are two counters in state, each with an action type defined to update them. For an online example, click here
Use Case 3: Use multiple Usereducers
The occurrence of two counters in a single state above is a typical global state approach. But we only need to use the local (local) state, so there is another method, useReducer can be used twice.
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'set': return action.count;
default: throw new Error('Unexpected action'); }};Copy the code
State here is a number, not an object, which is consistent with use case 1. Notice the difference here, the action is an object.
How is the component used
const Example03 = () => {
const [count1, dispatch1] = useReducer(reducer, initialState);
const [count2, dispatch2] = useReducer(reducer, initialState);
return (
<>
<div>
{count1}
<button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
</div>
<div>
{count2}
<button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
</div>
</>
);
};
Copy the code
As you can see, each counter has its own dispatch method, but the Reducer method is shared. The functionality of this is consistent with use case 2.
Use case 4: TextInput
Let’s look at a real example where multiple usereducers can do their job. Let’s take the React native input component as an example, storing text data in the local state state. Update the text status value by calling the dispatch function.
const initialState = ' ';
const reducer = (state, action) => action;
Copy the code
Notice that the old state is dropped each time the reducer is called. The specific use is as follows:
const Example04 = () => {
const [firstName, changeFirstName] = useReducer(reducer, initialState);
const [lastName, changeLastName] = useReducer(reducer, initialState);
return (
<>
<div>
First Name:
<TextInput value={firstName} onChangeText={changeFirstName} />
</div>
<div>
Last Name:
<TextInput value={lastName} onChangeText={changeLastName} />
</div>
</>
);
};
Copy the code
It’s as simple as that. Of course you can also add some validation logic to it. Complete code:
import React, { useReducer } from 'react';
const initialState = ' ';
const reducer = (state, action) => action;
const Example04 = () => {
const [firstName, changeFirstName] = useReducer(reducer, initialState);
const [lastName, changeLastName] = useReducer(reducer, initialState);
return (
<>
<div>
First Name:
<TextInput value={firstName} onChangeText={changeFirstName} />
</div>
<div>
Last Name:
<TextInput value={lastName} onChangeText={changeLastName} />
</div>
</>
);
};
// ref: https://facebook.github.io/react-native/docs/textinput
const TextInput = ({ value, onChangeText }) => (
<input type="text" value={value} onChange={e => onChangeText(e.target.value)} />
);
export default Example04;
Copy the code
Use Case 5: Context
Sometimes I want to share state (understood as implementing global state) between components. In general, global state limits the reuse of components, so we first consider using local state, delivered via props (to dispatch), but when it’s not that convenient (understand too much nested passing), we can use Context. If you’re familiar with the Context API, check out the official documentation.
In this example, we use the same reducer as in use case 3. Now let’s see how to create a context.
const CountContext = React.createContext();
const CountProvider = ({ children }) => {
const contextValue = useReducer(reducer, initialState);
return (
<CountContext.Provider value={contextValue}>
{children}
</CountContext.Provider>
);
};
const useCount = () => {
const contextValue = useContext(CountContext);
return contextValue;
};
Copy the code
UseCount is a custom hook, which is used just like any other official hook. As follows:
const Counter = () => {
const [count, dispatch] = useCount();
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);
};
Copy the code
ContextValue is the result returned by useReducer. We also used hook to reconstruct useCount. Notice at this point, it’s not fixed which context is being used.
Finally, use context like this:
const Example05 = () => (
<>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
</>
);
Copy the code
As shown above, having two CountProvider components means having two counters, even if we only use one context. Counters share state in the same CountProvider component. You can run the use case to see how it works. Check it out here
Use Case 6: Subscription
In hooks, it is recommended that the Context be used first, but what if there is already a shared state outside of the React component? The professional approach is to subscribe to listen on the state state and update the component when the shared state state is updated. There are some limitations, but React officially offers a public feature, CREate-Subscription, which you can use to subscribe.
Unfortunately, the public method package has not yet been overwritten with React Hooks, so it’s up to us to do our best with Hooks. Let’s implement the same functionality as in use case 5 without using Context.
First, create a custom hook:
const useForceUpdate = () => useReducer(state => ! state,false) [1];Copy the code
This reducer simply inverts the previous state and ignores the action. [1] Only dispatch is returned without state. Next, the main function implements the shared state and returns a custom hook:
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach(callback => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
callback(); // in case it's already updated const cleanup = () => { const index = subscribers.indexOf(callback); subscribers.splice(index, 1); }; return cleanup; } []); return [state, dispatch]; }; return useSharedState; };Copy the code
We use useEffect. It is a very important hook, you need to read the official document carefully to learn how to use it. In useEffect, we subscribe to a callback function to force an update to the component. The subscription needs to be cleared when the component is destroyed.
Next, we can create two shared state states. The reducer and initial value initialState are the same as in use cases 5 and 3:
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
Copy the code
This is different from use case 5, where the two hooks bind to a specific shared state. Then we use these two hooks.
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />
};
Copy the code
Note that the Counter component is a common stateless component. Use it like this:
const Example06 = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
Copy the code
As you can see, we didn’t use Context, but we also implemented shared state. You should all have a specific look at useReducer, which is very helpful for performance optimization.
All of the code in this article is here. For an online example, click here.
Some of my own conclusions:
- Hooks have Props and states for each Render. You can assume that each Render snapshot is created and retained (functions are destroyed, but variables are retained by react), so when the State changes and Rerender, N Render states are formed, and each Render State has its own fixed Props and states. This is also a property of functions — data invariance
- The parameter of the useState function is the initial value, but since the entire function is Render, it is called every time. If the initial value is very time-consuming to evaluate, it is recommended to use the function passed in, which will only be executed once:
- If you’re familiar with the React class component lifecycle, you can assume that
useEffect Hook
That’s the combinationComponentDidMount, componentDidUpdate, and componentWillUnmount (in the useEffect callback)
UseEffect does not prevent the browser from updating the screen- Hooks work with related logic together, instead of breaking it down by lifecycle
- UseEffect is triggered after browser render. If you want to synchronize DOM changes, use useLayoutEffect to synchronize DOM changes before browser redraw. But try to use the standard useEffect for the layout and everything else so you don’t block the view from updating.
``` function FunctionComponent(props) { const [rows, setRows] = useState(() => createRows(props.count)); } useRef does not support this feature, so you need to write some redundant functions to determine whether initialization has been performed. (https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily) ```Copy the code
If there are any mistakes or irregularities, please be sure to correct them. Thank you very much!
reference
- The complete guide to useEffect
- The react website