Context與Reducer

Context與Reducer

ContextReact提供的一種跨組件的通信方案,useContextuseReducer是在React 16.8之後提供的Hooks API,我們可以通過useContextuseReducer來完成全局狀態管理例如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了。
說回ContextContext提供了類似於服務提供者與消費者模型,在通過React.createContext創建Context後,可以通過Context.Provider來提供數據,最後通過Context.Consumer來消費數據。在React 16.8之後,React提供了useContext來消費ContextuseContext接收一個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

useReduceruseState的替代方案,類似於Redux的使用方法,其接收一個形如(state, action) => newStatereducer,並返回當前的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

對於狀態管理工具而言,我們需要的最基礎的就是狀態獲取與狀態更新,並且能夠在狀態更新的時候更新視圖,那麼useContextuseReducer的組合則完全可以實現這個功能,也就是說,我們可以使用useContextuseReducer來實現一個輕量級的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;

我們直接使用ContextReducer來完成狀態管理是具有一定優勢的,例如這種實現方式比較輕量,且不需要引入第三方庫等。當然,也有一定的問題需要去解決,當數據變更時,所有消費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在內層。

此外,雖然我們可以直接使用ContextReducer來完成基本的狀態管理,我們依然也有着必須使用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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章