Context與Reducer
Context
是React
提供的一種跨組件的通信方案,useContext
與useReducer
是在React 16.8
之後提供的Hooks API
,我們可以通過useContext
與useReducer
來完成全局狀態管理例如Redux
的輕量級替代方案。
useContext
React Context
適用於父子組件以及隔代組件通信,React Context
提供了一個無需爲每層組件手動添加props
就能在組件樹間進行數據傳遞的方法。一般情況下在React
應用中數據是通過props
屬性自上而下即由父及子進行傳遞的,而一旦需要傳遞的層次過多,那麼便會特別麻煩,例如主題配置theme
、地區配置locale
等等。Context
提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞props
。例如React-Router
就是使用這種方式傳遞數據,這也解釋了爲什麼<Router>
要在所有<Route
>的外面。
當然在這裏我們還是要額外討論下是不是需要使用Context
,使用Context
可能會帶來一些性能問題,因爲當Context
數據更新時,會導致所有消費Context
的組件以及子組件樹中的所有組件都發生re-render
。那麼,如果我們需要類似於多層嵌套的結構,應該去如何處理,一種方法是我們直接在當前組件使用已經準備好的props
渲染好組件,再直接將組件傳遞下去。
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>
</>
);
};
這種對組件的控制反轉減少了在應用中要傳遞的props
數量,這在很多場景下可以使得代碼更加乾淨,使得根組件可以有更多的把控。但是這並不適用於每一個場景,這種將邏輯提升到組件樹的更高層次來處理,會使得這些高層組件變得更復雜,並且會強行將低層組件適應這樣的形式,這可能不會是你想要的。這樣的話,就需要考慮使用Context
了。
說回Context
,Context
提供了類似於服務提供者與消費者模型,在通過React.createContext
創建Context
後,可以通過Context.Provider
來提供數據,最後通過Context.Consumer
來消費數據。在React 16.8
之後,React
提供了useContext
來消費Context
,useContext
接收一個Context
對象並返回該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
useReducer
是useState
的替代方案,類似於Redux
的使用方法,其接收一個形如(state, action) => newState
的reducer
,並返回當前的state
以及與其配套的dispatch
方法。
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>
</>
);
}
或者我們也可以相對簡單地去使用useReducer
,例如實現一個useForceUpdate
,當然使用useState
實現也是可以的。
function useForceUpdate() {
const [, forceUpdateByUseReducer] = useReducer<(x: number) => number>(x => x + 1, 0);
const [, forceUpdateByUseState] = useState<Record<string, unknown>>({});
return { forceUpdateByUseReducer, forceUpdateByUseState: () => forceUpdateByUseState({}) };
}
useContext + useReducer
對於狀態管理工具而言,我們需要的最基礎的就是狀態獲取與狀態更新,並且能夠在狀態更新的時候更新視圖,那麼useContext
與useReducer
的組合則完全可以實現這個功能,也就是說,我們可以使用useContext
與useReducer
來實現一個輕量級的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;
我們直接使用Context
與Reducer
來完成狀態管理是具有一定優勢的,例如這種實現方式比較輕量,且不需要引入第三方庫等。當然,也有一定的問題需要去解決,當數據變更時,所有消費Context
的組件都會需要去渲染,當然React
本身就是以多次的re-render
來完成的Virtual DOM
比較由此進行視圖更新,在不出現性能問題的情況下這個優化空間並不是很明顯,對於這個問題我們也有一定的優化策略:
- 可以完成或者直接使用類似於
useContextSelector
代替useContext
來儘量避免不必要的re-render
,這方面在Redux
中使用還是比較多的。 - 可以使用
React.memo
或者useMemo
的方案來避免不必要的re-render
,通過配合useImmerReducer
也可以在一定程度上緩解re-render
問題。 - 對於不同上下文背景的
Context
進行拆分,用來做到組件按需選用訂閱自己的Context
。此外除了層級式按使用場景拆分Context
,一個最佳實踐是將多變的和不變的Context
分開,讓不變的Context
在外層,多變的Context
在內層。
此外,雖然我們可以直接使用Context
與Reducer
來完成基本的狀態管理,我們依然也有着必須使用redux
的理由:
redux
擁有useSelector
這個Hooks
來精確定位組件中的狀態變量,來實現按需更新。redux
擁有獨立的redux-devtools
工具來進行狀態的調試,擁有可視化的狀態跟蹤、時間回溯等功能。redux
擁有豐富的中間件,例如使用redux-thunk
來進行異步操作,redux-toolkit
官方的工具集等。
每日一題
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