【React】useContext与useReducer结合实现状态管理

7,073 阅读2分钟

useContext

可以考虑这样一个场景,如组件树的机构如下图所示,在App组件中我们实现了用户登录,而登录状态需要组件都需使用,如A、B、C等组件需要拿到登录态从而响应用户的操作。如果使用props进行登录态的透传,会不可避免的进行一层一层的传递,如果嵌套层级特别深的时候,这种传递是很危险且不可控的。 image.png useContext Api可以在这种场景展现其的莫大作用,如下我们通过context将登录态的内容进行传递。

// login.context.ts
import React, { useContext, useEffect } from 'react';

interface IUser {
    name: string;
    age: number;
    id: string;
}

const LoginContext = React.createContext<IUser>();

export const LoginContextProvider = (props) => {
    const [user, setUser] = useState<IUser>({});
    useEffect(() => {
        (async () => {
            const userinfo = await fetch('user/get');
            setUser(userinfo);
        })();
    }, []);
    return (
        <LoginContext.Provider value={user}>
            {props.childred}
        </LoginContext.Provider>
    );
};

export useLoginContext = () => {
    return useContext(LoginContext);
};

// App 使用
import { useLoginContext } from './login.context';
const App = () => {
    const {name} = useLoginContext();
    
    return (
        <div>
            当前登录用户是:{name}
        </div>
    );
};

const AppWithContext = () => {
    return (
        <LoginContextProvider>
            <App />
        </LoginContextProvider>
    );
};

export default AppWithContext;

如上即可实现在任何需要的节点使用useLoginContext获取到最新的user信息,从而避免props的透传,当context的值更新时组件也会重新渲染。

useReducer

useReducer作为useState 的替代方案,详细的文档可以参看官网文档https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer。下面我们使用useReducercreate-react-app的示例重新实现一下。

// App.tsx
import React, { useReducer, useCallback } from 'react';

export default const App = () => {
    const reducer = useCallback((preState, action) => {
        const { type } = action;
        switch(type) {
            default:
                return preState;
            case 'increment':
                return { count: preState.count + 1 };
        }
    }, []);
    const [state, dispatch] = useReducer(reducer, { count: 0 });
    return (
        <div>
            <p>
               <button
                   type="button"
                   onClick={() => dispatch({ type: 'increment' })}
               >
                count is: {state.count}
              </button>
        </p>
        </div>
    );
};

useContext + useReducer = Redux

在实际的React开发项目的时候,有些全局状态数据会选择使用redux进行管理,但随着React Hooks的使用,有部分场景也可以使用useContext + useReducer实现Redux的功能。如下的场景我们就可以考虑使用useContext + useReducer的方式实现数据共享。

  1. 部分复杂组件需要进行深层props数据传递
  2. 项目较小,但仍然需要共享全局状态数据

useContext + useReducer两者结合替代Redux思路还是很简单的:

  1. userReducer获取statedispatch
  2. userContextstatedispatch共享到子组件
// Count.context.tsx
import React, { createContext, useCallback, Dispatch, useReducer, useContext } from 'react'

interface IState {
  count: number;
}

interface IContext {
  state: IState;
  dispatch: Dispatch<{
    type: string;
    payload?: Partial<IState>;
  }>;
}

const initValue: IState = {
  count: 0,
};

const Context = createContext<IContext>({
  state: initValue,
  dispatch: () => {},
});

export const ReducerContextProvider: React.FC = (props) => {
  const reducer = useCallback((preState: IState, action: {
    type: string;
    payload?: Partial<IState>;
  }) => {
    const { type, payload } = action;
    switch(type) {
      default:
        return preState;
        case 'increment':
          return {
            ...preState,
            count: preState.count + 1,
          };
        case 'decrement':
          return {
            ...preState,
            count: preState.count - 1,
          };
      case 'reset':
        return {
          ...preState,
          ...payload,
        };
    }
  }, []);
  const [state, dispatch] = useReducer(reducer, initValue);

  return (
    <Context.Provider value={{state, dispatch}}>
      {props.children}
    </Context.Provider>
  );
};

export const useReducerContext = () => {
  return useContext(Context);
};

// App.tsx
import { ReducerContextProvider, useReducerContext } from './Count.context';

const App = () => {
    const { state, dispatch } = useReducerContext();
    
    return (
        <div>
            <p>
               <button
                   type="button"
                   onClick={() => dispatch({ type: 'increment' })}
               >
                count is: {state.count}
              </button>
        </p>
        </div>
    );
};

const AppWithStore = () => {
  return (
    <ReducerContextProvider>
      <App />
    </ReducerContextProvider>
  )
}

export default AppWithStore