大家好,我是踩坑小星球。作为一个 Component 拥护者,迈出一小步踩下 Hook ,看看它到底是个啥玩意儿。 基础语法之类的就不说了,文章多的是,这个只是我自己在开发的时候,需要注意的点~ [TOC]
关于状态
useState管理状态,这里有一点需要注意,当你使用 useState 的 set方法的时候,旧状态不会自动 merge到新状态中去,也就是 set 所改变的是全量数据。
这句话的意思就是,如果你的 state 是一个对象,你需要手动的构成一个完整的新数据
// 定义
const [obj,setObj] = useState({name: "小蘑菇",age: 0 });
// 更新
setObj({
...obj,
name: "大蘑菇"
});
// 结果
// {name: "大蘑菇",age: 0 }
-
useState产生的 state 作为函数中的一个常量,就是普通的数据,并不存在诸如数据绑定这样的操作来驱使 DOM 发生更新。在调用 setState 后,React 将重新执行 render 函数,仅此而已。因此,状态也是函数作用域下的普通变量。
我们可以说每次函数执行拥有独立的状态,具有 Capture Value 的特性,简单来说,就是它不能实时。 -
利用
useRef就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
原因是因为,ref.current 将被赋予初始值 initialValue,之后便不再发生变化。但你可以自己去设置它的值。设置它的值不会重新触发 render 函数。
因此useRef除了用于获取dom节点外,还有一个强大的作用就是缓存数据,因为它不会触发函数更新;react-redux源码中的connectAdvanced等模块就用了这一技能
function Example() {
const [count, setCount] = useState(0);
// Initial ref
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
}
生命周期
Component 最大的好处是有生命周期可以处理各种数据,在 Hook 也可以利用Effect钩子模拟这种生命周期
- 通常我们会利用组件的生命周期函数去执行一些副作用,但通常副作用逻辑复杂,可能会有 Bug,所以在 Hook 里,建议
针对每一段逻辑单独使用一个Effect钩子
useEffect(() => {
console.log('模拟componentDidMount');
return () => {
console.log('模拟componentWillUnmount')
}
}, []);
useEffect(() => {
console.log('模拟componentDidMount以及name改变时,模拟componentDidUpdate');
return () => {
console.log('name改变时,都会先触发这里,再触发上面的effect')
}
}, [name]);
-
对于 useEffect 来说,执行的时机是完成所有的 DOM 变更并让浏览器渲染页面后,而 useLayoutEffect 和 class 组件中 componentDidMount, componentDidUpdate一致——在 React 完成 DOM 更新后马上同步调用,会阻塞页面渲染.
-
利用
useMemo减少不必要的渲染
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
特殊场景
- 依赖了某些值,但是不要在初始化就执行回调,要在依赖改变再去执行回调,模拟
componentDidUpdate
const init = useRef(true);
useEffect(() => {
if (init.current) {
init.current = false;
return;
}
// do something...
}, [ xxx ]);
- 循环/判断中使用Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们 官方文档是这么说的。实际情况是,由于Hook是基于链表进行注册的,有一个固定的顺序,如果使用判断或循环,会导致执行顺序不一样。但是,只要能保证执行顺序固定,是可以在条件或循环中使用Hook的。
useEffect(() => {
func1();
func2(a);
}, [a]);
useEffect(() => {
func1();
func2(b);
}, [b]);
useEffect(() => {
func1();
func2(c);
}, [c]);
上述例子,可以发现,重复代码过多,可以使用循环来执行userEffect
const arr = [a,b,c];
for (let i of arr){
useEffect(() => {
func1();
func2(i);
}, [i]);
}
userEffect中使用异步函数
useEffect是不能直接用 async await 语法糖的 若想使用,写成立即执行函数或useEffect中创建异步函数后再执行
useEffect(() => {
// Using an IIFE
(async function asyncFunction() {
await loadContent();
})();
},[]);
useEffect(() => {
const asyncFunction = async () =>{
const data = await loadContent();
setData(data)
}
asyncFunction();
}, []);
注意事项
useLayoutEffect可能会阻塞浏览器的绘制
useEffect执行顺序: 组件更新挂载完成 -> 浏览器dom绘制完成 -> 执行 useEffect 回调
useLayoutEffect执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成- 不要在循环,条件或嵌套函数中调用 Hook ,必须始终在 React 函数的顶层使用 Hook 。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
- 只能在 React 函数式组件或自定义 Hook 中使用 Hook
- 可以安装 eslint 插件
eslint-plugin-react-hooks,在.eslintrc.js中加入配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error"
}
}
- 谨慎将处于同个 useEffect dependences 之中且有逻辑关联的 state 放在多个 useEffect中
- 使用useMemo这种会缓存的API时,需要将其依赖的state值放到第二个参数里,否则可能获取不到最新的state