React Hooks使用规则:8个方法
▌ 技术引导 React Hooks 使用规则的8个方法是决定你项目是否稳定、可维护的核心因素。我见过太多团队在使用 Hooks 时因为规则不清晰导致状态不可预测、组件逻辑混乱甚至崩溃。关键点在于避免在循环、条件判断或嵌套函数中使用 Hooks,否则会触发警告甚至导致状态丢失。useEffect 和 useRef 是最容易搞错的两个,很多人在组件卸载时没有清理副作用,让内存泄漏成为常态。useContext 虽然方便,但不恰当的使用会让组件层级暴涨,性能掉线。useReducer 应该用于复杂状态逻辑,而不是简单情况。useMemo 用得不对,反而增加渲染负担,导致卡顿。useCallback 和 useLayoutEffect 之间容易混淆,误用会引发渲染顺序问题。Hooks 必须在顶层函数中调用,不能在 if、for、try 中使用,否则会打乱 React 的执行顺序,让组件行为变得不可控。我见过几个项目因为没遵守规则,导致 Redux 无法正确更新状态,甚至出现局部更新失败的问题。 React Hooks 的8个方法不仅仅是规则,更是一套行为准则,决定了你组件的可读性和可维护性。useEffect 是最容易被滥用的,很多人直接在 render 里写副作用,结果导致组件频繁重渲染,内存占用飙升。useRef 是很多人用来存变量的“万能工具”,但如果用它来保存组件状态,会绕过 React 的状态管理机制,引发逻辑错误。useContext 的最佳实践是用 Provider 和 Consumer 搭配,而不是直接在组件里调用 context。useReducer 应该与 useImperativeHandle 结合使用,避免状态管理变得复杂。useMemo 的使用前提是计算成本高,否则它只会带来额外开销。useCallback 与 useMemo 本质上是挂钩的,但它们解决的问题不同,混用会导致不必要的渲染。useLayoutEffect 用于同步 DOM 操作,但不要用它来处理异步请求,否则会破坏 React 的渲染流程。 我在一些大型项目中发现,Hooks 的使用方式往往决定了项目后期的重构难度。比如,一个项目用了大量 useReducer,但没有规范的 reducer 分割,导致后续维护成本极高。useContext 的滥用会导致嵌套层级过多,组件变得难以理解。useEffect 没有清理函数,组件卸载时依然在执行副作用,导致内存泄漏和性能下降。useRef 的误用会让组件失去 React 的状态管理能力,变成纯函数式组件,无法响应状态变化。useMemo 和 useCallback 没有正确设置依赖项,导致计算结果不准确,甚至引发无限循环。useLayoutEffect 的使用场景非常有限,很多人误以为它是 useEffect 的替代品,结果却引发了渲染顺序的混乱。Hooks 的规则不是限制,而是为了确保组件行为的可预测性。 实际开发中,Hooks 的使用规则往往被忽略。比如,在组件中使用多个 useEffect 但没有正确设置依赖项,导致不必要的重复执行。useRef 没有正确初始化,使得组件状态无法被正确引用。useContext 不加限制地传递数据,让组件变得臃肿。useMemo 被用来缓存所有变量,结果反而增加了内存占用。useCallback 被滥用为函数缓存,而不是优化渲染性能。useEffect 未使用清理函数,导致全局变量未被释放。React 的版本迭代中,Hooks 规则也不断收紧,比如在 React 18 中,useLayoutEffect 的执行时机发生了变化,很多人没有及时调整代码。Hooks 的使用必须遵循特定的模式,否则会导致组件行为不可控,甚至引发崩溃。 如果一个项目只靠 Hooks 做了状态管理,但没遵循规则,后期重构会非常痛苦。比如,一个项目用了多个 useReducer,但这些 reducer 没有被拆分,导致同一个组件里充斥着不同状态的处理逻辑。useContext 被用来传递组件间的数据,但没有明确的边界,结果让整个项目变得难以维护。useEffect 没有正确清理,导致组件卸载时还在运行定时器或请求。useMemo 的使用没有经过性能评估,反而让代码变得复杂。useCallback 被用来缓存所有函数,但实际上只有部分需要缓存。React 的规则不是随便写的,而是为了确保组件的可预测性和可维护性。 ▌ 技术参考 一 React Hooks 的使用规则必须严格遵守,否则会导致组件逻辑混乱。useEffect 不得用于 render 函数内,否则会触发警告并导致副作用执行失败。useEffect 的依赖项必须是 immutable 的,否则会导致无限循环。例如: ```javascript useEffect(() => { const timer = setInterval(() => { console.log('tick') }, 1000) return () => clearInterval(timer) }, []) ``` 这个例子中,如果依赖项不为空,每次组件更新都会重新创建 timer,导致内存泄漏。正确的做法是使用 empty array 作为依赖项,确保副作用只执行一次。 二 useReducer 是处理复杂状态逻辑的首选方案,而不是无脑使用 useState。它适合多个状态之间存在关联的场景。例如,一个表单组件需要管理多个字段的值,使用 useReducer 可以集中管理状态逻辑,避免多个 useState 调用带来的混乱。reducer 函数必须是纯函数,不能依赖外部变量,否则会导致状态更新不可预测。 三 useContext 的使用需要谨慎,不要将其作为组件间传递数据的“万能工具”。最佳实践是使用 Provider 和 Consumer 搭配,确保 context 的传递链条清晰。例如: ```javascript const ThemeContext = createContext() function App() { const [theme, setTheme] = useState('dark') return ( ) } ``` 如果 context 传递层级过多,建议使用 React 的 Context API 与 TypeScript 搭配,增加类型安全性和可读性。 四 useLayoutEffect 与 useEffect 类似,但执行时机更早,适合处理同步 DOM 操作。然而它的使用场景非常有限,不要用它来替代 useEffect。useLayoutEffect 通常用于在渲染之后但布局计算之前执行操作,例如手动调整 DOM 样式或高度。例如: ```javascript useLayoutEffect(() => { const node = ref.current if (node) { node.style.height = '100px' } }, [ref]) ``` 如果用在异步操作中,会导致渲染顺序混乱,甚至引发崩溃。 五 useReducer 与 useContext 的结合使用可以提升状态管理的可维护性。例如,在一个中大型项目中,状态由多个 reducer 管理,每个 reducer 负责一部分逻辑,通过 context 传递 dispatch 方法。这种方式让组件间的通信更加清晰,但需要注意 reducer 的顺序和合并策略,否则会导致状态更新错误。 六 useImperativeHandle 用于暴露子组件的实例方法,但它的使用必须遵循规则。例如: ```javascript useImperativeHandle(ref, () => ({ focus() { inputRef.current.focus() }, reset() { inputRef.current.value = '' } })) ``` 如果 ref 没有被正确使用,比如在函数组件中未通过 forwardRef,会导致方法调用失败。 七 useCallback 是用来缓存函数的,但它的依赖项必须准确。如果依赖项未正确设置,会导致函数被频繁创建,反而增加性能负担。例如: ```javascript const onClick = useCallback(() => { console.log('clicked') }, [someValue]) ``` 当 someValue 变化时,onClick 会被重新创建,否则不会。使用 useCallback 必须结合 useMemo 和 useEffect,确保函数传递的准确性。 八 useLayoutEffect 的依赖项必须和 useEffect 保持一致,否则会导致执行顺序混乱。例如,一个组件可能同时使用 useEffect 和 useLayoutEffect,但它们的依赖项不同,导致渲染顺序不一致。 九 useReducer 的初始化方式有多种,包括直接传入对象和使用函数。例如: ```javascript const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'increment': return { count: state.count + 1 } default: return state } }, { count: 0 }) ``` 或者: ```javascript const [state, dispatch] = useReducer((state, action) => { ... }, () => { return { count: 0 } }) ``` 第二种方式适用于异步初始化的情况,比如从 API 获取初始值。 十 useImperativeHandle 的返回值必须与 ref 的类型一致,否则会导致类型错误。例如,如果 ref 的类型是 RefObject,那么返回的函数必须是针对 input 元素的。如果返回的是其他类型,会导致调用失败。 十一 useLayoutEffect 的执行顺序在 React 18 中发生了变化,必须注意其对渲染流程的影响。在 React 18 中,useLayoutEffect 会在 commit 阶段执行,而 useEffect 会在 post-commit 阶段执行。这种变化可能会影响组件的渲染行为。例如,useLayoutEffect 可能会导致组件在渲染后立即更新,而 useEffect 可能会延迟。 十二 useReducer 的性能优化需要结合 useImperativeHandle 和 useContext,确保状态更新不会触发不必要的重新渲染。例如,在一个表单组件中,使用 useReducer 管理表单字段的状态,使用 useImperativeHandle 暴露接口,使用 useContext 传递 dispatch 方法。这种方式让组件逻辑更加清晰。 十三 useRef 的用途非常广泛,但不能用来替代 state。例如,如果一个组件需要记住某个值,比如输入框的当前值,不要使用 useRef,而是使用 state。useRef 会保存变量,但不会触发组件更新,导致数据无法同步。 十四 useReducer 的 reducer 函数必须是纯函数,不能修改传入的参数。例如: ```javascript function reducer(state, action) { // 不要直接修改 state // 应该返回新的对象 return { ...state, count: state.count + 1 } } ``` 如果 reducer 有副作用,会导致状态更新不可预测。 十五 useLayoutEffect 的执行时间比 useEffect 更早,适合处理需要同步的 DOM 操作,但不要用它来做异步请求。例如,调整元素大小或者手动设置样式时,可以使用 useLayoutEffect,但不要在其中发起 API 请求,否则会导致渲染顺序混乱。





