我应该如何在 Redux Toolkit 中使用选择器?

say*_*ode 7 optimization reactjs redux redux-toolkit

我正在学习 Redux 工具包。从 React POV 来看,使用内联箭头函数从 useSelector 中访问您需要的状态的任何部分似乎非常直观,然后进行任何计算。作为示例,请考虑 redux 存储中的购物车商品及其数据(例如商品计数)。

function CartItemCounter({ itemId }){
  const cart = useSelector(state => state.cart);
  const itemInCart = cart.items[itemId];
  const count = itemInCart?.count || 0;

  return <div>{itemId} - {count} nos</div>
}
Run Code Online (Sandbox Code Playgroud)

但我看到所有这些信息都表明您应该在切片旁边定义选择器,使用 createSelector 等。什么是正确的方法,为什么更好?

say*_*ode 33

现有的信息本质上是在讨论使用时的不同级别的优化useSelector。首先你需要了解的是useSelector内部是如何运作的。

怎样useSelector运作?

当你将一个函数传递给useSelector(显然是在 React 组件内部)时,它本质上会挂接到全局 redux 状态。每当全局状态的任何部分发生任何更改时(即dispatch()从应用程序的任何部分调用时),redux 将运行您在应用程序中传递给的所有函数useSelector,并执行某些检查。

Redux 将从每个函数中获取结果,并将其与上次运行相同函数时获得的值进行比较。

它是如何进行这种比较的?

它使用引用相等来进行此比较。因此,如果 redux 必须认为函数的结果没有改变,那么从函数返回的值必须是原始值并且相等。

4 === 4 // true
'itemA' === 'itemA // true
Run Code Online (Sandbox Code Playgroud)

或者,返回的值必须是具有相同引用的派生数据类型(数组、对象)。所以本质上是同一个对象。

const x = { name: 'Shashi' }
const fn1 = () => x;
const fn2 = () => x;
const fn3 = () => { name: 'Shashi' }

fn1() === fn2(); // true
fn1() === fn3(); // false, because the objects are different, with different references
Run Code Online (Sandbox Code Playgroud)

实际上,如果某个键(或嵌套对象的键)发生更改,或者您使用分派操作手动更改整个对象(这与 immer库集成有关),则 redux 会更改包装对象。这与您在常规 React 中的做法类似。

/* See how most keys are spread in, and will hence maintain reference equality.
While certain keys like 'first', 'first.second', 'first.second[action.someId]' 
are changed with new objects, and so will break reference equality */
function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue,
        },
      },
    },
  }
}
Run Code Online (Sandbox Code Playgroud)

否则,它会在其状态中维护相同的对象,并在您访问它们时返回具有相同引用的完全相同的对象。为了验证这一点,如果您两次访问购物车,它实际上将是同一个对象。

const cart1 = useSelector(state => state.cart)
const cart2 = useSelector(state => state.cart)
cart1 === cart2; // true
Run Code Online (Sandbox Code Playgroud)

这个比较有什么作用呢?

如果比较返回 true,即新值与旧值相同,Redux 会告诉useSelectortf 实例冷静下来,而不做任何事情。然而,如果它返回 false,它会告诉该组件重新渲染。毕竟,您从状态访问的值已经“更改”(根据 Redux 法则),因此您可能希望显示新值。

有了这些信息,我们就可以改变我们传递给 的函数类型useSelector,以获得一定的优化好处。

优化级别

级别 0:内联访问切片数据

const cart = useSelector(state => state.cart)

// extract the information you need from within the cart
const itemInCart = cart.items[itemId];
const count = itemInCart?.count || 0;
Run Code Online (Sandbox Code Playgroud)

这不是访问数据的好方法。您实际上需要购物车中的一部分数据,但您正在获取整个数据,并在选择器外部进行计算。

问题:

  • 当您将这样的内容内联时,如果您将来更改数据的形状会发生什么?你必须去每个使用它的地方useSelector并手动更改它。不太好。
  • 更重要的是,每当购物车的任何部分发生变化时,整个购物车对象实际上都会发生变化。所以 Redux 看到你的组件请求购物车,并认为

    购物车已更改。该组件正在请求购物车。它可能应该重新渲染。

BAM该组件的每个实例都会重新渲染。为了什么?您引用的项目的数量可能没有改变。所以理想情况下不应该重新渲染。

第 1 级:集中选择器

一个简单的优化是将选择器功能放在切片旁边的集中位置。这样,如果您的数据形状将来发生变化,您只需在一个位置更改它,您的整个应用程序(无论在何处使用该数据)都将使用新的形状。

// inside or next to the slice file
const selectCart = (state) => state.cart

//...
// somewhere inside a react component
const cart = useSelector(selectCart)
Run Code Online (Sandbox Code Playgroud)

第 2 级:访问相关数据

由于 redux 正在比较选择器函数的结果,因此如果您想避免不必要的重新渲染,您需要确保结果具有引用相等性 ( ===)。因此,在选择器中定位您想要查看的确切值。

// extract the information you need from within the cart, *within the selector*
const count = useSelector(state => state.cart.items[itemId]?.count || 0)
// You don't have to use a one-liner, a multi-line function is better for readability
Run Code Online (Sandbox Code Playgroud)

当 Redux 执行这些函数时,它会为每个单独的 useSelector 保留从这些选择器函数返回的值的记录。这次,除了实际更改的计数器之外,每个计数器的值都将相同。所有其他实际上没有改变值的计数器不必再进行不必要的重新渲染!

如果你们中有人认为这是不成熟的优化,那么答案是否定的。这更像是在useEffects 上放置一个依赖数组以避免无限循环。

别忘了一级优化,我们也可以集中提取这个函数

const selectItemById = (state, itemId) => (state.cart.items[itemId]?.count || 0);

function CartItemCounter({ itemId }){
  //...
  // somewhere inside a react component
  const count = useSelector((state) => selectCart(state, itemId))
  //...
}
Run Code Online (Sandbox Code Playgroud)

这样就解决了我们所有的问题,对吗? 目前来说,是的。但是如果这个选择器函数必须运行一些昂贵的计算怎么办?

const selectSomething = (state) => reallyExpensiveFn(state.cart)

//...
// somewhere inside a react component
const cart = useSelector(selectSomething)
Run Code Online (Sandbox Code Playgroud)

你不想继续跑步吧?

或者,如果您别无选择,只能从 select 函数返回新对象,该怎么办?这种情况的常见场景是从状态返回数据的子集。

const selectFilteredItems = (state) => state.itemsArray.filter(checkCondition) // the filter method will always return a new array

//...
// somewhere inside a react component
const cart = useSelector(selectFilteredItems) // re-renders every time
Run Code Online (Sandbox Code Playgroud)

要解决这个问题,您必须记住或缓存函数调用的结果。本质上,您需要确保如果输入参数相同,则结果将保持与先前结果的引用相等。这就需要维护某种缓存状态。

3级:createSelector

幸运的是,使用 Redux Tookit 重新导出的 Reselect 库可以为您完成这项工作。你可以看看redux工具包的语法。

const selectFilteredItems = createSelector(
  (state) => state.itemsArray, // the first argument accesses relevant data from global state
  (itemsArray) => itemsArray.filter(checkCondition) // the second parameter conducts the transformation
)

//...
// somewhere inside a react component
const cart = useSelector(selectFilteredItems) // re-renders only when needed
Run Code Online (Sandbox Code Playgroud)

这里第二个函数称为转换函数,是我们进行昂贵计算的地方,或者是返回不一致引用作为结果的函数(过滤器、映射等)。缓存createSelector

  • a) 变换函数的参数
  • b) 上次调用该函数的转换函数的结果selectFilteredItems。如果参数相同,它会跳过执行转换函数,并给出上次执行时得到的结果。

因此,当 useSelector 查看结果时,它会获得引用相等性。因此跳过重新渲染!

这里需要注意的一点是,createSelector仅缓存前一个结果。如果您考虑单个组件,这是有道理的。在单个组件中,您只关心与先前渲染相比的值和结果的差异。但实际上,您可能会在多个组件之间共享选择器。如果发生这种情况,您将拥有一个缓存位置和多个使用此缓存的组件。即东西坏了。

第四级:createSelector工厂功能

由于选择器的逻辑是相同的,因此您需要createSelector为使用它的每个组件运行。这为每个组件创建了一个缓存,为我们提供了所需的行为。为了做到这一点,我们使用工厂函数。

const makeSelectFilteredItems = () => createSelector(
  (state) => state.itemsArray, // the first argument accesses relevant data from global state
  (itemsArray) => itemsArray.filter(checkCondition) // the second parameter conducts the transformation
)

//...
// somewhere inside a react component
const selectFilteredItems = useMemo(makeSelectFilteredItems,[]); // make a new selector for each component, when it mounts
const cart = useSelector(selectFilteredItems) // re-renders only when needed
Run Code Online (Sandbox Code Playgroud)

您打算为每个安装的新组件创建一个新的选择器(以及扩展的缓存)。因此,您将其放在实际的组件函数中,而不是放在模块作用域中。但这将为makeFilteredSelector每个渲染重新运行,因此为每个渲染创建一个新的选择器,从而消除缓存。这就是为什么您需要将函数包装在useMemo带有空依赖项数组的 a 中。它在每个安装座上运行。

瞧!

您现在知道在 Redux 中何处、为何以及如何使用选择器。我个人觉得createSelector语法有点做作。目前正在进行一些关于更改缓存大小的讨论。但现在我觉得坚持阅读文档应该可以帮助你度过大多数情况。

  • 这是一个_太棒了_的答案!请注意,根据该链接,我们确实在 Reselect 4.1.0 的“createSelector”中提供了缓存大小选项。此外,只有当多个组件使用每个组件不同的参数调用同一个选择器实例时,“每个组件实例唯一的选择器实例”位才开始起作用,例如 `useSelector(state =&gt; selectItemsByCategory(state, props.category))`,因为这会阻止它正确记忆。 (2认同)