Disclaimer: We will focus on useSelector
itself in this article, rather than third-party libraries like reselect
, because it's out of scope of the article.
免责声明 :本文将重点关注useSelector
本身,而不是像reselect
这样的第三方库,因为它不在本文讨论范围之内。
以前怎么样? (How was it like before?)
Now, before talking about useSelector
, we need to know some background. Let's go back to pre-function component era, where all we needed to care about were the props
. Not so much you needed to do. Choices were PureComponent
and shouldComponentUpdate
. Nothing else. Like this:
现在,在谈论useSelector
之前,我们需要了解一些背景。 让我们回到功能前组件时代,在这里我们只需要关心props
。 您不需要做太多。 选择是PureComponent
和shouldComponentUpdate
。 没有其他的。 像这样:
import React from 'react'
interface Props {
readonly shouldShowRed: boolean;
}
class SomeDiv extends React.PureComponent<Props, {}> {
public render() {
return (
<div style={{
width: '300px',
height: '300px',
background: this.props.shouldShowRed ? 'red' : 'white'
}}>
hi
</div>
)
}
}
export default SomeDiv;
Right. So if you make such component, this component is only going to update when shouldShowRed
is changed. Otherwise, it is going to stay still even if its parent renders for some reason. It goes the same for shouldComponentUpdate
; It just gives you additional tooling to specify your own method of telling the component when to update.
对。 因此,如果您制作了这样的组件,则该组件仅在shouldShowRed
更改时shouldShowRed
更新。 否则,即使其父级出于某种原因进行渲染,它也将保持静止。 shouldComponentUpdate
; 它只是为您提供了额外的工具来指定您自己的告诉组件何时更新的方法。
与Redux一起使用吗? (Using it with Redux?)
Again, using the component with redux was pretty straightforward too. Just create a container and pass states and dispatches (I didn’t write any mapDispatchToProps
for the sake of simplicity) mapped as props:
同样,将组件与redux一起使用也非常简单。 只需创建一个容器并传递映射为道具的状态和分派(为简单起见,我没有编写任何mapDispatchToProps
):
// Presentational component (same as the above component)
import React from 'react'
interface Props {
readonly shouldShowRed: boolean;
}
class SomeDiv extends React.PureComponent<Props, {}> {
public render() {
return (
<div style={{
width: '300px',
height: '300px',
background: this.props.shouldShowRed ? 'red' : 'white'
}}>
hi
</div>
)
}
}
export default SomeDiv;
// container component
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import SomeDiv from './SomeDiv';
// Let's say that we have this redux root state
interface RootReduxState {
readonly conditions: {
readonly shouldShowRed: boolean;
readonly shouldShowGreen: boolean;
readonly shouldShowBlue: boolean;
}
}
export const mapStateToProps: (
state: RootReduxState;
) => { readonly shouldShowRed: RootReduxState['conditions']['shouldShowRed'] } = (
state,
) => ({
shouldShowRed: state.shouldShowRed,
});
export default connect(mapStateToProps, null)(SomeDiv);
Right. Let’s assume that we have RootReduxState
as we have seen from the code above. And this is just going to work so well. Nothing difficult here. SomeDiv
will keep being efficient at its best because PureComponent
is working well. But how should we precisely port this example to a function component?
对。 假设我们具有上面的代码中看到的RootReduxState
。 这将很好地工作。 这里没什么难的。 因为PureComponent
运行良好,所以SomeDiv
将保持最佳PureComponent
。 但是我们应该如何将这个示例精确地移植到功能组件上呢?
功能组成 (Function component)
Well, it’s easy. Just use useSelector
, Right?
好吧,这很容易。 只需使用useSelector
,对吗?
import React from 'react'
const SomeDiv: FC = () => {
const shouldShowRed: boolean = useSelector((s: RootReduxState) => s.conditions.shouldShowRed);
return (
<div style={{
width: '300px',
height: '300px',
background: shouldShowRed ? 'red' : 'white'
}}></div>
)
}
export default SomeDiv;
Perfect. But what if you need more than just shouldShowRed
? The moment you start to get more than one thing from useSelector (which is a very normal case), you are going to need additional optimization efforts with appropriate knowledge.
完善。 但是,如果您不仅需要shouldShowRed
怎么shouldShowRed
? 当您开始从useSelector获得不止一项功能时(这是非常正常的情况),您将需要具有适当知识的其他优化工作。
If we were to use shouldShowGreen
too, we could choose:
如果我们也使用shouldShowGreen
,我们可以选择:
Option 1
选项1
const {
shouldShowRed,
shouldShowGreen,
}: RootReduxState['conditions'] = useSelector(({ conditions: { shouldShowRed, shouldShowGreen }}: RootReduxState) => ({
shouldShowRed, shouldShowGreen
}));
Or, we could:
或者,我们可以:
Option 2
选项2
const {
conditions: {
shouldShowRed,
shouldShowGreen,
}
}: RootReduxState = useSelector((s: RootReduxState) => s;
Alternatively:
或者:
Option 3
选项3
const shouldShowRed: boolean = useSelector((s: RootReduxState) => s.conditions.shouldShowRed)
const shouldShowGreen: boolean = useSelector((s: RootReduxState) => s.conditions.shouldShowGreen)
Or, just a small variant..
或者,只是一个小的变体。
Option 4
选项4
const { shouldShowRed, shouldShowGreen }: RootReduxState['conditions'] = useSelector((s: RootReduxState) => s.conditions)
useSelector强制重新渲染 (useSelector forces re-renders)
Which method have you been using? Whichever one you are using, you should be able to give reasons! Now, before having a look at each option, let’s go back to useSelector
and see what it does.
您一直在使用哪种方法? 无论您使用的是哪种,您都应该能够给出理由! 现在,在查看每个选项之前,让我们回到useSelector
并查看其作用。
So let me bring the most important thing to the table immediately: useSelector
forces re-render of your component. It's not a prop, but it can still make your component re-render. Really? Yes. Let's look at the offical documentation:
因此,让我立即将最重要的内容带到表中: useSelector
强制重新渲染组件。 它不是一个道具,但仍然可以使您的组件重新渲染。 真? 是。 让我们看一下官方文档 :
When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
调度动作时,useSelector()将对前一个选择器结果值和当前结果值进行参考比较。 如果它们不同,则将强制重新渲染组件 。 如果它们相同,则组件将不会重新渲染。
So how do you judge if they are different? By running strict equality (===
) comparision. ( Click here if you are feeling like looking at the official repo's source code). In the source code, useSelector
by default uses a function called refEquality
, and all it does is simply const refEquality = (a, b) => a === b
.
那么,您如何判断它们是否不同? 通过运行严格相等( ===
)比较。 ( 如果您想查看官方仓库的源代码,请单击此处 )。 在源代码中,默认情况下, useSelector
使用一个称为refEquality
的函数,而它所做的只是const refEquality = (a, b) => a === b
。
This means that if you are returning an object from useSelector
for whatever reason, useSelector
will cause a re-render every time an action is dispatched from the store, simply because objects of the same structure (same keys and values) do not strictly equal each other in javascript. For example, { a: 1 } === { a: 1 }
is false
. Official doc says the same:
这意味着如果出于任何原因从useSelector
返回对象,则每次从商店调度动作时, useSelector
都会导致重新渲染,这仅仅是因为结构相同的对象(相同的键和值)并不严格相等其他在javascript中。 例如, { a: 1 } === { a: 1 }
为false
。 官方文件也这么说:
returning a new object every time will always force a re-render by default.
默认情况下, 每次返回一个新对象将始终 强制重新渲染 。
Do they really force it? Yes. From the source code:
他们真的强迫吗? 是。 从源代码 :
const [, forceRender] = useReducer(s => s + 1, 0)
...
forceRender()
So… now, with this in our mind, let’s go back to the options we saw previously.
所以……现在,有了这一点,让我们回到以前看到的选项。
选项1 (Option 1)
const {
shouldShowRed,
shouldShowGreen,
}: RootReduxState['conditions'] = useSelector(({ conditions: { shouldShowRed, shouldShowGreen }}: RootReduxState) => ({
shouldShowRed, shouldShowGreen
}));
Catches:
数量:
You are just writing a pair of
shouldShowRed
andshouldShowGreen
for three times just to take the desired state out.您只写了
shouldShowRed
和shouldShowGreen
对,只是为了取出所需的状态。- This will cause a re-render forcefully every time an action is dispatched, because you are returning a new object from your selector. 每次分派操作时,这都会导致强行重新渲染,因为您要从选择器返回一个新对象。
Verdict: not good enough.
判决 :不够好。
选项2 (Option 2)
const {
conditions: {
shouldShowRed,
shouldShowGreen,
}
}: RootReduxState = useSelector((s: RootReduxState) => s);
Catches: you are returning the entire state from your selector, and destructuring it outside of the selector. This will too cause a re-render. What’s the point of having the selector callback if you intend to receive the entire state? This is a bad practice. It’s just tantamount to passing the entire state in mapStateToProps
. You don't do that there. So, why here?
捕获 :您正在从选择器返回整个状态,并在选择器之外对其进行破坏。 这也将导致重新渲染。 如果打算接收整个状态,则使用选择器回调有什么意义? 这是一个坏习惯。 这仅等于在mapStateToProps
传递整个状态。 你不在那里做。 那么,为什么在这里?
Verdict: not good enough.
判决 :不够好。
选项3 (Option 3)
const shouldShowRed: boolean = useSelector((s: RootReduxState) => s.conditions.shouldShowRed)
const shouldShowGreen: boolean = useSelector((s: RootReduxState) => s.conditions.shouldShowGreen)
Catches:
数量:
you are calling
useSelector
twice, and this does not matter. According to the official doc:您两次调用
useSelector
,这无关紧要。 根据官方文件:
Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple useSelector()s in the same component to return new values should only result in a single re-render.
由于React Redux v7中使用了React更新批处理行为,因此导致同一组件中的多个useSelector()返回新值的分派操作 只能导致一次重新渲染。
2. strict equality is functioning as properly for each selected state because you are returning a primitive from each selector.
2.严格相等对于每个选定状态都可以正常运行,因为您要从每个选择器返回一个原语。
Verdict: usable.
判决 :可用。
选项4 (Option 4)
const { shouldShowRed, shouldShowGreen }: RootReduxState['conditions'] = useSelector((s: RootReduxState) => s.conditions)
Catches: It’s just the same as option 1 or 2. It will cause a re-render too.
斑点 :与选项1或2相同。它也会引起重新渲染。
Verdict: Not good enough.
判决 :不够好。
改善不良选择 (Improving the bad options)
Thankfully, redux gives us a chance to insert our own equality functions. The concept is the same shouldComponentUpdate
or the equality callback in React.memo
. We could do this:
幸运的是,redux使我们有机会插入我们自己的相等函数。 这个概念与shouldComponentUpdate
或React.memo
中的React.memo
回调React.memo
。 我们可以这样做:
const { shouldShowRed, shouldShowGreen }: RootReduxState['conditions'] =
useSelector((s: RootReduxState) => s.conditions, (prev, next) => prev.shouldShowRed === next.shouldShowRed && prev.shouldShowGreen === next.shouldShowGreen)
or,
要么,
import deepEqual from 'react-fast-compare'
const { shouldShowRed, shouldShowGreen }: RootReduxState['conditions'] = useSelector((s: RootReduxState) => s.conditions, deepEqual)
Something like that. However you should really note that using deepEqual
cannot ever be fast if you are trying to equal a large object (Dan Abramov said it, too!):
这样的事情。 但是,您应该真正注意到,如果要尝试等于一个大对象,则使用deepEqual
永远不会很快( Dan Abramov也说过! ):
Note also that you only want to run deepEqual
on what you need. In the code snippet above, you are also comparing shouldShowBlue
which is a part of RootReduxState['conditions']
. But you don't need that anyways, but you are still comparing it. Make sure you select and compare what you only need.
还要注意,您只想对所需的内容运行deepEqual
。 在上面的代码片段中,您还比较了shouldShowBlue
是RootReduxState['conditions']
一部分的RootReduxState['conditions']
。 但是无论如何您都不需要这样做,但是您仍在进行比较。 确保选择并比较您只需要的内容。
但是,为什么首先要尝试对其进行优化? (But why would you try to optimize it in the first place?)
Well, at first, it’s totally okay. Your app has ~100 components only, your redux state is quite shallow, and it does not take a long time to render whatever’s being rendered.
好吧,起初,这完全没问题。 您的应用仅包含约100个组件,您的redux状态非常浅,并且无需花费很长时间即可呈现任何正在呈现的内容。
The problem comes at two points:
问题来自两点:
once you start to scale your application. If you succeed in making a popular application, you are going to support more features, and thus, need more components. Your components will be numerous, leading to the point where re-render of expensive components will be causing a sluggish interaction.
一旦开始扩展应用程序。 如果成功制作出流行的应用程序,则将支持更多功能,因此需要更多组件。 您的组件将很多,导致重新渲染昂贵的组件将导致缓慢的交互。
once you start to care about users using low-end devices. You want to support users with low-end spec computers. You want to support mobile devices with inherently less performance than most computers. Just get a 6x slowdown on your CPU from Chrome’s performance tool and try to see how long it takes for your components to react.
一旦您开始关心使用低端设备的用户。 您想使用低端规格的计算机来支持用户。 您想要支持性能比大多数计算机差的移动设备。 只需通过Chrome的性能工具将CPU的速度降低6倍,然后尝试查看组件React所需的时间即可。
结论 (Conclusion)
So far we looked at possible problems with using useSelector
and how to solve them:
到目前为止,我们研究了使用useSelector
可能出现的问题以及如何解决它们:
- If you are returning an object from your selector callback, it’s going to force re-render by default. 如果要从选择器回调中返回一个对象,则默认情况下它将强制重新渲染。
- To prevent re-render, either let your selector return a primitive type, or use a custom equality function. 为防止重新渲染,请让选择器返回原始类型,或使用自定义的相等函数。
That’s it. Thank you!
而已。 谢谢!
翻译自: https://medium.com/@9oelm/how-to-make-useselector-not-a-disaster-6fdd1c558fd3