Context
is a cross-component communication solution provided by React
. useContext
and useReducer
are part of the Hooks API
introduced after React 16.8
, and they can be used to manage global state as a lightweight alternative to Redux
.
React Context
is suitable for communicating between parent and child components, as well as across multiple components. It provides a way to pass data between components without manually adding props
at each level of the component tree. In a typical React
application, data is passed from parent to child components via props
. However, when the need for passing data becomes more complex, such as with theme and locale configurations, it becomes cumbersome. Context
provides a way to share such values between components without explicitly passing props
through each level of the component tree. For example, React-Router
uses this method for data sharing, which explains why <Router>
must be outside all <Route>
components.
Of course, we should also discuss whether the use of Context
is necessary. Using Context
may introduce some performance issues because when the Context
data updates, it causes all components consuming the Context
and all the components in the child component tree to re-render. So, if we need to handle a structure with deep nesting, a possible approach is to render the component directly in the current component using the prepared props
and then pass the component down directly.
export const Page: React.FC<{
user: { name: string; avatar: string };
}> = props => {
const user = props.user;
const Header = (
<>
<span>
<img src={user.avatar}></img>
<span>{user.name}</span>
</span>
<span>...</span>
</>
);
const Body = <></>;
const Footer = <></>;
return (
<>
<Component header={Header} body={Body} footer={Footer}></Component>
</>
);
};
This form of control inversion for components reduces the number of props
that need to be passed in the application, which can make the code cleaner in many scenarios and allows the root component to have more control. However, this approach is not suitable for every scenario. Moving the logic to higher levels of the component tree can make those higher-level components more complex and force the lower-level components to adapt to this form, which may not be what you want. In such cases, considering the use of Context
becomes necessary.
Speaking of Context
, it provides a model similar to a service provider and consumer. After creating a Context
using React.createContext
, data can be provided using Context.Provider
and consumed using Context.Consumer
. After React 16.8
, React
introduced useContext
to consume Context
. useContext
takes a Context
object and returns the current value of that Context
.
// https://codesandbox.io/s/react-context-reucer-q1ujix?file=/src/store/context.tsx
import React, { createContext } from "react";
export interface ContextProps {
state: {
count: number;
};
}
const defaultContext: ContextProps = {
state: {
count: 1
}
};
export const AppContext = createContext<ContextProps>(defaultContext);
export const AppProvider: React.FC = (props) => {
const { children } = props;
return (
<AppContext.Provider value={defaultContext}>{children}</AppContext.Provider>
);
};
// https://codesandbox.io/s/react-context-reucer-q1ujix?file=/src/App.tsx
import React, { useContext } from "react";
import { AppContext, AppProvider } from "./store/context";
interface Props {}
const Children: React.FC = () => {
const context = useContext(AppContext);
return <div>{context.state.count}</div>;
};
const App: React.FC<Props> = () => {
return (
<AppProvider>
<Children />
</AppProvider>
);
};
export default App;
useReducer
is an alternative to useState
, similar to the way Redux
is used. It accepts a reducer
of the form (state, action) => newState
and returns the current state
as well as its associated dispatch
method.
const initialState = { count: 0 };
type State = typeof initialState;
const ACTION = {
INCREMENT: "INCREMENT" as const,
SET: "SET" as const,
};
type IncrementAction = {
type: typeof ACTION.INCREMENT;
};
type SetAction = {
type: typeof ACTION.SET;
payload: number;
};
type Action = IncrementAction | SetAction;
function reducer(state: State, action: Action) {
switch (action.type) {
case ACTION.INCREMENT:
return { count: state.count + 1 };
case ACTION.SET:
return { count: action.payload };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<div>
<button onClick={() => dispatch({ type: ACTION.INCREMENT })}>INCREMENT</button>
<button onClick={() => dispatch({ type: ACTION.SET, payload: 10 })}>SET 10</button>
</div>
</>
);
}
Or we can also use useReducer
relatively simply, for example, implementing a useForceUpdate
, of course, using useState
is also possible.
function useForceUpdate() {
const [, forceUpdateByUseReducer] = useReducer<(x: number) => number>(x => x + 1, 0);
const [, forceUpdateByUseState] = useState<Record<string, unknown>>({});
return { forceUpdateByUseReducer, forceUpdateByUseState: () => forceUpdateByUseState({}) };
}
For state management tools, what we need most is to get and update the state, and be able to update the view when the state changes. Therefore, the combination of useContext
and useReducer
can completely achieve this functionality, which means we can use useContext
and useReducer
to implement a lightweight redux
.
// https://codesandbox.io/s/react-context-reducer-q1ujix?file=/src/store/reducer.ts
export const initialState = { count: 0 };
type State = typeof initialState;
export const ACTION = {
INCREMENT: "INCREMENT" as const,
SET: "SET" as const
};
type IncrementAction = {
type: typeof ACTION.INCREMENT;
};
type SetAction = {
type: typeof ACTION.SET;
payload: number;
};
export type Action = IncrementAction | SetAction;
export const reducer = (state: State, action: Action) => {
switch (action.type) {
case ACTION.INCREMENT:
return { count: state.count + 1 };
case ACTION.SET:
return { count: action.payload };
default:
throw new Error();
}
};
// https://codesandbox.io/s/react-context-reducer-q1ujix?file=/src/store/context.tsx
import React, { createContext, Dispatch, useReducer } from "react";
import { reducer, initialState, Action } from "./reducer";
export interface ContextProps {
state: {
count: number;
};
dispatch: Dispatch<Action>;
}
const defaultContext: ContextProps = {
state: {
count: 1
},
dispatch: () => void 0
};
export const AppContext = createContext<ContextProps>(defaultContext);
export const AppProvider: React.FC = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
// https://codesandbox.io/s/react-context-reducer-q1ujix?file=/src/App.tsx
import React, { useContext } from "react";
import { AppContext, AppProvider } from "./store/context";
import { ACTION } from "./store/reducer";
interface Props {}
const Children: React.FC = () => {
const { state, dispatch } = useContext(AppContext);
return (
<>
Count: {state.count}
<div>
<button onClick={() => dispatch({ type: ACTION.INCREMENT })}>
INCREMENT
</button>
<button onClick={() => dispatch({ type: ACTION.SET, payload: 10 })}>
SET 10
</button>
</div>
</>
);
};
const App: React.FC<Props> = () => {
return (
<AppProvider>
<Children />
</AppProvider>
);
};
export default App;
Using Context
and Reducer
directly for state management has its advantages. For example, this approach is lightweight and doesn't require the introduction of third-party libraries. However, there are certain issues that need to be addressed. When data changes, all components consuming the Context
will need to re-render. Of course, React
itself relies on multiple re-renders to update the Virtual DOM
, and unless there are performance issues, this optimization space is not very obvious. We have certain optimization strategies for this issue as well:
- You can use or directly use something similar to
useContextSelector
to replaceuseContext
in order to avoid unnecessary re-renders as much as possible, which is quite common inRedux
. - You can use the
React.memo
oruseMemo
approach to avoid unnecessary re-renders. UsinguseImmerReducer
in combination can also alleviate the re-render issue to a certain extent. - Splitting the
Context
of different contextual backgrounds to allow components to selectively subscribe to their ownContext
. In addition to splittingContext
hierarchically based on usage scenarios, a best practice is to separate the variable and immutableContext
, placing the immutableContext
in the outer layer and the variableContext
in the inner layer.
Furthermore, although we can directly use Context
and Reducer
to accomplish basic state management, we still have reasons to use redux
:
redux
has theuseSelector
hook to precisely locate state variables within components for on-demand updates.redux
has its ownredux-devtools
for debugging state, with features such as visual state tracking and time travel.redux
provides a rich set of middleware, such as usingredux-thunk
for asynchronous operations and the officialredux-toolkit
toolset.
https://github.com/WindrunnerMax/EveryDay
https://zhuanlan.zhihu.com/p/360242077
https://zhuanlan.zhihu.com/p/313983390
https://www.zhihu.com/question/24972880
https://www.zhihu.com/question/335901795
https://juejin.cn/post/6948333466668777502
https://juejin.cn/post/6973977847547297800
https://segmentfault.com/a/1190000042391689
https://segmentfault.com/a/1190000023747431
https://zh-hans.reactjs.org/docs/context.html#gatsby-focus-wrapper
https://stackoverflow.com/questions/67537701/react-topic-context-vs-redux