为 React Context 实现 useSelector 等价物?

IT技术 reactjs redux react-redux react-hooks react-context
2021-05-05 13:37:42

有很多文章展示了如何用上下文和钩子替换 Redux(例如,参见Kent Dodds 的这篇文章)。基本思想是通过上下文使您的全局状态可用,而不是将其放在 Redux 存储中。但是这种方法有一个大问题:只要上下文发生任何更改,订阅上下文的组件就会重新呈现,无论您的组件是否关心刚刚更改的状态部分。对于函数式组件,React-redux 通过useSelector hook解决了这个问题. 所以我的问题是:是否可以创建像 useSelector 这样的钩子,它会抓取一段上下文而不是 Redux 存储,具有与 useSelector 相同的签名,并且就像 useSelector 一样,只会在“选择”部分上下文发生了变化?

(注意:React Github 页面上的这个讨论表明它不能完成)

4个回答

不,这不可能。每当您将新的上下文值放入提供者中时,所有使用者都会重新渲染,即使他们只需要该上下文值的一部分。

就是我们在 React-Redux v6 中放弃使用上下文来传播状态更新,并在 v7 中改回使用直接存储订阅具体原因之一

一个社区编写的 React RFC 将选择器添加到 context,但没有迹象表明 React 团队会真正实施该 RFC。

正如markerikson回答的那样,这是不可能的,但是您可以不使用外部依赖项的情况下解决它,也无需退回到手动订阅。

作为一种解决方法,您可以让组件重新渲染,但通过使用useMemo.

function Section(props) {
  const partOfState = selectPartOfState(useContext(StateContext))

  // Memoize the returned node
  return useMemo(() => {
    return <div>{partOfState}</div>
  }, [partOfState])
}

这是因为在内部,当 React diffs 2 个版本的虚拟 DOM 节点时,如果它遇到完全相同的引用,它将完全跳过协调该节点。

我创建了一个使用 ContextAPI 管理状态的工具包。它提供useSelector(带有自动完成功能)以及useDispatch.

图书馆在这里可用:

它用:

这是我对这个问题的看法:我使用函数作为带有 useMemo 的子模式来创建一个通用的选择器组件:

import React, {
  useContext,
  useReducer,
  createContext,
  Reducer,
  useMemo,
  FC,
  Dispatch
} from "react";
export function createStore<TState>(
  rootReducer: Reducer<TState, any>,
  initialState: TState
) {
  const store = createContext({
    state: initialState,
    dispatch: (() => {}) as Dispatch<any>
  });
  const StoreProvider: FC = ({ children }) => {
    const [state, dispatch] = useReducer(rootReducer, initialState);
    return (
      <store.Provider value={{ state, dispatch }}>{children}</store.Provider>
    );
  };
  const Connect: FC<{
    selector: (value: TState) => any;
    children: (args: { dispatch: Dispatch<any>; state: any }) => any;
  }> = ({ children, selector }) => {
    const { state, dispatch } = useContext(store);
    const selected = selector(state);
    return useMemo(() => children({ state: selected, dispatch }), [
      selected,
      dispatch,
      children
    ]);
  };
  return { StoreProvider, Connect };
}

计数器组件:

import React, { Dispatch } from "react";

interface CounterProps {
  name: string;
  count: number;
  dispatch: Dispatch<any>;
}
export function Counter({ name, count, dispatch }: CounterProps) {
  console.count("rendered Counter " + name);
  return (
    <div>
      <h1>
        Counter {name}: {count}
      </h1>
      <button onClick={() => dispatch("INCREMENT_" + name)}>+</button>
    </div>
  );
}

用法:

import React, { Reducer } from "react";
import { Counter } from "./counter";
import { createStore } from "./create-store";
import "./styles.css";
const initial = { counterA: 0, counterB: 0 };
const counterReducer: Reducer<typeof initial, any> = (state, action) => {
  switch (action) {
    case "INCREMENT_A": {
      return { ...state, counterA: state.counterA + 1 };
    }
    case "INCREMENT_B": {
      return { ...state, counterB: state.counterB + 1 };
    }
    default: {
      return state;
    }
  }
};
const { Connect, StoreProvider } = createStore(counterReducer, initial);
export default function App() {
  return (
    <StoreProvider>
      <div className="App">
        <Connect selector={(state) => state.counterA}>
          {({ dispatch, state }) => (
            <Counter name="A" dispatch={dispatch} count={state} />
          )}
        </Connect>
        <Connect selector={(state) => state.counterB}>
          {({ dispatch, state }) => (
            <Counter name="B" dispatch={dispatch} count={state} />
          )}
        </Connect>
      </div>
    </StoreProvider>
  );
}

工作示例:CodePen