本文来分享 React 中的 16 种常见反模式和最佳实践。
1、在组件外部声明CSS
如果使用 CSS in JS 的解决方案,尽量避免在组件内声明 CSS。
因为在每次渲染时都会重新创建对象,可以将其从组件中提出来:
2、使用 useCallback 防止函数重新创建
每当重新渲染 React 组件时,都会重新创建组件中的所有函数。React 提供了一个 useCallback Hook,可以用来避免这种情况。只要其依赖项不改变,useCallback 就会在渲染之间保留函数的旧实例。
对于示例中的小函数,不能保证将函数包装在useCallback中确实更好。所以还需要根据实际情况来判断是否需要使用 useCallback 包装。
在底层,React将在每次渲染时检查依赖关系,以确定是否需要创建新函数,而且有时依赖关系经常发生变化。因此,useCallback提供的优化并不总是必需的。然而,如果函数的依赖项不经常更新,那么使用useCallback是一种很好的优化方法,以避免在每次渲染时重新创建函数。
3、使用 useCallback 防止依赖项更改
useCallback 不仅可以用于避免函数实例化,但它也可用于更重要的事情。由于 useCallback 在渲染之间为包装函数保留相同的内存引用,因此它可用于优化其他 useCallback 和记忆的使用。
4、使用 useCallback 防止 useEffect 触发
前面的示例展示了如何借助 useCallback 来优化渲染,同样,也可以避免不必要的 useEffect 触发。
5、当不需要依赖项时,向 useEffect 添加空依赖项
如果 effect 不依赖于任何变量,可以将空依赖项数组作为 useEffect 的第二个参数。否则,effect 将在每次渲染时运行。
这个逻辑也适用于其他 React hook,例如 useCallback 和 useMemo。不过,如果没有任何依赖项,可能根本不需要使用这些 Hooks。
6、始终将所有依赖项添加到 useEffect 和其他 React Hooks
在处理内置 React Hooks 的依赖项项(例如 useEffects 和 useCallback)时,请将所有依赖项添加到依赖项列表(Hooks 的第二个参数)。当省略依赖项时,effect 或回调可能会使用它的旧值,这通常会导致难以预测的错误。
那当 useEffect 被触发的次数比希望的次数多时,如何避免副作用?不幸的是,没有完美的解决方案。不同的场景需要不同的解决方案。可以尝试使用 hook 仅运行一次代码,这有时很有用,但实际上并不是一个值得推荐的解决方案。
大多数情况下,可以使用 if-case 来解决问题。可以查看当前状态并从逻辑上决定是否确实需要运行代码。例如,如果不将 value 值添加为上述 effect 的依赖项的原因是仅在值未定义时运行代码,则只需在 effect 内添加 if 语句即可。
其他场景可能更复杂,也许使用 if 语句来防止 effect 多次发生不太可行。在这种情况下,首先应该确定,真的需要 effect 吗?在很多情况下,开发人员在实际上不应该这样做时却使用了 effect。
7、不要将外部函数包装在 useCallback 中
不需要 useCallback 来调用外部函数。只需按原样调用外部函数即可。这使得 React 不必检查 useCallback 是否需要重新创建,并且使代码更简洁。
使用 useCallback 的一个用例是回调调用多个函数或读取或更新内部状态(例如 useState hook 中的值或组件传入的 props 之一)时。
8、不要将 useMemo 与空依赖数组一起使用
如果添加了带有空依赖项数组的 useMemo,问问自己为什么要这样做。因为它依赖于组件的状态变量而不想添加它?在这种情况下,应该列出所有依赖变量!因为 useMemo 没有任何依赖项?那就不需要使用 useMemo 了。
9、不要在其他组件中声明组件
这样写的话,组件内声明的变量将在每次组件呈现时重新声明。在这种情况下,这意味着每次父级重新渲染时都必须重新创建功能子组件。就必须在每次渲染时实例化一个函数。React 将无法决定何时进行任何类型的组件优化。如果在 ChildComponent 中使用 hooks,它们将在每次渲染时重新启动。
那该怎么办呢?只需在父组件之外声明子组件即可。
或者,更好的方式是:
10、不要在 If 语句中使用 Hook
在React内部,Hook的调用顺序是必须固定的,以确保正确地管理组件状态和生命周期。如果在if语句内部使用Hook,会导致两个问题:
违反Hook的调用规则:根据React的规定,Hook应该在每次渲染中按照相同的顺序被调用。当条件发生变化时,Hook调用的顺序可能会发生变化,这会破坏React对Hook调用顺序的依赖,导致无法预料的行为和错误。
Hook的依赖关系无效:Hook的工作原理是基于依赖项列表,它可以检测依赖项的变化,并在需要时重新运行。如果将Hook放在if语句中,它的依赖关系可能无法正确地捕捉到变化,从而导致状态更新或副作用的错误。
11、使用 useState 而不是变量
在React中,存储状态应该始终使用 React hooks(如useState或useReducer),不要直接将状态声明为组件中的变量。这样做会导致在每次渲染时重新声明变量,这意味着React无法像通常一样对其进行记忆化处理。
在上述情况下,依赖于value的AnotherComponent及其相关内容将在每次渲染时重新渲染,即使它们使用memo、useMemo或useCallback进行了记忆化处理。
如果将一个带有value作为依赖的useEffect添加到组件中,它将在每次渲染时触发。因为每次渲染时 value 的JavaScript引用都会不同。
通过使用 React 的useState,React会保留value的相同引用,直到使用setValue进行更新。然后,React 将能够检测何时触发和何时不触发 effect ,并重新计算记忆化处理。
如果只需要一个状态,在初始化后就不再更新,那么可以将变量声明在组件外部。这样 JavaScript 引用将不会改变。
12、return 后不使用 Hook
根据定义,if语句是有条件执行的,“return”关键字也会导致条件 Hook 渲染。
条件语句中的 return 语句会使后续的 Hook 成为有条件的。为了避免这种情况,将所有的 Hook 放在组件的第一个条件渲染之前。也就是说,始终将 Hook 放在组件的顶部。
13、让子组件决定是否应该渲染
在许多情况下应该让子组件决定是否应该渲染:
以上是有条件地渲染子组件的常见方法。代码很好,除了在有很多子组件时有点冗长之外。但根据 ChildComponent 的作用,可能存在更好的解决方案。下面来稍微重写一下代码。
这里重写了两个组件,将条件渲染移至子组件中。那条件渲染移至子组件有什么好处?
最大的好处是 React 可以继续渲染 ChildComponent,即使它不可见。这意味着,ChildComponent 可以在隐藏时保持其状态,然后第二次渲染而不会丢失其状态。它一直都在那里,只是不可见。
如果组件像第一个代码那样停止渲染,则 useState 中保存的状态将被重置,并且一旦组件再次渲染,useEffect、useCallback 和 useMemo 都需要重新运行并重新计算新值。
如果代码会触发一些网络请求或进行一些复杂的计算,那么当组件再次渲染时,这些请求也会运行。同样,如果将一些表单数据存储在组件的内部状态中,则每次组件隐藏时都会重置。
14、使用 useReducer 而不是多个 useState
可以使用一个 useReducer 来代替使用多个 useState,这样写起来可能比较麻烦,但是这样既可以避免不必要的渲染,又可以让逻辑更容易理解。一旦有了 useReducer,向组件添加新逻辑和状态就会容易得多。
15、将初始状态写为函数而不是对象
来看看下面的 getInitialFormState 函数:
这里将将初始状态写成了一个函数,但其实直接使用一个对象也是可以的。
那为什么不直接写成对象呢?答案很简单,避免可变性。在上面的例子中,当initialFormState是一个对象时,我们可能会一不小心就在代码中的某个地方改变了该对象。如果这样,当再次使用该变量,例如在重置表单时,将无法恢复初始状态。相反,会得到变异的对象。
因此,将初始状态转换为返回初始状态对象的 getter 函数是一个很好的做法。或者更好的是,使用像 Immer 这样的库,它用于避免编写可变代码。
16、当组件不应重新渲染时,使用 useRef 而不是 useState
可以通过用 useRef 替换 useState 来优化组件渲染。来看下面的例子:
当运行上面的代码时,组件将在调用 setTriggered 时重新渲染。在这种情况下,触发状态变量可能是确保 effect 仅运行一次的一种方法。
由于在这种情况下触发变量的唯一用途是跟踪函数是否已被触发,因此不需要组件渲染任何新状态。因此,可以将 useState 替换为 useRef,这样更新时就不会触发组件重新渲染。
那为什么需要使用 useRef,而不简单地使用组件外部的变量呢?
这里需要 useRef 的原因是因为上面的代码不能以同样的方式工作!上面的变量只会为 false 一次。如果组件卸载了,当组件再次挂载时,triggered变量仍然会被设置为true,因为triggered变量并没有绑定到React的生命周期中。当使用 useRef 时,React 将在组件卸载并再次安装时重置其值。在这种情况下,就可以要使用 useRef。