承接上文 react 官网「核心、高级」总结条目
Hook
简介
- Hook 可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
- 使用函数声明组件时,用的就是 Hook
- 特点
- 完全可选的。 可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的。 Hook 不包含任何破坏性改动。
- 现在可用。 Hook 已发布于 v16.8.0。
- 没有计划从 React 中移除 class。
- Hook 不会影响你对 React 概念的理解。
- Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。
动机
-
在组件之间复用状态逻辑很难
- React 没有提供将可复用性行为“附加”到组件的途径。
- 常用 render props 和 高阶组件 解决此问题。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
- 由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”
- 使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- React 没有提供将可复用性行为“附加”到组件的途径。
-
复杂组件变得难以理解
- 每个生命周期常常包含一些不相关的逻辑,逐渐会被状态逻辑和副作用充斥。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。
- 如此很容易产生 bug,并且导致逻辑不一致。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
- 每个生命周期常常包含一些不相关的逻辑,逐渐会被状态逻辑和副作用充斥。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。
-
难以理解的 class
- Hook 使你在非 class 的情况下可以使用更多的 React 特性。
-
Hook 和现有代码可以同时工作,你可以渐进式地使用他们。
概览
后续有概览的详情细节
- Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。
- Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
- Hook 已经保存在函数作用域中,可以方便访问
state
、props
等变量。 - Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
State Hook
- 在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。
useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。useState
唯一的参数就是初始 state。这个初始 state 参数只有在第一次渲染时会被用到。- 可以在一个组件中多次使用 State Hook
Effect Hook
-
React 会等待浏览器完成画面渲染之后才会延迟调用
useEffect
-
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。- 合并了class 组件中的
componentDidMount
、componentDidUpdate
和componentWillUnmount
- 通过使用 Hook,你可以把组件内相关的副作用组织在一起,而不要把它们拆分到不同的生命周期函数里。
- 合并了class 组件中的
-
在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
-
由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。
-
默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。
-
副作用函数还可以通过
return
返回一个函数来指定如何“清除”副作用。 -
可以在组件中多次使用
useEffect
Hook 使用规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(自定义的 Hook 中也可以调用)
自定义 Hook
- 自定义 Hook 可以在不增加组件的情况下达到想要在组件之间重用一些状态逻辑的目的。来替代两种主流方案:高阶组件和 render props。
- 我们只需将调用
useState
和useEffect
的 Hook 逻辑抽取到一个自定义 Hook 即可。 - 可以在单个组件中多次调用同一个自定义 Hook。
- Hook 的每次调用都有一个完全独立的 state
- 自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “
use
” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。- 更方便在检查工具中找到问题所在,如 linter 插件。
其他 Hook
useContext
让你不使用组件嵌套就可以订阅 React 的 Context。useReducer
可以让你通过 reducer 来管理组件本地的复杂 state。
usestate
-
useState
与 class 里面的this.state
提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。 -
useState()
方法里面唯一的参数就是初始 state。- 参数与 class 不同的是可以是基础数据类型,而不一定是对象。
-
useState()
的返回值为:当前 state 以及更新 state 的函数,需要成对的获取它们。 -
const [count, setCount] = useState(0); 复制代码
- JavaScript 语法叫数组解构。它意味着我们同时创建了两个变量,第一个变量返回的是第一个值,第二个变量是返回的第二个值。
Effect Hook
- Effect Hook 可以让你在函数组件中执行副作用操作。
- 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
- 每次重新渲染都会生成新的 effect 替换掉之前的。
- 某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
- 传递给
useEffect
的函数在每次渲染中都会有所不同,因此我们可以在 effext 中获取到最新的数据值,而不同担心其过期的原因
useEffect
会在每次渲染后都执行(在第一次渲染之后和每次更新之后都会执行。)- 可以把
useEffect
Hook 看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。- 避免在多个生命周期函数中编写重复的代码。
- React 组件中有两种常见副作用操作:需要清除的和不需要清除的。
- 无需清除的 effect:在 React 更新 DOM 之后运行一些额外的代码,执行完这些操作之后,就可以忽略他们了。
- 如发送网络请求,手动变更 DOM,记录日志。
- 需清除的 effect:例如订阅外部数据源,清除工作是非常重要的,可以防止引起内存泄露!
- 每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
- React 会在组件卸载的时候执行清除操作。而 effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。每次都重新渲染新的 effect。
- 无需清除的 effect:在 React 更新 DOM 之后运行一些额外的代码,执行完这些操作之后,就可以忽略他们了。
进阶
-
使用多个 Effect 实现关注点分离
- Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
- 解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
-
每次更新的时候都要运行 Effect
useEffect
会默认在调用一个新的 effect 之前对前一个 effect 进行清理。避免了组件函数中的内存泄漏造成了 bug 问题。
-
通过跳过 Effect 进行性能优化
-
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为
useEffect
的第二个可选参数。-
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 仅在 count 更改时更新 复制代码
-
-
同样对于有清除操作的 effect 同样适用
-
确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
-
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组
[]
作为第二个参数。
-
Hook 规则
- 只在最顶层使用 Hook
- 确保 Hook 在每一次渲染中都按照同样的顺序被调用,保持 hook 状态的正确。
- React 靠的是 Hook 调用的顺序,来确定哪个 state 对应哪个 useState。
- 若不在顶层使用 Hook 则有可能破坏顺序(if for 等),导致 state 状态出现 bug。
- 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
- 确保 Hook 在每一次渲染中都按照同样的顺序被调用,保持 hook 状态的正确。
- 只在 React 函数中调用 Hook
- 确保组件的状态逻辑在代码中清晰可见。
- 不要在普通的 JavaScript 函数中调用 Hook。
Hook API
基础 Hook
useState
const [state, setState] = useState(initialState);
- 在初始渲染期间,返回的状态 (
state
) 与传入的第一个参数 (initialState
) 值相同。 setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。- 两种用法:普通式和函数式
setCount(x)
直接传入参数值,设置 state 状态。setCount(x => x++)
调用函数改变 state 状态。
setState
函数的标识是稳定的,并且不会在组件重新渲染时发生变化。- 如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
useState
不会自动合并更新对象。- 你可以用函数式的
setState
结合展开运算符来达到合并更新对象的效果return {...prevState, ...updatedValues};
- 你可以用函数式的
- 惰性初始 state:初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用。
- 跳过 state 更新:调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。
- React 使用
Object.is
比较算法 来比较 state。 - React 可能仍需要在跳过渲染前渲染该组件。
- 如果你在渲染期间执行了高开销的计算,则可以使用
useMemo
来进行优化。
- React 使用
useEffect
useEffect(didUpdate);
- 该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
- 使用
useEffect
完成副作用操作,赋值给useEffect
的函数会在组件渲染到屏幕之后执行。 - 默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
- 为防止内存泄漏,清除函数会在组件卸载前执行。
- 如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
useEffect
的函数与componentDidMount
、componentDidUpdate
不同,在浏览器完成布局与绘制之后执行。- React 为此提供了一个额外的
useLayoutEffect
Hook 来处理不能被延迟执行的 effect。它和useEffect
的结构相同,区别只是调用时机不同。- 类似于被动监听事件和主动监听事件的区别。
- 可以给
useEffect
传递第二个参数,它是 effect 所依赖的值数组。只有当值数组改变后才会重新渲染执行。- 所有 effect 函数中引用的值都应该出现在依赖项数组中,因为依赖项数组不会作为参数传给 effect 函数。
- 未来编译器会更加智能,届时自动创建数组将成为可能。
useContext
const value = useContext(MyContext);
- 接收一个 context 对象(
React.createContext
的返回值)并返回该 context 的当前值。 useContext
的参数必须是 context 对象本身:- 正确:
useContext(MyContext)
- 错误:
useContext(MyContext.Consumer)
- 错误:
useContext(MyContext.Provider)
- 正确:
- 当前的 context 值由上层组件中距离当前组件最近的
<MyContext.Provider>
的value
prop 决定。 - 祖先使用
React.memo
或shouldComponentUpdate
,也会在组件本身使用useContext
时重新渲染。
额外的 Hook
额外的 Hook 是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如(state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的dispatch
方法。- 使用
useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递dispatch
而不是回调函数 。 - 惰性初始化:将
init
函数作为useReducer
的第三个参数传入,这样初始 state 将被设置为init(initialArg)
。 - 跳过 dispatch:如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。
- React 使用
Object.is
比较算法 来比较 state。
- React 使用
- React 会确保
dispatch
函数的标识是稳定的,并且不会在组件重新渲染时改变。
useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], ); 复制代码
- 返回一个 memoized 回调函数。
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
。- 该回调函数仅在某个依赖项改变时才会更新。
- 用于当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如
shouldComponentUpdate
)的子组件。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); 复制代码
- 返回一个 memoized 值。
- 把“创建”函数和依赖项数组作为参数传入
useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。 - 这种优化有助于避免在每次渲染时都进行高开销的计算。
- 传入
useMemo
的函数会在渲染期间执行。- 请不要在这个函数内部执行与渲染无关的操作。
- 诸如副作用这类的操作属于
useEffect
的适用范畴,而不是useMemo
。
- 如果没有提供依赖项数组,
useMemo
在每次渲染时都会计算新的值。
useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。- 返回的 ref 对象在组件的整个生命周期内保持不变。
useRef()
比ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。useRef()
创建的是一个普通 Javascript 对象。和自建一个{current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref 对象。- 当 ref 对象内容发生变化时,
useRef
并不会通知你。变更.current
属性不会引发组件重新渲染。 - 如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
可以让你在使用ref
时自定义暴露给父组件的实例值。- 在大多数情况下,应当避免使用 ref 这样的命令式代码。
useImperativeHandle
应当与forwardRef
一起使用
useLayoutEffect
-
其函数签名与
useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。- 可以使用它来读取 DOM 布局并同步触发重渲染。
-
在浏览器执行绘制之前,
useLayoutEffect
内部的更新计划将被同步刷新。 -
尽可能使用标准的
useEffect
以避免阻塞视觉更新。 -
useLayoutEffect
与componentDidMount
、componentDidUpdate
的调用阶段是一样的。- 推荐你一开始先用
useEffect
,只有当它出问题的时候再尝试使用useLayoutEffect
。
- 推荐你一开始先用
-
useLayoutEffect
还是useEffect
都无法在 Javascript 代码加载完成之前执行。- 这就是为什么在服务端渲染组件中引入
useLayoutEffect
代码时会触发 React 告警。 - 解决1:需要将代码逻辑移至
useEffect
中(如果首次渲染不需要这段逻辑的情况下), - 解决2:将该组件延迟到客户端渲染完成后再显示(如果直到
useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。 - 若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用
showChild && <Child />
进行条件渲染,并使用useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
- 这就是为什么在服务端渲染组件中引入
useDebugValue
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。- 格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook 的特殊情况
- 接受一个格式化函数作为可选的第二个参数。
- 该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
- 不推荐向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
官方 Hooks 的 FAQ
-
官方推荐使用 Hooks 成为编写 React 组件的主要方式。
-
Hook 对静态类型支持十分友好。
-
Hooks 的 lint 规则强制了两点规则:(ESLint 插件 )
- 对 Hook 的调用要么在一个
大驼峰法
命名的函数(视作一个组件)内部,要么在另一个useSomething
函数(视作一个自定义 Hook)中。 - Hook 在每次渲染时都按照相同的顺序被调用。
- 对 Hook 的调用要么在一个
-
Class 的生命周期方法对应到 Hook
constructor
:函数组件不需要构造函数。你可以通过调用useState
来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给useState
。getDerivedStateFromProps
:改为 在渲染时 安排一次更新。shouldComponentUpdate
:详见 下方React.memo
.render
:这是函数组件体本身。componentDidMount
,componentDidUpdate
,componentWillUnmount
:useEffect
Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。getSnapshotBeforeUpdate
,componentDidCatch
以及getDerivedStateFromError
:目前还没有这些方法的 Hook 等价写法,但很快会被添加。
-
类似实例变量:
useRef()
Hook 不仅可以用于 DOM refs。「ref」 对象是一个current
属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。 -
在定义 state 变量时,推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。
-
通过 ref 来手动实现获取上一轮的
props
或state
。 -
若函数中看到陈旧的 props 和 state。
- 想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。
- 使用了「依赖数组」优化但没有正确地指定所有的依赖。
-
使用
useReducer
以一个增长的计数器来在 state 没变的时候依然强制一次重新渲染 -
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。
-
条件式的发起 effect 可以在更新时跳过 effect,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。
-
避免在 effect 外部去声明所需要的函数,记住 effect 外部的函数使用了哪些 props 和 state 很难。
- 如果没用到组件作用域中的任何值,就可以安全地把条件参数指定为
[]
- 如果没用到组件作用域中的任何值,就可以安全地把条件参数指定为
-
在特殊需求下,可以使用一个
ref
来保存一个可变的变量,类似 class 中的this
的功能。 -
用
React.memo
包裹一个组件来对它的 props 进行浅比较,实现shouldComponentUpdate
-
使用
useMemo
Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果。 -
Hook 不会因为在渲染时创建函数而变慢吗。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
-
Hook 的设计在某些方面更加高效:
- Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
- 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。
-
React 内联函数,每次渲染都传递新的回调会如何破坏子组件的
shouldComponentUpdate
优化有关。Hook 从三个方面解决了这个问题。useCallback
Hook 允许你在重新渲染之间保持对相同的回调引用以使得shouldComponentUpdate
继续工作useMemo
Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。useReducer
Hook 减少了对深层传递回调的依赖,正如下面解释的那样。
-
若一个经常变化的值,则推荐 在 context 中向下传递
dispatch
而非在 props 中使用独立的回调。 -
Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
-
多个
useState()
调用会得到各自独立的本地 state 的原因。- 每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。
- 当你用
useState()
调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。
后言
官方文档总结学完后,进而利用 Rax.js 重构官方例子“井字棋”,并添加新功能任务