一、hook概念
Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。
二、常用hook
1、useState
2、useEffect
3、useCallback/useMemo
4、useRef
5、useReducer
6、useContext
三、useState
1、概念:
useState让我们能在函数式组件中定义并使用state。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
(1)参数:useState() 方法里面唯一的参数就是初始 state。
(2)返回值:当前 state 以及更新 state 的函数。
setCount 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
(3)state的更新是直接替换,而不是类组件的合并方式;
(4) state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
2、惰性初始 state
一个常见的使用场景是当创建初始 state 很昂贵时(大量计算):
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用(因为是直接执行函数createRows(props.count))
const [rows, setRows] = useState(createRows(props.count));
// ...
}
为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
四、useEffect
1、概念:
Effect Hook可以让你在函数组件中执行副作用操作,比如数据获取,设置订阅以及手动更改 React 组件中的 DOM等。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
You clicked {count} times
);
}
2、清除副作用
通过effect 返回一个函数实现清除,它会在调用一个新的 effect 之前对前一个 effect 进行清理。
在这个地方可以看到hooks一个小优点,就是我们的监听和销毁写到同一个effect,实现了逻辑的集中处理;我们可以定义多个effect来分类处理不同的副作用。
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
});
3、通过跳过 Effect 进行性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们会通过componentDidUpdate添加逻辑比较来跳过不必要的更新,在effect中只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
4、处理特殊使用场景
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
(1)如果我们频繁修改 count,每次执行 Effect,上一次的计时器被清除,需要调用 setInterval 重新进入时间队列,实际的定期时间被延后,甚至有可能根本没有机会被执行。
(2)由于(1)中问题,我们使组件只在挂载时运行一次(即依赖数组置为空),Effect 中明明依赖了count,但我们撒谎说它没有依赖,那么当 setInterval 回调函数执行时,获取到的 count 值永远为 0。
解决这个问题有以下几种方案:
(1)状态变更时,应该通过 setState 的函数形式来代替直接获取当前状态。
setCount(c => c + 1);
(2)在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。
(3)万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。
const [count, setCount] = useState(0);
const countRef = useRef();
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
(4)另外的情况是,Effects 依赖了函数或者其他引用类型。与原始数据类型不同的是,在未优化的情况下,每次 render 函数调用时,因为对这些内容的重新创建,其值总是发生了变化,导致 Effects 在使用 deps 的情况下依然会频繁被调用。
这个问题的解决方案就是我们后续会聊到的useCallback/useMemo,等一下就带着这个问题去看它们。
五、useCallback/useMemo
1、概念:
官方介绍:把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
一个是「缓存函数」, 一个是缓存「函数的返回值」,用于保证依赖没有变化的情况下引用类型数据地址不变,减少重新渲染。
2、memoized函数实现原理:
使用一组参数初次调用函数时,缓存参数和计算结果,当再次使用相同的参数调用该函数时,直接返回相应的缓存结果。
const memoize = fn => {
const cache = new Map();
const cached = function(val) {
return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
};
cached.cache = cache; return cached;
};
3、何时使用useCallback
(1)在组件内部,那些会成为其他useEffect依赖项的方法,建议用 useCallback 包裹,或者直接编写在引用它的useEffect中。
(2)己所不欲勿施于人,如果你的function会作为props传递给子组件,请一定要使用 useCallback 包裹。
(3)实践--测量 DOM 节点
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
Hello, world
The above header is {Math.round(height)}px tall
>
);
}
注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
4、useMemo使用场景:
(1)缓存一些相对耗时的计算;
(2)优化子组件;对于对象和数组,如果某个子组件使用了它作为 props,减少它的重新生成,就能避免子组件不必要的重复渲染,提升性能。
未优化的代码如下:
const data = { id };
return ;
此时,每当父组件需要 render 时,子组件也会执行 render。如果使用 useMemo 对 data 进行优化:
const data = useMemo(() => ({ id }), [id]);
return ;
(3)避免一部分elements的重复渲染
在过去的 class 组件中,我们通过 shouldComponentUpdate 判断当前属性和状态是否和上一次的相同,来避免组件不必要的更新。其中的比较是对于本组件的所有属性和状态而言的,无法根据 shouldComponentUpdate 的返回值来使该组件一部分 elements 更新,另一部分不更新。
为了进一步优化性能,我们会对大组件进行拆分,拆分出的小组件只关心其中一部分属性,从而有更多的机会不去更新。
function Example(props) {
const [count, setCount] = useState(0);
const [foo] = useState("foo");
const main = useMemo(() => (
), [foo]);
return (
{count}
{main}
);
}
六、useRef
1、概念:
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
2、通常情况下,useRef有两种用途:
(1)访问DOM节点,或者React元素
(2)保持可变变量,其类似于在 class 中的 this
const [count, setCount] = useState(0);
const countRef = useRef();
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
3、避免滥用 refs
当 useEffect 的依赖频繁变化,你可能想到把频繁变化的值用 ref 保存起来。然而,useReducer 可能是更好的解决方式:使用 dispatch 消除对一些状态的依赖。官网的 FAQ 有详细的解释。
最终可以总结出这样的实践:
(1)useEffect 对于函数依赖,尝试将该函数放置在 effect 内,或者使用 useCallback 包裹;
(2)useEffect/useCallback/useMemo,对于 state 或者其他属性的依赖,根据 eslint 的提示填入 deps;
(3)如果不直接使用 state,只是想修改 state,用 setState 的函数入参方式(setState(c => c + 1))代替;
(4)如果修改 state 的过程依赖了其他属性,尝试将 state 和属性聚合,改写成 useReducer 的形式。
当这些方法都不奏效,使用 ref,但是依然要谨慎操作。
七、useReducer
1、useState 的替代方案,类似Redux。
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment': return {count: state.count + 1};
case 'decrement': return {count: state.count - 1};
default: throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
2、useReducer能解决的问题:
(1)把 state 更新逻辑移到 effect 之外,移除了依赖项,又能保证数据的准确性;
(2)使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
八、useContext
1、概念:
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定。
调用了 useContext 的组件总会在 context 值变化时重新渲染。
2、使用useContext和useReducer避免向下传递回调
我们已经发现大部分人并不喜欢在组件树的每一层手动传递回调。
在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch 函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化
const [todos, dispatch] = useReducer(todosReducer);
return (
);
}
TodosApp 内部组件树里的任何子节点都可以使用 dispatch 函数来向上传递 actions 到 TodosApp:
function DeepChild(props) {
// 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
);
}
九、自定义HOOK
1、自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
自定义 Hook 是一种重用状态逻辑的机制。在此我们以自定义一个 "获取上一轮的props或state的hook" 为例。
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
2、使用自定义hook
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return Now: {count}, before: {prevCount}
;
}
十、规则和注意事项
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则:
1、只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
2、只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
- ✅ 在 React 的函数组件中调用 Hook。
- ✅ 在自定义 Hook 中调用其他 Hook。
React 是如何把对 Hook 的调用和组件联系起来的?
React 保持对当前渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。
引用文献
React官方文档
React Hooks 最佳实践
写React Hooks前必读
React Hooks 最佳实践