“State management” is a hot topic in React. Because React data flows down from the top (the ancestor component), when a deeper component needs to access the state of the ancestor component, it usually needs to pass that state through multiple components. Components must pass state that the component is not actually using, resulting in serious coupling between components that is contrary to component design principles. At this point, we need a “state manager” to provide a global state that can be accessed without multiple layers of component delivery.
By far the most popular solution is Redux. Redux is a strictly one-way data flow, single-source state manager. Because Redux imposes strict restrictions on “when” and “how” state changes, state changes are predictable and documented.
Usually we don’t use the Redux API directly, instead we use a library like React-Redux.
To obtain state and DIpatch, use the connect function provided by React-Redux to inject it into the corresponding components in the form of higher-order components. This is cumbersome and can add unnecessary nesting of components to the component tree. The use of connect separates the view and logic layers of a component, which is no longer recommended.
Although react-Redux is cumbersome to write, Redux still has many users in the community, mainly because of the design philosophy of Redux is respected by most people.
After React V16.8, Hooks are introduced. It allows us to use the core ideas of Redux through useReducer without having to introduce Redux or restrict the writing style we use with other Redux. With Context, we can create custom state managers.
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
The useReducer is a set of Hooks that come with React. It receives initial values of reducer and state. The call returns an array with state as the first item and the dispatch method as the second.
UseReducer is a better choice than useState when you have complex state to manage in a component, or when you need to update state across a deep component hierarchy.
Here is an example of a useReducer:
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter({ initialState }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={()= > dispatch({ type: 'increment' })}>+</button>
<button onClick={()= > dispatch({ type: 'decrement' })}>-</button>
</>
);
}
Copy the code
The use of useReducer is very simple, but it covers the core part of Redux. The Reducer returns a new state according to the action object distributed through dispatch.
However, if you use useReducer alone, you still pass the Dispatch method across multiple component levels. Fortunately, we have Context.
Context
Context provides a way to pass data to the entire component tree without passing it down one layer at a time.
Call the React. CreateContext method to generate a Context containing the two properties Provider and Consumer, both of which are React components.
The Provider receives a value property, which is received by the Consumer in the Provider’s descendant component.
<MyContext.Provider value={/* Put some values */} >Copy the code
The Consumer uses Render Prop to provide value properties to its children, which are already defined in the Provider.
< mycontext.consumer > {value => /* Render something based on the context value */} </ mycontext.consumer >Copy the code
store
Knowing useReducer and Context, we can combine the two to implement a simple state manager.
First we declare a Context object to host our state manager.
StoreContext.js
import { createContext } from 'react';
const StoreContext = createContext();
export default StoreContext;
Copy the code
To make the component tree accessible to the shared state, we also need to implement a Provider component.
Provider.js
import React, { useReducer } from 'react';
import PropTypes from 'prop-types';
import StoreContext from './StoreContext';
export default function Provider({ children, reducer, initialState }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={[state, dispatch]} >
{children}
</StoreContext.Provider>
);
}
Provider.propTypes = {
children: PropTypes.element.isRequired,
reducer: PropTypes.func.isRequired,
initialState: PropTypes.any.isRequired,
};
Copy the code
The Provider receives two props, Reducer and initialState. Using these two properties as arguments, call useReducer to get state and dispatch. We then pass state and dispatch to storeContext. Provider intact so that we can retrieve state and dispatch in the Provider’s descendant components.
In React-Redux, the connect function is used to inject state and dispatch into the corresponding components in the form of higher-order components. But now that we have Hooks, we can use a simpler way to get state and dispatch.
useStore.js
import { useContext } from 'react';
import StoreContext from './StoreContext';
export default function useStore() {
const [state, dispatch] = useContext(StoreContext);
return { state, dispatch };
}
Copy the code
We import the StoreContext we declared earlier and call the useContext method to get the state and dispatch saved in usecontext.provider. This is done by returning state and Dispatch as an object, mainly for IDE intelligence hints.
Note that the function name begins with “use” so React can recognize that this is a custom Hook and handle it accordingly.
We save the above three files in the Store folder and create an index.js file to export the Provider and useStore.
All code can be viewed online here.
For specific use, we first declare reducer and initial state, just as we did with Redux. The Provider is then imported from the Store folder and the Reducer and initial state are passed to the Provider as props.
import React from 'react';
import { Provider } from './store';
import Count from './Count';
function reducer(state, action) {
switch (action.type) {
case 'increase': {
return state + 1;
}
case 'decrease': {
return state - 1;
}
default: {
returnstate; }}}const initialState = 0;
function App() {
return (
<Provider reducer={reducer} initialState={initialState}>
<Count />
</Provider>
);
}
export default App;
Copy the code
Next, we get the state and dispatch from the component with a single line of code:
const { state, dispatch } = useStore();
Copy the code
Unlike Redux, which has a single state tree, we can insert providers multiple times into the component tree. When useStore is called, it looks up and automatically retrieves the state saved in the Provider closest to the current component.
If you just want to be able to use a simple state manager and don’t want to manage a complex single state tree like Redux, useReducer + Context is the best choice. That makes it impossible to use Redux middleware and its associated ecosystem. While Hooks allow us to write and reuse code flexibly, implementation decisions need to be made on a project-by-project basis.