React 反模式和最佳实践 - 注意事项

2025-05-26

React 反模式和最佳实践 - 注意事项

React 似乎是狂野西部网络中最不固执己见的框架之一。尽管如此,它仍然有很多错误,而且为了编写干净易读的代码,你还可以做更多的事情。本文将解释 React 中 17 种常见的反模式和最佳实践。

在本文中

  1. 使用 useState 代替变量
  2. 在组件外部声明 CSS
  3. 使用 useCallback 防止函数重建
  4. 使用 useCallback 防止依赖项更改
  5. 使用 useCallback 来防止 useEffect 触发
  6. 当不需要依赖项时,向 useEffect 添加一个空的依赖项列表
  7. 始终将所有依赖项添加到 useEffects 和其他 React Hooks
  8. 不要使用 useEffect 来启动外部代码
  9. 不要将外部函数包装在 useCallback 中
  10. 不要在依赖列表为空的情况下使用 useMemo
  11. 不要在其他组件中声明组件
  12. 不要在 If 语句中使用钩子(无条件钩子)
  13. 返回后不要使用钩子(无条件钩子)
  14. 让子组件决定是否渲染
  15. 使用 useReducer 代替多个 useState
  16. 将初始状态写为函数而不是对象
  17. 当组件不需要重新渲染时,使用 useRef 而不是 useState

使用 useState 代替变量

第一个应该是基础的,但我仍然看到一些开发者这样做,有时甚至是资深开发者。要在 React 中存储状态,你应该始终使用 React hooks,例如 useState 或 useReducer。切勿在组件中将状态直接声明为变量。这样做会在每次渲染时重新声明变量,这意味着 React 无法记住它通常会记住的内容。

import AnotherComponent from 'components/AnotherComponent'

const Component = () => {
  // Don't do this.
  const value = { someKey: 'someValue' }

  return <AnotherComponent value={value} />
}
Enter fullscreen mode Exit fullscreen mode

在上述情况下,AnotherComponent 和所有依赖于值的内容将在每次渲染时重新渲染,即使它们使用 memo、useMemo 或 useCallback 进行记忆。

如果你将 useEffect 添加到组件中,并将value作为依赖项,它将在每次渲染时触发。这是因为value的 JavaScript 引用在每次渲染时都会不同。

通过使用 React 的 useState,React 将始终保持相同的值引用,直到你使用setValue更新它。这样,React 将能够检测何时触发效果以及何时不触发效果,并重新计算记忆。

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

const Component = () => {
  // Do this instead.
  const [value, setValue] = useState({ someKey: 'someValue' })

  return <AnotherComponent value={value} />
}
Enter fullscreen mode Exit fullscreen mode

如果您只需要一个仅初始化一次且永不更新的状态,那么请在组件外部声明该变量。这样做时,JavaScript 引用将永远不会改变。

// Do this if you never need to update the value.
const value = { someKey: 'someValue' }

const Component = () => {
  return <AnotherComponent value={value} />
}
Enter fullscreen mode Exit fullscreen mode

在组件外部声明 CSS

如果您使用 JS 解决方案中的 CSS,请避免在组件内声明 CSS。

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

const Component = () => {
  // Don't do this.
  return <div className={makeCss({ background: red, width: 100% })} />
}
Enter fullscreen mode Exit fullscreen mode

不这样做的原因是每次渲染时都必须重新创建该对象。相反,应该将其从组件中移除。

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

// Do this instead.
const someCssClass = makeCss({
  background: red,
  width: 100%
})

const Component = () => {
  return <div className={someCssClass} />
}
Enter fullscreen mode Exit fullscreen mode

使用 useCallback 防止函数重建

每当一个 React 函数式组件重新渲染时,它都会重新创建组件中所有常规函数。React 提供了一个 useCallback hook 来避免这种情况。只要依赖项没有发生变化,useCallback 就会在渲染之间保留该函数的旧实例。

import { useCallback } from 'react'

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

  // This function will be recreated on each render.
  const handleClick = () => {
    setValue(true)
  }

  return <button onClick={handleClick}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode
import { useCallback } from 'react'

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

  // This function will only be recreated when the variable value updates.
  const handleClick = useCallback(() => {
    setValue(true)
  }, [value])

  return <button onClick={handleClick}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

这次,我不会再说该怎么做了。有些人会建议你用 useCallback hook 来优化每个函数,但我不会。对于像示例中这样的小函数,我无法保证用 useCallback 包装函数真的更好。

在底层,React 必须在每次渲染时检查依赖项,以确定是否需要创建新函数,而且有时依赖项会频繁更改。因此,useCallback 提供的优化可能并非总是必要的。

但是,如果对函数的依赖关系没有进行大量更新,则 useCallback 可以进行很好的优化,以避免在每次渲染时重新创建该函数。

另一个 JS 框架 meme
React 难吗?快来加入这位创建全新 JS 框架的兄弟吧!

使用 useCallback 防止依赖项更改

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

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

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

const Component = ({ someProp }) => {
  // Reference to onTrigger function will only change when someProp does.
  const onTrigger = useCallback(() => {
    // Some code...
  }, [someProp])

  // This memoized value will only update when onTrigger function updates.
  // The value would be recalculated on every render if onTrigger wasn't wrapper in useCallback.
  const memoizedValue = useMemo(() => {
    // Some code...
  }, [onTrigger])

  // MemoizedChildComponent will only rerender when onTrigger function updates.
  // If onTrigger wasn't wrapped in a useCallback, MemoizedChildComponent would rerender every time this component renders.
  return (<>
    <MemoizedChildComponent onTriggerFn={onTrigger} />
    <button onClick={onTrigger}>Click me</button>
   </>)
}
Enter fullscreen mode Exit fullscreen mode

使用 useCallback 来防止 useEffect 触发

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

import { useCallback, useEffect } from 'react'

const Component = ({ someProp }) => {
  // Reference to onTrigger function will only change when someProp does.
  const onTrigger = useCallback(() => {
    // Some code...
  }, [someProp])

  // useEffect will only run when onTrigger function updates.
  // If onTrigger wasn't wrapped in a useCallback, useEffect would run every time this function renders.
  useEffect(() => {
    // Some code...
  }, [onTrigger])

  return <button onClick={onTrigger}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

当不需要依赖项时,向 useEffect 添加一个空的依赖项列表

如果您有一个不依赖于任何变量的效果,请确保将空的依赖列表作为 useEffect 的第二个参数。如果不这样做,该效果将在每次渲染时运行。

import { useEffect } from 'react'

const Component = () => {
  useEffect(() => {
    // Some code.

    // Do not do this.
  })

  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode
import { useEffect } from 'react'

const Component = () => {
  useEffect(() => {
    // Some code.

    // Do this.
  }, [])

  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode

同样的逻辑也适用于其他 React hooks,例如 useCallback 和 useMemo。不过,正如本文后面所述,如果您没有任何依赖项,可能根本不需要使用这些 hooks。

始终将所有依赖项添加到 useEffects 和其他 React Hooks

处理 React 内置 hooks(例如 useEffects 和 useCallback)的依赖列表时,请务必将所有依赖项添加到依赖列表(hooks 的第二个参数)。如果省略了某个依赖项,effect 或回调可能会使用其旧值,这通常会导致难以检测的 bug。

添加所有变量可能是一个非常棘手的事情,有时您只是不希望在值更新时再次运行效果,但尝试找到解决方案不仅可以避免错误,而且通常还会导致更好的代码编写。

更重要的是,如果你为了避免 bug 而忽略了依赖项,那么升级到较新的 React 版本后,该 bug 还会再次出现。在 React 18 的严格模式下,更新钩子(例如 useEffect、useMemo)在开发环境中会被触发两次,而在未来 React 版本的生产环境中,这种情况可能会再次发生。

复仇的虫子
为了安全起见,最好将所有依赖项添加到 React Hooks 中

import { useEffect } from 'react'

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

  useEffect(() => {
    // Some code.

    // Don't neglect adding variables to dependency list.
    // The value variable should be added here.
  }, [])

  return <div>{value}</div>
}
Enter fullscreen mode Exit fullscreen mode

你可能会想,当 useEffects 触发次数超过预期时,该如何避免副作用?不幸的是,这个问题并没有万能的解决方案。不同的场景需要不同的解决方案。你可以尝试使用hooks 来只运行一次代码,这有时可能有用,但实际上并不推荐。

通常情况下,你可以使用 if 语句来解决问题。你可以查看当前状态,并从逻辑上判断是否真的需要运行这段代码。例如,如果你不想将变量值作为依赖项添加到上述 effect 中,是因为只在未定义时运行代码,那么你只需在 effect 中添加一个 if 语句即可。

import { useEffect } from 'react'

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

  useEffect(() => {
    if (!value) {
      // Some code to run when value isn't set.
    }

  // Do this, always add all dependencies.
  }, [value])

  return <div>{value}</div>
}
Enter fullscreen mode Exit fullscreen mode

其他场景可能更复杂,使用 if 语句来阻止 effect 多次发生可能不太可行。如果这不容易做到,你应该避免使用它来避免 bug。在这种情况下,你应该首先问问自己,你真的需要一个 effect 吗?很多情况下,开发人员在不应该使用 effect 的情况下使用了它。

然而,生活并非如此简单。假设你确实需要使用 useEffect,而你又无法用 if 条件语句轻松解决。你还有什么其他选择呢?实际上,在这种情况下,最简单的方法可能是最好的方法:只需添加所有依赖项,然后让 effect 运行比你预期更多的次数。

与其试图阻止代码执行,不如编写代码,使其无论是否被多次调用都无关紧要。这样的代码被称为幂等代码,非常适合函数式编程。这种行为可以通过使用缓存、节流阀和去抖动函数来实现。我以后可能会写一篇文章详细解释这个主题,但现在先就此打住。

不要使用 useEffect 来启动外部代码

假设你想运行一些代码来初始化一个库。我经常看到类似的初始化代码被放在 useEffect 中,并且依赖列表为空,这完全没有必要,而且容易出错。如果你调用的函数不依赖于组件的内部状态,那么它应该在组件外部初始化。

import { useEffect } from 'react'
import initLibrary from '/libraries/initLibrary'

const Component = () => {
  // Do not do this.
  useEffect(() => {
    initLibrary()
  }, [])

  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode
import initLibrary from '/libraries/initLibrary'

// Do this instead.
initLibrary()

const Component = () => {
  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode

如果初始化需要组件的内部状态,则可以将其放入 useEffect 中,但如果这样做,请确保将使用的所有依赖项添加到 useEffect 的依赖项列表中,如上一个标题所述。

不要将外部函数包装在 useCallback 中

就像上面在 useEffect 中触发 init 函数的情况一样,调用外部函数时不需要 useCallback。只需按原样调用外部函数即可。这样 React 就无需检查 useCallback 是否需要重新创建,从而使代码更简洁。

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

const Component = () => {
  // Do not do this.
  const handleClick = useCallback(() => {
    externalFunction()
  }, [])

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

const Component = () => {
  // Do this instead.
  return <button onClick={externalFunction}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

使用 useCallback 的有效用例是当回调调用多个函数或读取或更新内部状态时,例如来自 useState 钩子的值或组件传入的 props 之一。

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

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

  // This is okay...
  const handleClick = useCallback(() => {
    // ...because we call multiple functions.
    externalFunction()
    anotherExternalFunction()

    // ...because we read and/or set an internal value or prop.
    setValue(passedInProp)
  }, [passedInProp, value])

  return <button onClick={handleClick}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

不要在依赖列表为空的情况下使用 useMemo

如果您添加了具有空依赖列表的 useMemo,请问自己为什么这样做。

是不是因为它依赖于组件的状态变量,而你不想添加它?如果是那样的话,我们已经讨论过了,你应该始终列出所有依赖变量!

是因为 useMemo 实际上没有任何依赖吗?好吧,那就把它从组件里移除吧,它不属于组件!

import { useMemo } from 'react'

const Component = () => {
  // Do not do this.
  const memoizedValue = useMemo(() => {
    return 3 + 5
  }, [])

  return <div>{memoizedValue}</div>
}
Enter fullscreen mode Exit fullscreen mode

// Do this instead.
const memoizedValue = 3 + 5

const Component = () => {
  return <div>{memoizedValue}</div>
}
Enter fullscreen mode Exit fullscreen mode

不要在其他组件中声明组件

我经常看到这种情况,请停止这样做。

const Component = () => {

  // Don't do this.
  const ChildComponent = () => {
    return <div>I'm a child component</div>
  }

  return <div><ChildComponent /></div>
}
Enter fullscreen mode Exit fullscreen mode

这有什么问题?问题在于你滥用了 React。如前所述,组件内声明的变量每次渲染时都会重新声明。在这种情况下,这意味着每次父组件重新渲染时,都必须重新创建函数式子组件。

由于多种原因,这是有问题的。

  1. 每次渲染时都必须实例化一个函数。
  2. React 无法决定何时进行任何类型的组件优化。
  3. 如果在 ChildComponent 中使用钩子,它们将在每次渲染时重新启动。
  4. 组件的代码行数越来越多,阅读起来越来越困难。我见过一个 React 组件里就包含几十个甚至二十个这样的子组件!

该怎么做呢?只需在父组件外部声明子组件即可。

// Do this instead.
const ChildComponent = () => {
    return <div>I'm a child component</div>
}

const Component = () => {
  return <div><ChildComponent /></div>
}
Enter fullscreen mode Exit fullscreen mode

或者更好的是,放在单独的文件中。

// Do this instead.
import ChildComponent from 'components/ChildComponent'

const Component = () => {
  return <div><ChildComponent /></div>
}
Enter fullscreen mode Exit fullscreen mode

不稳定的建造模因
记住,我写这篇文章是有原因的

不要在 If 语句中使用钩子(无条件钩子)

React 文档中对此进行了解释。切勿简单地编写条件钩子。

import { useState } from 'react'

const Component = ({ propValue }) => {
  if (!propValue) {
    // Don't do this.
    const [value, setValue] = useState(propValue)
  }

  return <div>{value}</div>
}
Enter fullscreen mode Exit fullscreen mode

返回后不要使用钩子(无条件钩子)

如果语句根据定义是有条件的,那么在阅读React 的文档时很容易理解你不应该在其中放置钩子

一个更隐蔽的关键字是“return”。很多人没有意识到“return”可以导致条件钩子渲染。请看这个例子。

import { useState } from 'react'

const Component = ({ propValue }) => {

  if (!propValue) {
    return null
  }

  // This hook is conditional, since it will only be called if propValue exists.
  const [value, setValue] = useState(propValue)

  return <div>{value}</div>
}
Enter fullscreen mode Exit fullscreen mode

如你所见,条件返回语句会使后续的钩子也变成条件执行。为了避免这种情况,请将所有钩子放在组件第一个条件渲染的上方。或者,为了更容易记住,干脆始终将钩子放在组件的顶部。

import { useState } from 'react'

const Component = ({ propValue }) => {
  // Do this instead, place hooks before conditional renderings.
  const [value, setValue] = useState(propValue)

  if (!propValue) {
    return null
  }

  return <div>{value}</div>
}
Enter fullscreen mode Exit fullscreen mode

让子组件决定是否渲染

这并不是你总是应该做的事情,但在很多情况下它是合适的。让我们考虑下面的代码。

import { useState } from 'react'

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

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

  return <>
    { !!shouldRender && <ChildComponent shouldRender={shouldRender} /> }
  </>
}
Enter fullscreen mode Exit fullscreen mode

上面是条件渲染子组件的常用方法。这段代码没什么问题,只是在子组件较多的情况下会稍微冗长一些。但根据 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} />
}
Enter fullscreen mode Exit fullscreen mode

在上面的示例中,我们重写了两个组件,将条件渲染移到了子组件中。你可能会想,将条件渲染移到子组件中有什么好处呢?

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

如果组件停止渲染,就像第一个代码一样,则 useState 中保存的状态将被重置,并且 useEffects、useCallbacks 和 useMemos 都需要在组件再次渲染时重新运行并重新计算新值。

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

正如最初提到的,这并非总是你想要做的。有时你确实希望组件完全卸载。例如,如果你的子组件中有一个 useEffect,你可能不想在重新渲染时继续运行它。请参阅下面的示例。

const ChildComponent = ({ shouldRender, someOtherPropThatChanges }) => {

  useEffect(() => {
    // If we don't want this code to run when shouldRender is false,
    // then don't keep render this component when shouldRender is false.
  }, [someOtherPropThatChanges])

  if (!shouldRender) {
    return null
  }

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

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

  return <ChildComponent
    shouldRender={shouldRender}
    someOtherPropThatChanges={someOtherPropThatChanges} />
}
Enter fullscreen mode Exit fullscreen mode

当然,我们可以在子组件内部使用条件逻辑来使上面的代码正常工作,但这样做很容易出错。另外,请记住,不允许使用条件钩子,因此您不能将 useEffect 放在 if 语句之后。

const ChildComponent = ({ shouldRender, someOtherPropThatChanges }) => {

  if (!shouldRender) {
    return null
  }

  useEffect(() => {
    // We cannot avoid running this useEffect by putting it after the
    // null-render. Conditional hook rendering is not allowed in React!
  }, [someOtherPropThatChanges])

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

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

  return <ChildComponent
    shouldRender={shouldRender}
    someOtherPropThatChanges={someOtherPropThatChanges} />
}
Enter fullscreen mode Exit fullscreen mode

使用 useReducer 代替多个 useState

与其使用多个 useState 来增加组件的臃肿,不如使用一个 useReducer。虽然写起来可能比较繁琐,但它既可以避免不必要的渲染,又可以使逻辑更易于理解。有了 useReducer,向组件添加新的逻辑和状态就会容易得多。

在重构为 useReducer 之前,没有神奇的数字来规定要编写多少个 useState,但我个人认为大约三个。

import { useState } from 'react'

const Component = () => {
  // Do not add a lot of useState.
  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>
  </>
}
Enter fullscreen mode Exit fullscreen mode
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>
  </>
}
Enter fullscreen mode Exit fullscreen mode

Typescript 模因
需要更多结构,请考虑使用 TypeScript!

将初始状态写为函数而不是对象

注意本技巧中的代码。查看 getInitialFormState 函数。

// Code removed for brevity.

// Initial state is a function here.
const getInitialFormState = () => ({
  text: '',
  error: '',
  touched: false
})

const formReducer = (state, action) => {
  // Code removed for brevity.
}

const Component = () => {
  const [state, dispatch] = useReducer(formReducer, getInitialFormState());
  // Code removed for brevity.
}
Enter fullscreen mode Exit fullscreen mode

注意到我把初始状态写成了一个函数。我其实可以直接用对象。

// Code removed for brevity.

// Initial state is an object here.
const initialFormState = {
  text: '',
  error: '',
  touched: false
}

const formReducer = (state, action) => {
  // Code removed for brevity.
}

const Component = () => {
  const [state, dispatch] = useReducer(formReducer, initialFormState);
  // Code removed for brevity.
}
Enter fullscreen mode Exit fullscreen mode

我为什么不这么做呢?答案很简单,为了避免可变性。在上面的例子中,当 initialFormState 是一个对象时,我们可能会在代码的某个地方修改它。

如果是这样,当我们再次使用该变量时(例如重置表单时),我们将无法恢复初始状态。相反,我们会获取一个变异的对象,例如,当touched 的值可能为 true 时。

运行单元测试时也是如此。测试上述代码时,多个测试可以使用 initialFormState 并对其进行修改。这样,每个测试在单独运行时都可以正常工作,而当所有测试在一个测试套件中一起运行时,某些测试可能会失败。

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

当组件不需要重新渲染时,使用 useRef 而不是 useState

你知道吗?用 useRef 替换 useState 可以优化组件渲染。看看这段代码。

import { useEffect } from 'react'

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

  useEffect(() => {
    if (!triggered) {
      setTriggered(true)

      // Some code to run here...
    }
  }, [triggered])
}
Enter fullscreen mode Exit fullscreen mode

运行上述代码时,组件会在 setTriggered 调用时重新渲染。在这种情况下,触发状态变量可以确保效果只运行一次(这在 React 18 中实际上不起作用,请参阅这篇关于 useRunOnce hook 的文章了解原因)。

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

import { useRef } from 'react'

const Component = () => {
  // Do this instead.
  const triggeredRef = useRef(false)

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

      // Some code to run here...
    }

  // Note missing dependency. This isn't optimal.
  }, [])
}
Enter fullscreen mode Exit fullscreen mode

请注意 useEffect 缺少依赖项,在使用 useRef 时修复该依赖项有点棘手,但 React 在其文档中对此进行了解释。

对于上面的情况,你可能会疑惑,为什么我们需要使用 useRef 呢?为什么我们不能简单地使用组件外部的变量呢?

// This does not work the same way!
const triggered = false

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

      // Some code to run here...
    }
  }, [])
}
Enter fullscreen mode Exit fullscreen mode

我们之所以需要 useRef 是因为上面的代码无法以相同的方式工作!上面的触发变量只会为 false 一次。如果组件卸载,当组件再次挂载时,触发变量仍然会被设置为 true,因为触发变量没有绑定到 React 的生命周期。

当使用 useRef 时,React 会在组件卸载并重新挂载时重置其值。在这种情况下,我们可能希望使用 useRef,但在其他情况下,我们可能要查找组件外部的变量。

文章来源:https://dev.to/perssondennis/react-anti-patterns-and-best-practices-dos-and-donts-3c2g
PREV
编写 SOLID React Hooks
NEXT
每个前端开发人员都需要的免费图像工具列表