Use Context and hook for state management
After learning React Hook, you will know that custom Hook can encapsulate state management logic and achieve the effect of reuse and sharing.
Now we have a timer đ°
import React, { useState } from 'react'; import { render } from 'react-dom'; function CounterDisplay() { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return ( <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div> ); } render(<CounterDisplay />, document.getElementById('root'));
If we want to reuse the code of counter state management, we can use custom hook:
import React, { useState } from 'react'; import { render } from 'react-dom'; function useCounter() { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } function CounterDisplay() { const { count, decrement, increment } = useCounter(); return ( <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div> ); } function AnotherCounterDisplay() { const { count, decrement, increment } = useCounter(); return ( <div> <button onClick={decrement}>-</button> <p>Current score:{count}</p> <button onClick={increment}>+</button> </div> ); } render( <> <CounterDisplay /> <AnotherCounterDisplay /> </>, document.getElementById('root'), );
Through efforts, counter display and another counter display share the counting state management logic. But what if the two components are required to share state? At this time, we may think of state improvement, as follows:
import React, { useState } from 'react'; import { render } from 'react-dom'; interface Counter { count: number; decrement: () => void; increment: () => void; } function useCounter(): Counter { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } function CounterDisplay(props: { counter: Counter }) { const { count, decrement, increment } = props.counter; return ( <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div> ); } function AnotherCounterDisplay(props: { counter: Counter }) { const { count, decrement, increment } = props.counter; return ( <div> <button onClick={decrement}>-</button> <p>Current score:{count}</p> <button onClick={increment}>+</button> </div> ); } function App() { const counter = useCounter(); return ( <> <CounterDisplay counter={counter} /> <AnotherCounterDisplay counter={counter} /> </> ); } render(<App />, document.getElementById('root'));
If the level of components that need to share state is deeper than that of common parent components, we can use React Context to simplify state promotion and transfer component properties level by level:
import React, { useState, useContext } from 'react'; import { render } from 'react-dom'; interface Counter { count: number; decrement: () => void; increment: () => void; } function useCounter(): Counter { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } const CounterContext = React.createContext<Counter>(defaultValue); function CounterDisplay() { const { count, decrement, increment } = useContext(CounterContext); return ( <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div> ); } function AnotherCounterDisplay() { const { count, decrement, increment } = useContext(CounterContext); return ( <div> <button onClick={decrement}>-</button> <p>Current score:{count}</p> <button onClick={increment}>+</button> </div> ); } function CounterInfo() { const counter = useContext(CounterContext); return <div>Current count:{counter.count}</div>; } function Header() { return ( <div> <h1>Counter</h1> <CounterInfo /> </div> ); } function App() { const counter = useCounter(); return ( <CounterContext.Provider value={counter}> <div> <Header /> <CounterDisplay /> <AnotherCounterDisplay /> </div> </CounterContext.Provider> ); } render(<App />, document.getElementById('root'));
â ď¸ be careful:
React.createContext<Counter>(defaultValue)
< Counter > refers to the parameter passed in. defaultValue is of Counter type
Now, the status of AnotherCounterDisplay is required to be managed separately. You can still use Context:
function App() { const counter = useCounter(); const anotherCounter = useCounter(); return ( <CounterContext.Provider value={counter}> <div> <Header /> <CounterDisplay /> <CounterContext.Provider value={anotherCounter}> <AnotherCounterDisplay /> </CounterContext.Provider> </div> </CounterContext.Provider> ); }
We can also create another component to provide the context of counting state management:
function CounterContextProvider({ children }: { children: React.ReactNode }) { const counter = useCounter(); return ( <CounterContext.Provider value={counter}> {children} </CounterContext.Provider> ); }
â ď¸ be careful:
{ children }: { children: React.ReactNode }
{children}:
A variable is an object, which contains children
{ children: React.ReactNode }
The children in the variable is a react Reactnode type
A ReactNode can be:
-
- ReactElement
- string (aka ReactText)
- number (aka ReactText)
- Array of ReactNodes (aka ReactFragment)
They are used as properties of other ReactElements to represent children In fact, they created a tree of {ReactElements
With a little practice, we will find that the pattern of creating a context for a custom hook is very useful. Let's sort out the counter examples of this mode:
interface Counter { count: number; decrement: () => void; increment: () => void; } // First define a React Hook function useCounter() { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } // Then define a context: const CounterContext = React.createContext<Counter>(null); // Then we create a context Provider Components: function CounterContextProvider({ children }: { children: React.ReactNode }) { const counter = useCounter(); return ( <CounterContext.Provider value={counter}> {children} </CounterContext.Provider> ); } // After that, we can enjoy it: function App() { <CounterContextProvider> <CounterDisplay /> </CounterContextProvider>; } function CounterDisplay() { const counter = useContext(CounterContext); return <div>{counter.count}</div>; }
The core of this mode is the need to customize the hook. Then there will be step 2 and step 3, so we can continue to refine (step 2 and step 3):
//func: () => T function createContainer<T>(func:Function):T { const ContainerContext = React.createContext<T | null>(null); const Provider = ({ children }: { children: React.ReactNode }) => { const result = func(); return ( <ContainerContext.Provider value={result}> {children} </ContainerContext.Provider> ); }; const useContainer = () => { return useContext(ContainerContext); }; return { Provider, useContainer }; }
We use createContainer to simplify the custom hook context mode:
// First define a React Hook function useCounter() { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } // Then define the count container const CounterContainer = createContainer(useCounter); // After that, we can enjoy it: function App() { <CounterContainer.Provider> <CounterDisplay /> </CounterContainer.Provider>; } function CounterDisplay() { const counter = CounterContainer.useContainer(); return <div>{counter.count}</div>; }
Here, we introduce a container noun to indicate that the custom hook is packaged into the context. We can call it "hook container" or "container" for short.
The createContainer just was created by unstated-next realization:
Installation unstated-nextăă
yarn add unstated-next
import { createContainer } from 'unstated-next'; // First define a React Hook function useCounter() { const [count, setCount] = useState(0); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; } // Then define the count container const CounterContainer = createContainer(useCounter); // After that, we can enjoy it: function App() { <CounterContainer.Provider> <CounterDisplay /> </CounterContainer.Provider>; } function CounterDisplay() { const counter = CounterContainer.useContainer(); return <div>{counter.count}</div>; }
State management of page components and "hook container" mode
Separate the state management logic from the UI logic - React Hooks.
For a page component, we use components to handle UI rendering and React Hooks to handle state. In general, each part of the page needs to share the state. Through this analysis, you will find that the "hook container" mode is very suitable for the development of page components: put the page level state management in the page custom hook, and each sub component of the page can quickly obtain the required shared state through the context.
function useXxxxPage() { .... } const XxxxPageContainer = createContainer(useXxxxPage); function XxxxPageHeader() { const xxxxPageState = XxxxPageContainer.useContainer(); //.... } function XxxxPageContent() { const xxxxPageState = XxxxPageContainer.useContainer(); //... } function XxxxPageFooter() { const xxxxPageState = XxxxPageContainer.useContainer(); //... } function XxxxPage() { return <XxxxPageContainer.Provider> <div> <XxxxPageHeader /> <XxxxPageContent /> <XxxxPageFooter /> </div> </XxxxPageContainer.Provider> }
Emphasize that only the data that needs to be shared at the page level needs to be put into usexpage. The local state is still solved at the local component level.
Page components are also components. There is no special setting in React, but page components often face cross level sharing of States, and we generally start from page components when developing applications. Therefore, we can choose a state management mode as the reference implementation of state management, "hook container" is a good mode. However, the state management of page components also needs to follow the best practices of component state management. When it is found that the "hook container" is not suitable, other best practices should be considered.
During application development, the following points shall be followed:
- UI rendering with components
- Reuse UI rendering logic with components
- Using React Hooks for component state management
- Reuse state management logic with custom hook
- Separating the logic of state management and UI rendering with custom hook
- When cross level sharing status is encountered, use React Context
- If you use React Context + custom hooks for cross level sharing, you can consider using unstated next
Unstated next usage points
- Important #1: keep Containers small
- Important #2: combine Containers
- Important #3: optimize components
Reprinted from: https://sinoui.github.io/sinoui-guide/docs/context-and-hook