• 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就是使用这种方式传递数据,这也解释了为什么要在所有>的外面。
    当然在这里我们还是要额外讨论下是不是需要使用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>
        </>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这种对组件的控制反转减少了在应用中要传递的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>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    // 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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>
        </>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    或者我们也可以相对简单地去使用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({}) };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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();
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    // 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>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    // 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    我们直接使用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
    
    • 1

    参考

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    3GPP R18冻结,哪些信息值得关注?
    【UE4 反射系统】 UCLAS UFUNCTION UPROPERTY 宏简单解析 持续更新
    软考高级之系统架构师之数据流图和流程图
    汇率查询接口
    SpringBoot集成redis依赖包及步骤
    【玩儿】Win 11 安装安卓子系统
    前端后端的爱恨情仇
    音频修复和增强工具 iZotope RX 10 for mac激活最新
    【摸鱼神器】UI库秒变LowCode工具——列表篇(一)设计与实现
    证件照素材大合集(全网最全版本),满足证件照的一切需求!
  • 原文地址:https://blog.csdn.net/qq_40413670/article/details/126910751