避免React中的Hooks闭包陷阱
在 React 中使用 Hooks 是一种强大而灵活的方式来管理组件的状态和副作用。然而,当使用 useState 和 useEffect 时,我们必须小心处理闭包陷阱,以避免出现意外的行为。
什么是闭包陷阱?
闭包是指一个函数可以访问其词法作用域之外的变量。在 React 中,当在 useEffect 或 useState 的回调函数中引用状态或 props 时,就会创建闭包。如果不小心处理,闭包可能导致意外的行为,甚至成为 bug 的源头。
React Hooks 中的闭包陷阱主要会发生以下几种情况:
- 在 useState 中的闭包陷阱
- 在 useEffect 中的闭包陷阱
- 在 useCallback 中的闭包陷阱
useState 陷阱
1. 陷阱:【异步陷阱】
export default function Hooks() {
const [count, setCount] = useState(0);
const add = () => {
setCount( count + 1 );
console.log(count, '修改前的值');
}
return (
<div>
<span>{count}</span>
<button onClick={ add }> + </button>
</div>
);
}
出现的问题:
- 点击添加按钮,发现值更新了,打印的值却还是上次的
- useState 修改状态 是异步执行无法获取更新后的值
解决方案:
- 所以我们不能修改后,把值拿去其他操作 (应该拿 count+1)
- 可以通过useRef 获取最新值
修改后代码:
export default function Hooks() {
const [count, setCount] = useState(0);
const countRef = useRef()
countRef.current = count
const add = () => {
setCount( count => count + 1 );
setTimeout(() => { // 模拟异步请求
console.log(count, '修改前的值');
console.log(countRef.current, '修改后的值');
}, 0)
}
return (
<div>
<span>{count}</span>
<button onClick={ add }> + </button>
</div>
);
}
2. 陷阱:【只更新最后1个】
传入一个基于状态的值
import React, { useState } from 'react'
export default function Hooks() {
const [count, setCount] = useState(0);
const add = () => {
console.log("value1: ", count)
setCount( count + 1 );
console.log("value2: ", count)
setCount( count + 2 );
console.log("value3: ", count)
setCount( count + 3 );
console.log("value4: ", count)
}
return (
<div>
<span>{count}</span>
<button onClick={ add }> + </button>
</div>
);
}
- 打印可以看出如果我们传入的是一个普通值,他只会进行最后一次更新
传入一个函数
const add = () => {
console.log("value1: ", count)
setCount( count => count + 1 );
console.log("value2: ", count)
setCount( count => count + 2 );
console.log("value3: ", count)
setCount( count => count + 3 );
console.log("value4: ", count)
}
- 可以看出,传入一个函数的话,它会进行两次赋值,因为它更新的值是基于之前的值来执行,所以在开 发中推荐使用函数传入的形式进行修改
useEffect 陷阱
1. 陷阱:【过期闭包】
import React, { useState, useEffect } from 'react'
export default function Hooks3() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1)
}, 1000)
}, [])
useEffect(()=>{
setInterval(() => {
console.log(`Count: ${count}`)
}, 1000);
}, []);
return (
<div>
<span>{count}</span>
</div>
);
}
- 页面上count一直显示1
- useEffect的第二个参数为空数组,所以只会在组件加载后仅执行一次,我们知道组件每次render的
时候都会生成一个新的state对象,对应一个快照,上述代码中,因为useEffect只执行了一次,所
以定时器中的 count 一直是最初快照里的 count ,那么页面中 count 的显示肯定不会改变 - 闭包陷阱产生的原因就是 useEffect 的函数里引用了某个 state,形成了闭包(也有叫过时的闭包)
2. 解决方法
- useEffect第二个参数添加依赖项count ,
- 并且每次更新,添加计时器,清除计时器
解决后代码:
import React, { useState, useEffect } from 'react'
export default function Hooks() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [count])
useEffect(()=>{
const timer = setInterval(() => {
console.log(`Count: ${count}`)
}, 1000);
return () => clearInterval(timer)
}, [count]);
return (
<div>
<span>{count}</span>
</div>
);
}
扩展知识:
- 使用 useEffect 时,若有多个副作用,则应该调用多个 useEffect ,而不是写在一个里面;
- useEffect 第一个参数可以返回一个函数,这个函数会在组件卸载时(也就是render了,生成新的快照时)执行,可以用来清除副作用里的操作;
useCallback 陷阱
- useCallback 本来拿来优化性能,当依赖变化不用重新注册该函数
- 使用不当也会,出现一定的问题
1. 陷阱:【获取父组件的值,不是最新】
import React, { useCallback, useState } from 'react'
function Child(props) {
let log = useCallback(() => {
console.log(props.count, '子组件打印的props')
}, [])
return (
<>
count: {props.count}
<button onClick={() => log()}> 打印 </button>
</>
);
}
export default function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count+1)}> +1 </button>
<Child count={count} />
</>
);
}
- 此时我们在 父组件点击 增加按钮
- 子组件的 count 发生改变 ,我们在点击打印按钮,发现count 一直是0
- 说明useCallback 依赖为[]数组,取到count 已经过期了
2. 解决方法
- 给useCallback第二个参数添加依赖的数据,依赖变化了函数会重新生成
let log = useCallback(() => {
console.log(props.count, '子组件打印的props')
}, [props.count])
总结
在 React 中,使用 Hooks 是一种高效和灵活的方式来管理状态和副作用。但是,在使用 useState 和 useEffect 时,务必小心处理闭包陷阱。通过使用回调函数更新状态,以及在 useEffect 中正确地处理依赖项,可以有效地避免闭包陷阱,确保组件行为的可靠性和一致性。