redux 是否为React上下文实现useSelector等效项?

aelbi1ox  于 2022-11-12  发布在  React
关注(0)|答案(8)|浏览(148)

有很多文章介绍了如何用上下文和钩子来替换Redux(例如,请参见this one from Kent Dodds)。基本思想是通过上下文来提供全局状态,而不是将其放在Redux存储中。但这种方法存在一个大问题:无论您的组件是否关心刚刚改变的那部分状态,订阅了上下文的组件都将被重新呈现.对于功能组件,React-redux通过useSelector钩子解决了这个问题.所以我的问题是:是否可以创建一个像useSelector这样的钩子来获取上下文的一部分而不是Redux存储区,它具有与useSelector相同的签名,并且像useSelector一样,只在上下文的“选定”部分发生更改时才重新呈现组件?
(note:this discussion在React Github页面上提示这是不可能的)

1szpjjfi

1szpjjfi1#

不,这是不可能的。任何时候你把一个新的上下文值放入一个提供者,* 所有 * 消费者都会重新呈现,即使他们只需要那个上下文值的一部分。
这是specifically one of the reasons why we gave up on using context to propagate state updates in React-Redux v6, and switched back to using direct store subscriptions in v7
a community-written React RFC to add selectors to context,但没有迹象表明React团队会真正实施该RFC。

nnsrf1az

nnsrf1az2#

正如markerikson所回答的,这是不可能的,但是您可以解决这个问题,而不使用外部依赖项,也不需要退回到手动订阅。
作为一种解决方案,您可以 * 让组件重新呈现,但跳过VDOM协调 *,方法是使用useMemo存储返回的React元素。

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

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

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

vybvopom

vybvopom3#

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

该库可从以下位置获得:

它使用:

6ioyuze2

6ioyuze24#

下面是我对这个问题的看法:我将该函数作为子模式与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

f0brbegy

f0brbegy5#

我已经创建了这个小包react-use-context-selector,它正好完成了这项工作。
我使用了Redux的useSelector中使用的相同方法,它还附带了类型声明,并且返回类型与选择器函数的返回类型相匹配,这使得它适合在TS项目中使用。

function MyComponent() {
  // This component will re-render only when the `name` within the context object changes.
  const name = useContextSelector(context, value => value.name);

  return <div>{name}</div>;
}
vecaoik1

vecaoik16#

React 18随附了带有新挂钩useSyncExternalStore的外部存储解决方案(Redux或Zustand类方法)。
对于React 18:定义createStoreuseStore函数:

import React, { useCallback } from "react";
import { useSyncExternalStore } from "react";

const createStore = (initialState) => {
  let state = initialState;
  const getState = () => state;
  const listeners = new Set();
  const setState = (fn) => {
    state = fn(state);
    listeners.forEach((l) => l());
  };
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  return { getState, setState, subscribe };
};

const useStore = (store, selector) =>
  useSyncExternalStore(
    store.subscribe,
    useCallback(() => selector(store.getState()), [store, selector])
  );

现在使用它:

const store = createStore({ count: 0, text: "hello" });

const Counter = () => {
  const count = useStore(store, (state) => state.count);
  const inc = () => {
    store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
  };
  return (
    <div>
      {count} <button onClick={inc}>+1</button>
    </div>
  );
};

对于React 17和任何支持挂钩的React版本:

***选项1:**您可以使用外部库(由React团队维护)use-sync-external-store/shim

import { useSyncExternalStore } from "use-sync-external-store/shim";

***选项2:**如果您不想添加新库,也不关心并发性问题:

const createStore = (initialState) => {
  let state = initialState;
  const getState = () => state;
  const listeners = new Set();
  const setState = (fn) => {
    state = fn(state);
    listeners.forEach((l) => l());
  }
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }
  return {getState, setState, subscribe}
}

const useStore = (store, selector) => {
  const [state, setState] = useState(() => selector(store.getState()));
  useEffect(() => {
    const callback = () => setState(selector(store.getState()));
    const unsubscribe = store.subscribe(callback);
    callback();
    return unsubscribe;
  }, [store, selector]);
  return state;
}

资料来源:

3yhwsihp

3yhwsihp7#

使用HoCReact.memo防止额外渲染的简单方法:

const withContextProps = (WrappedComponent) => {
    const MemoizedComponent = React.memo(WrappedComponent);

    return (props) => {
        const state = useContext(myContext);
        const mySelectedState = state.a.b.c;

        return (
            <MemoizedComponent
                {...props}
                mySelectedState={mySelectedState} // inject your state here
            />
        );
    };
};

withContextProps(MyComponent)
e1xvtsh3

e1xvtsh38#

我做了一个库react-context-slices,它可以解决你所寻找的问题。它的想法是把存储或状态分解成状态片,也就是更小的对象,并为每一个对象创建一个上下文。我告诉你的那个库就是这样做的,它公开了一个函数createSlice,它接受一个reducer,初始状态,片的名称,以及创建动作的函数。您可以根据需要创建切片(“todos”、“counter”等),并将它们轻松地集成到一个唯一的接口中,在最后公开两个自定义钩子useValuesuseActions,它们可以“攻击”所有切片(也就是说,在你的客户端组件中,你不使用useTodosValues,而是使用useValues)。关键是useValues接受切片的名称,所以它等同于redux中的useSelector。这个库和redux一样使用immer。它是一个非常小的库,关键是如何使用它。这个函数库只公开了两个函数,createSlicecomposeProviders

相关问题