查看原文
其他

React最佳实践

CUGGZ 前端充电宝 2023-10-06
全文约 4350 字,预计阅读需要 15 分钟。
文来分享 React 中的 16 种常见反模式和最佳实践。
1
在组件外部声明CSSSummer IS HERE

如果使用 CSS in JS 的解决方案,尽量避免在组件内声明 CSS。

import makeCss from 'some/css/in/js/library'

const Component = () => {
// 不要这样写
return <div className={makeCss({ background: red, width: 100% })} />
}

因为在每次渲染时都会重新创建对象,可以将其从组件中提出来:

import cssLibrary from 'some/css/in/js/library'

const someCssClass = makeCss({
background: red,
width: 100%
})

const Component = () => {
return <div className={someCssClass} />
}
2
使用 useCallback 防止函数重新创建Summer IS HERE

每当重新渲染 React 组件时,都会重新创建组件中的所有函数。React 提供了一个 useCallback Hook,可以用来避免这种情况。只要其依赖项不改变,useCallback 就会在渲染之间保留函数的旧实例。

import { useCallback } from 'react'

const Component = () => {
const [value, setValue] = useState(false)

// 该函数将在每次渲染时重新创建
const handleClick = () => {
setValue(true)
}

return <button onClick={handleClick}>Click</button>
}
import { useCallback } from 'react'

const Component = () => {
const [value, setValue] = useState(false)

// 仅当变量值更新时才会重新创建此函数
const handleClick = useCallback(() => {
setValue(true)
}, [value])

return <button onClick={handleClick}>Click</button>
}

对于示例中的小函数,不能保证将函数包装在useCallback中确实更好。所以还需要根据实际情况来判断是否需要使用 useCallback 包装。

在底层,React将在每次渲染时检查依赖关系,以确定是否需要创建新函数,而且有时依赖关系经常发生变化。因此,useCallback提供的优化并不总是必需的。然而,如果函数的依赖项不经常更新,那么使用useCallback是一种很好的优化方法,以避免在每次渲染时重新创建函数。

3
使用 useCallback 防止依赖项更改Summer IS HERE

useCallback 不仅可以用于避免函数实例化,但它也可用于更重要的事情。由于 useCallback 在渲染之间为包装函数保留相同的内存引用,因此它可用于优化其他 useCallback 和记忆的使用。

import { memo, useCallback, useMemo } from 'react'

const MemoizedChildComponent = memo({ onTriggerFn }) => {
// ...
})

const Component = ({ someProp }) => {
// 仅当 someProp 发生变化时,对 onTrigger 函数的引用才会发生变化
const onTrigger = useCallback(() => {
// ...
}, [someProp])

// 这个记忆值只会在 onTrigger 函数更新时更新
// 如果 onTrigger 不是 useCallback 中的包装器,则将在每次渲染时重新计算该值
const memoizedValue = useMemo(() => {
// ...
}, [onTrigger])

// Memoize子组件只会在onTrigger函数更新时重新渲染
// 如果 onTrigger 未包装在 useCallback 中,MemoizedChildComponent 将在每次渲染此组件时重新渲染
return (<>
<MemoizedChildComponent onTriggerFn={onTrigger} />
<button onClick={onTrigger}>Click me</button>
</>)
}
4
使用 useCallback 防止 useEffect 触发Summer IS HERE

前面的示例展示了如何借助 useCallback 来优化渲染,同样,也可以避免不必要的 useEffect 触发。

import { useCallback, useEffect } from 'react'

const Component = ({ someProp }) => {
// 仅当 someProp 发生变化时,对 onTrigger 函数的引用才会发生变化
const onTrigger = useCallback(() => {
// ...
}, [someProp])

// useEffect 仅在 onTrigger 函数更新时运行
// 如果 onTrigger 未包装在 useCallback 中,则 useEffect 将在每次此函数渲染时运行
useEffect(() => {
// ...
}, [onTrigger])

return <button onClick={onTrigger}>Click me</button>
}
5
当不需要依赖项时,向 useEffect 添加空依赖项Summer IS HERE

如果 effect 不依赖于任何变量,可以将空依赖项数组作为 useEffect 的第二个参数。否则,effect 将在每次渲染时运行。

import { useEffect } from 'react'

const Component = () => {
useEffect(() => {
// ...
}, [])

return <div>Example</div>
}

这个逻辑也适用于其他 React hook,例如 useCallbackuseMemo。不过,如果没有任何依赖项,可能根本不需要使用这些 Hooks。

6
始终将所有依赖项添加到 useEffect 和其他 React HooksSummer IS HERE

在处理内置 React Hooks 的依赖项项(例如 useEffectsuseCallback)时,请将所有依赖项添加到依赖项列表(Hooks 的第二个参数)。当省略依赖项时,effect 或回调可能会使用它的旧值,这通常会导致难以预测的错误。

import { useEffect } from 'react'

const Component = () => {
const [value, setValue] = useState()

useEffect(() => {
// 使用 value 变量的代码

// 将变量添加到依赖项数组中,应在此处添加 value 变量
}, [])

return <div>{value}</div>
}

那当 useEffect 被触发的次数比希望的次数多时,如何避免副作用?不幸的是,没有完美的解决方案。不同的场景需要不同的解决方案。可以尝试使用 hook 仅运行一次代码,这有时很有用,但实际上并不是一个值得推荐的解决方案。

大多数情况下,可以使用 if-case 来解决问题。可以查看当前状态并从逻辑上决定是否确实需要运行代码。例如,如果不将 value 值添加为上述 effect 的依赖项的原因是仅在值未定义时运行代码,则只需在 effect 内添加 if 语句即可。

import { useEffect } from 'react'

const Component = () => {
const [value, setValue] = useState()

useEffect(() => {
if (!value) {
// ...
}
}, [value])

return <div>{value}</div>

其他场景可能更复杂,也许使用 if 语句来防止 effect 多次发生不太可行。在这种情况下,首先应该确定,真的需要 effect 吗?在很多情况下,开发人员在实际上不应该这样做时却使用了 effect。

7
不要将外部函数包装在 useCallback 中Summer IS HERE

不需要 useCallback 来调用外部函数。只需按原样调用外部函数即可。这使得 React 不必检查 useCallback 是否需要重新创建,并且使代码更简洁。

import { useCallback } from 'react'
import externalFunction from '/services/externalFunction'

const Component = () => {
// ❌
const handleClick = useCallback(() => {
externalFunction()
}, [])

return <button onClick={handleClick}>Click me</button>
}
import externalFunction from '/services/externalFunction'

const Component = () => {
// ✅
return <button onClick={externalFunction}>Click me</button>
}

使用 useCallback 的一个用例是回调调用多个函数或读取或更新内部状态(例如 useState  hook 中的值或组件传入的 props 之一)时。

import { useCallback } from 'react'
import { externalFunction, anotherExternalFunction } from '/services'

const Component = ({ passedInProp }) => {
const [value, setValue] = useState()

const handleClick = useCallback(() => {
// 调用了多个函数
externalFunction()
anotherExternalFunction()

// 读取和或设置内部值或属性。
setValue(passedInProp)
}, [passedInProp, value])

return <button onClick={handleClick}>Click me</button>
}
8
不要将 useMemo 与空依赖数组一起使用Summer IS HERE

如果添加了带有空依赖项数组的 useMemo,问问自己为什么要这样做。因为它依赖于组件的状态变量而不想添加它?在这种情况下,应该列出所有依赖变量!因为 useMemo 没有任何依赖项?那就不需要使用 useMemo 了。

import { useMemo } from 'react'

const Component = () => {
// ❌
const memoizedValue = useMemo(() => {
return 3 + 5
}, [])

return <div>{memoizedValue}</div>
}
// ✅
const memoizedValue = 3 + 5

const Component = () => {
return <div>{memoizedValue}</div>
}
9
不要在其他组件中声明组件Summer IS HERE
const Component = () => {

// ❌
const ChildComponent = () => {
return <div>child</div>
}

return <div><ChildComponent /></div>
}

这样写的话,组件内声明的变量将在每次组件呈现时重新声明。在这种情况下,这意味着每次父级重新渲染时都必须重新创建功能子组件。就必须在每次渲染时实例化一个函数。React 将无法决定何时进行任何类型的组件优化。如果在 ChildComponent 中使用 hooks,它们将在每次渲染时重新启动。

那该怎么办呢?只需在父组件之外声明子组件即可。

const ChildComponent = () => {
return <div>child</div>
}

const Component = () => {
return <div><ChildComponent /></div>
}

或者,更好的方式是:

import ChildComponent from 'components/ChildComponent'

const Component = () => {
return <div><ChildComponent /></div>
}
10
不要在 If 语句中使用 HookSummer IS HERE

在React内部,Hook的调用顺序是必须固定的,以确保正确地管理组件状态和生命周期。如果在if语句内部使用Hook,会导致两个问题:

  1. 违反Hook的调用规则:根据React的规定,Hook应该在每次渲染中按照相同的顺序被调用。当条件发生变化时,Hook调用的顺序可能会发生变化,这会破坏React对Hook调用顺序的依赖,导致无法预料的行为和错误。

  2. Hook的依赖关系无效:Hook的工作原理是基于依赖项列表,它可以检测依赖项的变化,并在需要时重新运行。如果将Hook放在if语句中,它的依赖关系可能无法正确地捕捉到变化,从而导致状态更新或副作用的错误。

import { useState } from 'react'

const Component = ({ propValue }) => {
if (!propValue) {
const [value, setValue] = useState(propValue)
}

return <div>{value}</div>
}
11
使用 useState 而不是变量Summer IS HERE

在React中,存储状态应该始终使用 React hooks(如useStateuseReducer),不要直接将状态声明为组件中的变量。这样做会导致在每次渲染时重新声明变量,这意味着React无法像通常一样对其进行记忆化处理。

import AnotherComponent from 'components/AnotherComponent'

const Component = () => {
const value = { someKey: 'someValue' }

return <AnotherComponent value={value} />
}

在上述情况下,依赖于valueAnotherComponent及其相关内容将在每次渲染时重新渲染,即使它们使用memouseMemouseCallback进行了记忆化处理。

如果将一个带有value作为依赖的useEffect添加到组件中,它将在每次渲染时触发。因为每次渲染时 value 的JavaScript引用都会不同。

通过使用 React 的useState,React会保留value的相同引用,直到使用setValue进行更新。然后,React 将能够检测何时触发和何时不触发 effect ,并重新计算记忆化处理。

import { useState } from 'react'
import AnotherComponent from 'components/AnotherComponent'

const Component = () => {
const [value, setValue] = useState({ someKey: 'someValue' })

return <AnotherComponent value={value} />
}

如果只需要一个状态,在初始化后就不再更新,那么可以将变量声明在组件外部。这样 JavaScript 引用将不会改变。

// 如果不需要更新该值,就可以这样声明变量
const value = { someKey: 'someValue' }

const Component = () => {
return <AnotherComponent value={value} />
}
12
return 后不使用 HookSummer IS HERE

根据定义,if语句是有条件执行的,“return”关键字也会导致条件 Hook 渲染。

import { useState } from 'react'

const Component = ({ propValue }) => {

if (!propValue) {
return null
}

// 这个 hook 是有条件的,因为只有当 propValue 存在时才会调用它
const [value, setValue] = useState(propValue)

return <div>{value}</div>
}

条件语句中的 return 语句会使后续的 Hook 成为有条件的。为了避免这种情况,将所有的 Hook 放在组件的第一个条件渲染之前。也就是说,始终将 Hook 放在组件的顶部。

import { useState } from 'react'

const Component = ({ propValue }) => {
// 在条件渲染之前放 hooks
const [value, setValue] = useState(propValue)

if (!propValue) {
return null
}

return <div>{value}</div>
}
13
让子组件决定是否应该渲染Summer IS HERE

在许多情况下应该让子组件决定是否应该渲染:

import { useState } from 'react'

const ChildComponent = ({ shouldRender }) => {
return <div>Rendered: {shouldRender}</div>
}

const Component = () => {
const [shouldRender, setShouldRender] = useState(false)

return <>
{ !!shouldRender && <ChildComponent shouldRender={shouldRender} /> }
</>
}

以上是有条件地渲染子组件的常见方法。代码很好,除了在有很多子组件时有点冗长之外。但根据 ChildComponent 的作用,可能存在更好的解决方案。下面来稍微重写一下代码。

import { useState } from 'react'

const ChildComponent = ({ shouldRender }) => {

if (!shouldRender) {
return null
}

return <div>Rendered: {shouldRender}</div>
}

const Component = () => {
const [shouldRender, setShouldRender] = useState(false)

return <ChildComponent shouldRender={shouldRender} />
}

这里重写了两个组件,将条件渲染移至子组件中。那条件渲染移至子组件有什么好处?

最大的好处是 React 可以继续渲染 ChildComponent,即使它不可见。这意味着,ChildComponent 可以在隐藏时保持其状态,然后第二次渲染而不会丢失其状态。它一直都在那里,只是不可见。

如果组件像第一个代码那样停止渲染,则 useState 中保存的状态将被重置,并且一旦组件再次渲染,useEffectuseCallbackuseMemo 都需要重新运行并重新计算新值。

如果代码会触发一些网络请求或进行一些复杂的计算,那么当组件再次渲染时,这些请求也会运行。同样,如果将一些表单数据存储在组件的内部状态中,则每次组件隐藏时都会重置。

14
使用 useReducer 而不是多个 useStateSummer IS HERE

可以使用一个 useReducer 来代替使用多个 useState,这样写起来可能比较麻烦,但是这样既可以避免不必要的渲染,又可以让逻辑更容易理解。一旦有了 useReducer,向组件添加新逻辑和状态就会容易得多。

import { useState } from 'react'

const Component = () => {
// ❌
const [text, setText] = useState(false)
const [error, setError] = useState('')
const [touched, setTouched] = useState(false)

const handleChange = (event) => {
const value = event.target.value
setText(value)

if (value.length < 6) {
setError('Too short')
} else {
setError('')
}
}

return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
import { useReducers } from 'react'

const UPDATE_TEXT_ACTION = 'UPDATE_TEXT_ACTION'
const RESET_FORM = 'RESET_FORM'

const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})

const formReducer = (state, action) => {
const { data, type } = action || {}

switch (type) {
case UPDATE_TEXT_ACTION:
const text = data?.text ?? ''

return {
...state,
text: text,
error: text.length < 6,
touched: true
}
case RESET_FORM:
return getInitialFormState()
default:
return state
}
}

const Component = () => {
// ✅
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
const { text, error, touched } = state

const handleChange = (event) => {
const value = event.target.value
dispatch({ type: UPDATE_TEXT_ACTION, text: value})
}

return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
15
将初始状态写为函数而不是对象Summer IS HERE

来看看下面的 getInitialFormState 函数:

// 初始状态是一个函数
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})

const formReducer = (state, action) => {
// ...
}

const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
// ...
}

这里将将初始状态写成了一个函数,但其实直接使用一个对象也是可以的。

// 初始状态是一个对象
const initialFormState = {
text: '',
error: '',
touched: false
}

const formReducer = (state, action) => {
// ...
}

const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
// ...
}

那为什么不直接写成对象呢?答案很简单,避免可变性。在上面的例子中,当initialFormState是一个对象时,我们可能会一不小心就在代码中的某个地方改变了该对象。如果这样,当再次使用该变量,例如在重置表单时,将无法恢复初始状态。相反,会得到变异的对象。

因此,将初始状态转换为返回初始状态对象的 getter 函数是一个很好的做法。或者更好的是,使用像 Immer 这样的库,它用于避免编写可变代码。

16
当组件不应重新渲染时,使用 useRef 而不是 useStateSummer IS HERE

可以通过用 useRef 替换 useState 来优化组件渲染。来看下面的例子:

import { useEffect } from 'react'

const Component = () => {
const [triggered, setTriggered] = useState(false)

useEffect(() => {
if (!triggered) {
setTriggered(true)
// ...
}
}, [triggered])
}

当运行上面的代码时,组件将在调用 setTriggered 时重新渲染。在这种情况下,触发状态变量可能是确保 effect 仅运行一次的一种方法。

由于在这种情况下触发变量的唯一用途是跟踪函数是否已被触发,因此不需要组件渲染任何新状态。因此,可以将 useState 替换为 useRef,这样更新时就不会触发组件重新渲染。

import { useRef } from 'react'

const Component = () => {
const triggeredRef = useRef(false)

useEffect(() => {
if (!triggeredRef.current) {
triggeredRef.current = true

// ...
}

}, [])
}

那为什么需要使用 useRef,而不简单地使用组件外部的变量呢?

const triggered = false

const Component = () => {
useEffect(() => {
if (!triggered) {
triggered = true

// ...
}
}, [])
}

这里需要 useRef 的原因是因为上面的代码不能以同样的方式工作!上面的变量只会为 false 一次。如果组件卸载了,当组件再次挂载时,triggered变量仍然会被设置为true,因为triggered变量并没有绑定到React的生命周期中。当使用 useRef 时,React 将在组件卸载并再次安装时重置其值。在这种情况下,就可以要使用 useRef

Summer往期推荐

React 18 如何提高应用性能?

ECMAScript 最新进展汇总!

推荐10个开源的前端低代码项目!

JavaScript日期时间操作完整指南!

前端新人入职必备清单,保姆级教程!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存