useSelector 解构与多次调用

Posted

技术标签:

【中文标题】useSelector 解构与多次调用【英文标题】:useSelector destructuring vs multiple calls 【发布时间】:2020-03-23 03:03:48 【问题描述】:

最近我在阅读 react-redux 文档https://react-redux.js.org/next/api/hooks 还有一个与平等比较和更新相关的部分,其中说:

多次调用useSelector(),每次调用返回一个字段值。

第一种方法:

const  open, importId, importProgress  = useSelector((importApp) => importApp.productsImport);

第二种方法:

const open = useSelector((importApp) => importApp.productsImport.open);
const importId = useSelector((importApp) => importApp.productsImport.importId );
const importProgress = useSelector((importApp) => importApp.productsImport.importProgress);

那么有什么真正的区别吗?或者由于解构 useSelector 钩子会在检查引用时遇到麻烦?

【问题讨论】:

【参考方案1】:

从性能的角度来看,第一种方法更好。 在引擎盖下,破坏代码被转译为

var temp = useSelector((importApp) => importApp.productsImport);
var open = temp.open;
var importId = temp.importId;
var importProgress = temp.importProgress;

在第二种方法中,商店被访问了 3 次,操作成本更高。

【讨论】:

【参考方案2】:

当一个动作被分派到 Redux 存储时,useSelector() 只进行严格的“===”引用比较。这不会进行浅层检查。

因此,如果您想从存储中检索多个值,则必须多次调用 useSelector(),每次调用都会从 state 中返回单个字段值。或者在react-redux中使用shallowEqual

import  shallowEqual, useSelector  from 'react-redux'

const data = useSelector(state => state.something, shallowEqual)

详细解释请参考https://react-redux.js.org/next/api/hooks#equality-comparisons-and-updates。

【讨论】:

这是正确的。这是一个沙箱来说明这一点:codesandbox.io/s/… @sallf 感谢您创建示例。非常有用。【参考方案3】:

只是为了打好基础:当一个动作被调度时,你传递给useSelector()的选择器将被调用。如果它返回的值与上次调度动作时返回的值不同,组件将重新渲染。

破坏确实是错误的方法,但这里的最佳答案完全无关紧要。文档指的是选择器每次都创建一个新对象的场景,就像您在 mapStateToProps() 函数中所做的那样。这将导致组件在每次调度动作时重新渲染,无论该动作做什么,因为选择器返回的值在技术上是内存中的不同对象,即使实际数据没有改变。 在那种情况下,您需要担心严格相等和浅层相等比较。但是,您的选择器不会每次都创建一个新对象。如果调度的操作没有修改importApp.productsImport,它将是内存中与以前完全相同的对象,呈现所有这些没有意义。

相反,这里的问题是您选择了整个状态切片,而您实际上只关心该切片的一些特定属性。考虑importApp.productsImport 可能除了openimportIdimportProgress 之外还有其他属性。如果这些其他属性发生变化,那么您的组件将不必要地重新渲染,即使它没有引用它们。原因很简单:选择器返回importApp.productsImport,并且该对象发生了变化。 Redux 无法知道openimportIdimportProgress 是您真正关心的唯一属性,因为您没有选择这些属性;您选择了整个对象

解决方案

因此,要选择多个属性而不进行不必要的重新渲染,您有两种选择:

使用多个 useSelector() 挂钩,每个挂钩选择您商店中的一个属性。 拥有一个 useSelector() 挂钩和一个选择器,可将商店中的多个属性组合到一个对象中。你可以这样做: 使用来自reselect 的记忆选择器。 只需编写一个函数,从state 的特定属性创建一个新对象并返回它。如果您这样做了,那么您将不得不担心严格相等和浅相等比较。

为此,我觉得多个useSelector() 钩子实际上是要走的路。文档特别提到了这一点

对 useSelector() 的每次调用都会创建一个对 Redux 存储的单独订阅。

但我认为,与纯粹出于这个原因的单个调用相比,多个调用是否真的会导致真正的性能损失,还有待观察,在我看来,担心这可能是过度优化,除非你有拥有数百或数千个订阅的大型应用程序。如果您使用单个 useSelector() 钩子,那么此时您基本上只是在编写一个 mapStateToProps 函数,我觉得它破坏了使用钩子开始的很多魅力,尤其是如果您重新编写 TypeScript。如果你然后想要对结果进行解构,那就更麻烦了。我还认为使用多个钩子肯定更符合 Hooks API 的一般精神。

【讨论】:

非常感谢您的详尽回答。你是一个救生员。【参考方案4】:

如何选择多个状态 ::

const [village, timeZone, date] = useSelector((state) => [
    state.active_village,
    state.time_zone,
    state.date,
  ]);

【讨论】:

它仍然会重新渲染组件【参考方案5】:

这个答案没有很多技术细节。请浏览其他答案以更好地理解。在这里,我只提到用例场景。希望对您有所帮助:

假设:productsImport只有你提到的字段(open、importId、importProgress

1. 假设您将使用 useSelector 获取单个组件中的所有字段,那么我会说您应该使用 First Approach。因为使用 Second Approach 将添加几行额外的代码而没有其他好处。无论如何,只要有任何字段的值更新,您的组件就会重新渲染。

2. 现在,假设您在一个组件(Component1)中使用和更新 importId,而在另一个组件(Component2)中使用和更新其余字段。而且,这些组件也应该在同一个窗口/屏幕上呈现。那么你应该使用第二种方法

Component1:
    const importId = useSelector((importApp) => importApp.productsImport.importId );

Component2:
 const open = useSelector((importApp) => importApp.productsImport.open);
 const importProgress = useSelector((importApp) => importApp.productsImport.importProgress);
 
if you want to use only one selector:
const [open, importProgress] = useSelector((importApp) =>[importApp.productsImport.open, importApp.productsImport.importProgress], 
shallowEqual);
'Note: Here do not forget to use shallowEqual. Otherwise, your component will rerender on every useSelector() call.'

如果您将在此处使用 First Approach,那么您的两个组件都会在您每次更新任何字段时重新渲染。

【讨论】:

以上是关于useSelector 解构与多次调用的主要内容,如果未能解决你的问题,请参考以下文章

是否可以为 useSelector 创建一个可重用的函数?

ES6/Next: 对象解构与休息 - 分组

在我的权限上下文提供程序的函数错误中调用了 React 钩子“useSelector”

我应该使用 useselector/useDispatch 而不是 mapStateToProps

使用 React.memo 与连接的 useSelector

将 react-redux useSelector 与打字稿一起使用