import makeCss from 'some/css/in/js/library' // 堆代码 duidaima.com 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 防止函数重新创建
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是一种很好的优化方法,以避免在每次渲染时重新创建函数。
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 触发
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 添加空依赖项
import { useEffect } from 'react' const Component = () => { useEffect(() => { // ... }, []) return <div>Example</div> }这个逻辑也适用于其他 React hook,例如 useCallback 和 useMemo。不过,如果没有任何依赖项,可能根本不需要使用这些 Hooks。
import { useEffect } from 'react' const Component = () => { const [value, setValue] = useState() useEffect(() => { // 使用 value 变量的代码 // 将变量添加到依赖项数组中,应在此处添加 value 变量 }, []) return <div>{value}</div> }那当 useEffect 被触发的次数比希望的次数多时,如何避免副作用?不幸的是,没有完美的解决方案。不同的场景需要不同的解决方案。可以尝试使用 hook 仅运行一次代码,这有时很有用,但实际上并不是一个值得推荐的解决方案。
import { useEffect } from 'react' const Component = () => { const [value, setValue] = useState() useEffect(() => { if (!value) { // ... } }, [value]) return <div>{value}</div>其他场景可能更复杂,也许使用 if 语句来防止 effect 多次发生不太可行。在这种情况下,首先应该确定,真的需要 effect 吗?在很多情况下,开发人员在实际上不应该这样做时却使用了 effect。
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 与空依赖数组一起使用
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.不要在其他组件中声明组件
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 语句中使用 Hook
import { useState } from 'react' const Component = ({ propValue }) => { if (!propValue) { const [value, setValue] = useState(propValue) } return <div>{value}</div> }11.使用 useState 而不是变量
import AnotherComponent from 'components/AnotherComponent' const Component = () => { const value = { someKey: 'someValue' } return <AnotherComponent value={value} /> }在上述情况下,依赖于value的AnotherComponent及其相关内容将在每次渲染时重新渲染,即使它们使用memo、useMemo或useCallback进行了记忆化处理。如果将一个带有value作为依赖的useEffect添加到组件中,它将在每次渲染时触发。因为每次渲染时 value 的JavaScript引用都会不同。
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 后不使用 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.让子组件决定是否应该渲染
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 可以在隐藏时保持其状态,然后第二次渲染而不会丢失其状态。它一直都在那里,只是不可见。
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.将初始状态写为函数而不是对象
// 初始状态是一个函数 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 这样的库,它用于避免编写可变代码。
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。