如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?

Posted

技术标签:

【中文标题】如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?【英文标题】:How to correctly use a curried selector function with react-redux's useSelector hook? 【发布时间】:2019-12-19 05:11:06 【问题描述】:

我正在使用带有钩子的 react-redux,并且我需要一个选择器,该选择器采用不是道具的参数。 documentation 状态

选择器函数不接收 ownProps 参数。然而, props 可以通过闭包(参见下面的示例)或使用 一个咖喱选择器。

但是,他们没有提供示例。如文档中所述,正确的咖喱方法是什么?

这就是我所做的,它似乎有效,但这是对的吗?从useSelector 函数返回一个函数是否有影响(似乎它永远不会重新渲染?)

// selectors
export const getTodoById = state => id => 
  let t = state.todo.byId[id];
  // add display name to todo object
  return  ...t, display: getFancyDisplayName(t) ;
;

const getFancyDisplayName = t => `$t.id: $t.title`;

// example component
const TodoComponent = () => 
   // get id from react-router in URL
   const id = match.params.id && decodeURIComponent(match.params.id);

   const todo = useSelector(getTodoById)(id);

   return <span>todo.display</span>;

【问题讨论】:

这是个好问题,也找不到任何示例,但您仍然可以使用接收ownPropsconnect。或者你可以反转柯里化(例如id =&gt; state =&gt; state.todo.byId[id] 然后useSelector(getTodoById(id)) 或者直接自己调用:useSelector(state =&gt; getTodoById(state)(id)); 这样,返回值会在钩子中被考虑在内,并且应该相应地重新渲染。 @EmileBergeron 我想更好地理解您的第二条评论。不 useSelector 只是进行相等比较以查看是否更改,这对您的功能有何作用? useSelector 订阅了商店,因为你的getTodoById 总是返回一个新函数,我认为你的组件总是会重新渲染。 经过测试,我写了一个答案来突出useSelector(getTodoById)(id)的问题。 【参考方案1】:

When the return value of a selector is a new function, the component will always re-render on each store change.

useSelector() 默认使用严格的=== 引用相等检查,而不是浅相等

你可以用一个超级简单的选择器来验证这一点:

const curriedSelector = state => () => 0;

let renders = 0;
const Component = () => 
  // Returns a new function each time
  // triggers a new render each time
  const value = useSelector(curriedSelector)();
  return `Value $value (render: $++renders)`;

即使value 始终为0,组件也会在每个存储操作上重新渲染,因为useSelector 不知道我们正在调用函数 .

但如果我们确保useSelector 接收最终的value 而不是函数,那么组件只会在真正的value 更改时呈现。

const curriedSelector = state => () => 0;

let renders = 0;
const Component = () => 
  // Returns a computed value
  // triggers a new render only if the value changed
  const value = useSelector(state => curriedSelector(state)());
  return `Value $value (render: $++renders)`;


结论是它可以工作,但是每次调用时从与useSelector 一起使用的选择器返回一个新函数(或任何新的非primitives)是效率极低的。

props 可以通过闭包(参见下面的示例)或使用柯里化选择器来使用。

文档意味着:

关闭useSelector(state =&gt; state.todos[props.id]) 咖喱useSelector(state =&gt; curriedSelector(state)(props.id))

connect 始终可用,如果您稍微更改选择器,则两者都可以使用。

export const getTodoById = (state,  id ) => /* */

const Component =  props => 
  const todo = useSelector(state => getTodoById(state, props));

// or
connect(getTodoById)(Component)

请注意,由于您从选择器返回一个对象,您可能希望将默认相等检查 useSelector 更改为 shallow equality check。

import  shallowEqual  from 'react-redux'

export function useShallowEqualSelector(selector) 
  return useSelector(selector, shallowEqual)

或者只是

const todo = useSelector(state => getTodoById(state, id), shallowEqual);

如果您在选择器中执行昂贵的计算,或者数据嵌套很深并且性能成为问题,请查看Olivier's answer which uses memoization。

【讨论】:

就我而言,您甚至不需要柯里化,只需将状态和参数作为参数传递? useSelector(state =&gt; getTodoWithDisplay(state, id)) @user210757 我用适用于connectuseSelector 的选择器的通用形式更新了我的答案。 @user210757 但是是的,你是对的,如果你愿意,你可以直接传递id 如果他在getTodoById 函数中返回new object,则该组件将在每次商店更改时重新渲染 @OlivierBoissé 根据文档,我现在使用shallowEqual 解决这个问题。【参考方案2】:

这是一个解决方案,它使用 memoïzation 在每次商店更改时不重新渲染组件:

首先我创建一个函数来制作选择器,因为选择器依赖于组件属性id,所以我希望每个组件实例都有一个新选择器

当 todo 或 id 属性没有改变时,选择器会阻止组件重新渲染。

最后我使用useMemo,因为我不想每个组件实例有多个选择器

您可以查看last example of the documentation以了解更多信息

// selectors
const makeGetTodoByIdSelector = () => createSelector(
   state => state.todo.byId,
   (_, id) => id,
   (todoById, id) => (
       ...todoById[id], 
       display: getFancyDisplayName(todoById[id])
   )
);

const getFancyDisplayName = t => `$t.id: $t.title`;

// example component
const TodoComponent = () => 
  // get id from react-router in URL
  const id = match.params.id && decodeURIComponent(match.params.id);

  const getTodoByIdSelector = useMemo(makeGetTodoByIdSelector, []);

  const todo = useSelector(state => getTodoByIdSelector(state, id));

  return <span>todo.display</span>;

【讨论】:

您最初的答案没有添加任何新内容,但既然您已经添加了记忆化并且它与其他答案不同,我删除了我的反对意见,并在我自己的答案中添加了一个链接。 不过,我将把解释移到顶部,将代码示例留在最后,这样您就更清楚地专注于记忆。 ;)【参考方案3】:

是的,就是这样做的,简化的例子:

// Curried functions
const getStateById = state => id => state.todo.byId[id];

const getIdByState = id => state => state.todo.byId[id];

const SOME_ID = 42;

const TodoComponent = () => 
  // id from API
  const id = SOME_ID;

  // Curried
  const todoCurried = useSelector(getStateById)(id);
  const todoCurried2 = useSelector(getIdByState(id));

  // Closure
  const todoClosure = useSelector(state => state.todo.byId[id]);

  // Curried + Closure
  const todoNormal = useSelector(state => getStateById(state)(id));

  return (
    <>
      <span>todoCurried.display</span>
      <span>todoCurried2.display</span>
      <span>todoClosure.display</span>
      <span>todoNormal.display</span>
    </>
  );
;

完整示例:

【讨论】:

谢谢,我知道这只是一个示例,但闭包示例对我不起作用,因为我正在做的是动态获取数据 - 数据从 api 返回并存储在减速器(没有显示字段),我希望选择器获得一个“待办事项”并将派生的“显示”添加到其中。 我在赞成这个答案后发现它部分正确且不完整。大多数情况下,由 curried 选择器 useSelector(getStateById)(id) 和每次返回新对象时不起作用的严格相等引起的不断重新渲染。小心!【参考方案4】:

这是 TypeScript 的 helper-hook useParamSelector,它实现了 Redux Toolkit 的官方方法。

挂钩实现:

// Define types and create new hook 
export type ParametrizedSelector<A, R> = (state: AppState, arg: A) => R;
export const proxyParam: <T>(_: AppState, param: T) => T = (_, param) => param;
export function useParamSelector<A, R>(
  selectorCreator: () => ParametrizedSelector<A, R>,
  argument: A,
  equalityFn: (left: R, right: R) => boolean = shallowEqual
): R 
  const memoizedSelector = useMemo(() => 
    const parametrizedSelector = selectorCreator();
    return (state: AppState) => parametrizedSelector(state, argument);
  , [typeof argument === 'object' ? JSON.stringify(argument) : argument]);
  return useSelector(memoizedSelector, equalityFn);

创建参数化选择器:

export const selectUserById = (): ParametrizedSelector<string, User> =>
  createSelector(proxyParam, selectAllUsers, (id, users) => users.find((it) => it.id === id));

并使用它:

const user = useParamSelector(selectUserById, 1001); // in components
const user = selectUserById()(getState(), 1001); // in thunks

您还可以将它与使用 reselect 的 createSelector 创建的选择器结合使用。

【讨论】:

感谢分享!看起来你想像下面这样写它,因为filter 返回一个数组:export const selectUserById = (): ParametrizedSelector&lt;string, User | undefined&gt; =&gt; createSelector(proxyParam, selectAllUsers, (id, users) =&gt; users.find((it) =&gt; it.id === id));

以上是关于如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 react-redux 中正确键入映射到道具的动作创建者

如何使用 thunk 在 react-redux 挂钩中进行异步调用?

如何重置 react-redux 的值?

React-Redux Connected Component测试正确的操作已在click事件上发送

带有 Typescript 和 react-redux 的有状态组件:“typeof MyClass”类型的参数不可分配给 Component 类型的参数

React-Redux