如何正确使用带有 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>;
【问题讨论】:
这是个好问题,也找不到任何示例,但您仍然可以使用接收ownProps
的connect
。或者你可以反转柯里化(例如id => state => state.todo.byId[id]
然后useSelector(getTodoById(id))
。
或者直接自己调用:useSelector(state => 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)`;
结论是它可以工作,但是每次调用时从与 props 可以通过闭包(参见下面的示例)或使用柯里化选择器来使用。 文档意味着: 请注意,由于您从选择器返回一个对象,您可能希望将默认相等检查 或者只是 如果您在选择器中执行昂贵的计算,或者数据嵌套很深并且性能成为问题,请查看Olivier's answer which uses memoization。useSelector
一起使用的选择器返回一个新函数(或任何新的非primitives)是效率极低的。
useSelector(state => state.todos[props.id])
咖喱useSelector(state => 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);
【讨论】:
就我而言,您甚至不需要柯里化,只需将状态和参数作为参数传递?useSelector(state => getTodoWithDisplay(state, id))
@user210757 我用适用于connect
和useSelector
的选择器的通用形式更新了我的答案。
@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<string, User | undefined> => createSelector(proxyParam, selectAllUsers, (id, users) => users.find((it) => it.id === id));
以上是关于如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 react-redux 中正确键入映射到道具的动作创建者
如何使用 thunk 在 react-redux 挂钩中进行异步调用?
React-Redux Connected Component测试正确的操作已在click事件上发送
带有 Typescript 和 react-redux 的有状态组件:“typeof MyClass”类型的参数不可分配给 Component 类型的参数