React Hooks 是什么?
2022年11月16日
虽然近几年大部分开发 React 都是使用 Hooks,但 Hooks 是在 React 16.8 之后出现的,在面试时也会被问到 Hooks 的相关问题,本篇文章总结了
- 什么是 Hook?
- Hook 解决了什么问题?
- Hook 的规则?
- 常使用的 Hook?
什么是 Hook?
在 React 16.8 之前,如果我们想要使用到生命周期的方法或状态 (state),那我们只能使用 React 的 class component。但 React hooks 的出现,让我们即使使用 functional component,也能够使用到 React 的功能和状态。在 React 中,用 use
开头的函式我们会把它称之为 Hook,有些 Hooks 是 React 内建的功能,例如:useState
、useEffect
,但我们也可以创造自己的 Hooks。 Hooks 相较于函式更严谨、有一些需要遵守的规范,我们接下来在下方会提到。
Hook 解决了什么问题?
React 团队开发 Hooks 主要为了解决这三个原因
状态相关的逻辑 (stateful logics) 在 class component 之间难以复用
在使用 class component 时,React 并没有提供可以将重复的逻辑添加到元件的方法。因此,开发者可能使用 render props 或是 higher-order components 的方式达到,但这个缺点会是需要重新架构元件,不仅麻烦、而且代码也不易读 (React 文件戏称为 wrapper hell)。因此, Hook 的解决的主要问题之一,正是让共享状态逻辑。
使用 Hooks,开发者可以从元件中提取状态相关的逻辑,并对它进行独立复用。我们也不需要重组元件,就可以在元件中复用相同逻辑。举例来说,如果某个应用程式需要侦测页面滑动,并根据滑动来触发某些函式,这时我们可以写一个
useScroll
,并在不同页面共享这个useScroll
的状态逻辑。目前在 React 的社群中,有很多 Hooks 的函式库,便提供了各式各样可重复使用的 Hooks。在 Hooks 出现前,复杂元件的逻辑会越写越让人难理解
当 class component 越来越庞大或逻辑复杂时,我们可能会在同一个生命周期方法内需要加入很多不相关的逻辑,例如:
componentDidMount
要处理 data fetching 和事件侦听器逻辑,不仅难以理解也不好维护,且在很多情况下,也无法把这些元件拆成更小的元件。为了解决此问题, Hooks 允许将一个元件拆为更小的函式,而不是根据生命周期的方法拆分,例如上段例子,data fetching 和事件侦听器两种逻辑,可以在同一个元件,通过两个
useEffect
各自处理,又或者两者拆成两个 custom hooks。透过这样细分,,不相关的逻辑可以拆到不同的useEffect
,让副作用的逻辑更好管理。class 对于开发者来说不好理解
React 团队发现,class 可能会是学习 React 的主要障碍。因为 class 的概念在 JavaScript 和其他语言中相当不同 (在 JavaScript 当中,class 是语法糖),如果开发者过去是写其他语言,转来写 JavaScript 时,需要特别了解 class 在 JavaScript 是如何运作的。
为了解决这些问题,Hooks 可以在没有 class 的情况下使用更多 React 的特性。从概念上讲,React 元件一直更接近函式。 Hooks 包含函式,但不牺牲 React 的实用精神,且不需要特别学习复杂的 functional or reactive programming。
Hook 的规则
只能在最顶层使用 Hook
不能在
for
循环、if…else
或巢状中(如:map
)中使用 Hook,我们需要确保永远都在 React 函式的最顶层以及任何 return 之前使用(备注:在《为什么只能在最顶端层呼叫 Hook?》一文当中,我们有详细讨论原因,这也是常考的面试题,推荐大家多读读)。只能在 React 函式中使用 Hook
无法在一般的 JavaScript 函式中使用 Hook,只有以下两种情境可以使用
- 在 React 的 functional component 中使用
- 在自定义的 Hook (custom hook) 中使用其他 Hook
常使用的 Hook
- useState:用于定义和保存元件中状态(state),会回传一个包含两个值的数组,第一个值是现在 state 的值,第二个值是一个 setter function,我们可以透过这个 setter function 更新 state 值并触发 re-render。
- useEffect:当想执行副作用(side effect)时,会透过 useEffect 处理,例如:fetch api、纪录追踪、setInterval()。我们需要传送两个参数在
useEffect
中,第一是一个 setup function,代表我们想执行的副作用代码,如果最后我们要清除这个副作用,需要在最后回传一个 cleanup function; 第二个参数会是一个数组,数组中的元素会是相依于这个副作用代码会用到的变数。 - useLayoutEffect:与
useEffect
是类似的,唯一不同点在于执行时机不同,useLayoutEffect
会在 DOM 更新之后执行;useEffect
则是在 render 渲染结束后执行。 - useReducer:也是一种管理 state 的 hook,可作为
useState
替代方案。接收两个参数,第一个是 reducer,第二个为初始值。会返回两个值,现在 state 的值和dispatch
方法。当状态管理逻辑变得更复杂时,通常会建议使用useReducer
而非useState
。 - useCallback:在重新渲染之间,用来缓存函式的方法。会回传一个被 memoized 的 callback 函式,只有在依赖发生变化时,才会更新。此方法通常用在性能优化时。
- useMemo:在重新渲染之间,用来缓存计算结果的方法。传入一个创建函式和依赖项目,创建函式会需要返回一个值,只有在依赖发生变化时,才会更新值。此方法通常用在性能优化时。
- useRef:用来储存记录不需要渲染的值。会返回一个可变的 ref 物件,
.current
属性会被初始化为传入的参数值(initialValue)。