Hook

一系列以 “use” 作为开头的方法,它们提供了让你可以完全避开 class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

好处:

  • 跨组件复用: 其实render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱
  • 相比而言,类组件的实现更为复杂
    • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理
    • 时刻需要关注this的指向问题
    • 代码复用代价高高阶组件的使用经常会使整个组件树变得臃肿;
  • 状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

Hook 规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们

遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用

这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。你可以:

  • React 的函数组件中调用 Hook
  • 自定义 Hook 中调用其他 Hook

遵循此规则,确保组件的状态逻辑在代码中清晰可见

useState

概要

用于定义组件的 State,类似类定义中 this.state 的功能。

1
const [state, setState] = useState(initialState);
  • useState() 方法里面唯一的参数initialState就是初始 state

    不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象

  • 返回值为:当前 state 以及更新 state 的函数

    这与 class 里面 this.state.countthis.setState 类似,唯一区别就是你需要成对的获取它们。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state

React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。

这就是为什么可以安全地从 useEffectuseCallback 的依赖列表中省略 setState

补充

能用其他状态计算出来就不用单独声明状态。

一个 state 必须不能通过其它 state/props 直接计算出来,否则就不用定义 state。

保证数据源唯一

在项目中同一个数据,保证只存储在一个地方。

不要既存在 redux 中,又在组件中定义了一个 state 存储。

不要既存在父级组件中,又在当前组件中定义了一个 state 存储。

不要既存在 url query 中,又在组件中定义了一个 state 存储。

useState 适当合并

如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称

State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。

然而,不像 class 中的 this.setState更新 state 变量总是替换它而不是合并它

同样含义的变量可以合并成一个 state,代码可读性会提升很多:

1
2
3
4
5
6
7
8
9
10
const [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});

const [weather, setWeather] = useState();
const [room, setRoom] = useState();

当然这种方式我们在变更变量时,一定不要忘记带上老的字段,比如我们只想修改 firstName

1
2
3
4
setUserInfo(s=> ({
...s,
fristName,
}))

其实如果是 React Class 组件,state 是会自动合并的:

1
2
3
this.setState({
firstName
})

函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState

该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:

1
2
3
4
5
6
7
8
9
10
11
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}

“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。

但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值。

如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。

你可以用函数式的 setState 结合展开运算符达到合并更新对象的效果

1
2
3
4
5
const [state, setState] = useState({});
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});

useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。

惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略

如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

1
2
3
4
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

跳过 state 更新

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。

(React 使用 Object.is 比较算法来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useEffect

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。

不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。

无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求手动变更 DOM记录日志,这些都是常见的无需清除的操作。

useEffect 做了什么?

通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。

React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它

为什么在组件内部调用 useEffect

useEffect 放在组件内部让我们可以在 effect 中直接访问 state 变量(或其他 props)。

我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。

Hook使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?

是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。

你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。

React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

大多数情况下,effect 不需要同步地执行。

在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源

这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

为什么要在 effect 中返回一个函数?

这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数

如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect?

React 会在组件卸载的时候执行清除操作。

正如之前学到的,effect 在每次渲染的时候都会执行。

这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

补充

使用多个 Effect 实现关注点分离

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。

React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。

在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:

1
2
3
4
5
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量

要记住 effect 外部的函数使用了哪些 propsstate 很难。

这也是为什么 通常你会想要在 effect 内部去声明它所需要的函数。

只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。

如果出于某些原因你无法 把一个函数移动到 effect 内部,还有一些其他办法:

  • 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
  • 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
  • 万不得已的情况下,你可以 把函数加入 effect 的依赖但把它的定义包裹useCallbackHook。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变。

如果我的 effect 的依赖频繁变化,我该怎么办?

有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖

return <h1>{count}</h1>;
}

传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。

但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。

因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0

每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。

事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。

但这并不是我们想要的。要解决这个问题,我们可以使用setState 的函数式更新形式。

它允许我们指定 state 该 如何改变而不用引用 当前state:

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量

return <h1>{count}</h1>;
}

setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)

此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值(在回调中变量命名为 c)。

在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook把 state 更新逻辑移到 effect 之外。

useReducerdispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。

万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以使用一个 ref来保存一个可变的变量。然后你就可以对它进行读写了。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Example(props) {
// 把最新的 props 保存在一个 ref 中
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});

useEffect(() => {
function tick() {
// 在任何时候读取最新的 props
console.log(latestProps.current);
}

const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 这个 effect 从不会重新执行
}

仅当你实在找不到更好办法的时候才这么做,因为依赖于变更会使得组件更难以预测。

个人理解

依赖的值可以设置多个,只要有一个更新,就会执行effect。

放到 deps 数组中的变量变化时,就会触发 useEffect 函数执行。

  • 一种方法是在依赖中只放入需要触发函数执行的变量,选择性忽略 eslint-plugin-react-hooks 插件的警告。// eslint-disable-next-line
  • 另一种方法是在依赖中写全所有外部作用域中会随时间变化并且在 effect 中使用的变量,如果effect有条件触发,自己写if判断,而不是靠依赖数组。即具体逻辑是否执行应该在内部自己判断,而不是交给react。

useRef

  • 多次渲染之间保证唯一值的纽带

    useRef 会在所有的 render 中保持对返回值的唯一引用。因为所有对ref的赋值和取值拿到的都是最终的状态,并不会因为不同的 render 中存在不同的隔离。

  • 获取 Dom 元素,在 Function Component 中我们可以通过 useRef 来获取对应的 Dom 元素。

1
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。

返回的 ref 对象在组件的整个生命周期内持续存在。

一个常见的用例便是命令式地访问子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM的主要方式。

如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef()ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通Javascript 对象

useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象

useCallback

返回一个memoized回调函数。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

关于使用

在项目中不要随意useCallback,一些场景下,不仅没有提升性能,反而让代码可读性变的很差。

useCallback 可以记住函数,避免函数重复生成,这样函数在传递给子组件时,可以避免子组件重复渲染,提高性能。

但我们要注意,提高性能还必须有另外一个条件,子组件必须使用了 shouldComponentUpdate 或者 React.memo 来忽略同样的参数重复渲染。

useMemo

返回一个memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。

这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。

请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。

useMemo Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。

useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])
  • ref 表示需要被赋值的 ref 对象
  • createHandle 函数的返回值作为 ref.current 的值。
  • deps 依赖数组,依赖发生变化会重新执行 createHandle 函数。

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。

当然,在日常 React 开发中可能会存在这样一种情况。我们希望在父组件中调用子组件的方法,虽然 React 官方并不推荐这样声明式的写法,但是有时候我们不得不这样做。

useReducer

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

useState的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

自定义hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

自定义 Hook 必须以 “use” 开头吗?

必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了Hook的规则

在两个组件中使用相同的 Hook 会共享 state 吗?

不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的

我们可以在一个组件中多次调用 useStateuseEffect,它们是完全独立的。