以定时器为代表的问题
相信很多人都遇见过以下代码这种问题。无论点击多少次,定时器里面输出的count永远是0。这是因为下面示例代码中useEffect仅在首次执行,而useEffect也是一个闭包。所以虽然setCount更新了count的值,并且页面刷新了,但是useEffect里面的count还是原来的count。
function App() {
const [count, setCount] = useState<number>(0);
useEffect(() => {
setInterval(() => {
console.log('count', count);
}, 1000)
}, [])
return (
<div>
{count}
<div onClick={() => setCount(count + 1)}>点击count+1</div>
</div>
);
}
那么我们如何解决呢?
- 我们可以使用useRef
- 可以使用hook版本的定时器比如ahook的useInterval
第二种我们就不过多介绍了,根据文档直接使用就行。 下面我们使用useRef每次返回的都是相同的引用解决这个问题:
function App() {
const [count, setCount] = useState<number>(0);
const countRef = useRef<number>(count);
useEffect(() => {
countRef.current = count;
}, [count])
useEffect(() => {
setInterval(() => {
console.log('count', countRef.current);
}, 1000)
}, [])
}
然后我们进行测试,发现打印结果已经正常。虽然useRef可以解决这个问题,但是我还是推荐大家使用ahook封装好的。
setState之后调用函数无法获取刚刚设置最新的state值。
比如以下例子,用户触发点击事件,我更新了一个state,紧接着调用了一个函数,输出这个state。
function App() {
const [count, setCount] = useState<number>(0);
const logCount = () => {
console.log('count', count);
}
return (
<div>
{count}
<div onClick={() => {
setCount(count + 1);
logCount();
}}>点击count+1
</div>
</div>
);
}
我们发现每次输出的结果,都是上一次的副本。达不到我们的期望。
为什么会出现这种情况?
如果理解的hook的执行顺序,这个问题就很理解。
触发点击事件的时候先执行点击事件的内部逻辑setCount(count + 1); logCount();所以先打印了count 0 这时候的count是原来的count,然后从上开始执行, count进行了更新,logCount函数里面的count也会更新成新的count,然后render渲染页面,然后触发useEffect。
这样我们是不是知道问题所在了,打印出现在了赋值之前。所以每次count打印的都是上一次的副本。
我们可以debug调试一下,看一下执行顺序
通过debug我们可以明显的看的这个发生这个问题的原因和执行顺序,找到了原因,我们就可以解决问题了。
- 使用useRef + setTimeout,这个方案不推荐,因为我也没测试过会不会出现异常情况。仅仅是为了更好的理解hook的执行顺序。
以下是示例代码:
function App() {
const [count, setCount] = useState<number>(0);
const countRef = useRef<number>(count);
useEffect(() => {
countRef.current = count;
}, [count])
const logCount = () => {
setTimeout(() => {
console.log('count:', countRef.current);
}, 0)
}
return (
<div>
{count}
<div onClick={() => {
setCount(count + 1);
logCount();
}}>点击count+1
</div>
</div>
);
}
可以自己debug一一下,就能更清楚的理解react hook的执行顺序。
使用useEffect解决
function App() {
const [count, setCount] = useState<number>(0);
const [flag, setFlag] = useState<boolean>(false);
useEffect(() => {
if (flag) {
console.log('count:',count);
setFlag(false);
}
}, [flag])
return (
<div>
{count}
<div onClick={() => {
setCount(count + 1);
setFlag(true);
}}>点击count+1
</div>
</div>
);
}
我们发现虽然实现了效果,但并不是我们想要的效果,我们之前是一个函数,现在变成了useEffect的副作用。所以我们封装一下变成一个hook方便我们调用。
const useSyncCallback = (callback: Function) => {
const [flag, setFlag] = useState(false);
const fn = useCallback(() => {
setFlag(true);
}, [])
useEffect(() => {
if (flag) {
callback && callback();
setFlag(false)
}
}, [flag])
return fn
}
然后再改造一下原来的代码
function App() {
const [count, setCount] = useState<number>(0);
const logCount = useSyncCallback(()=>{
console.log('count', count);
})
return (
<div>
{count}
<div onClick={() => {
setCount(count + 1);
logCount();
}}>点击count+1
</div>
</div>
);
}
这样写也有弊端。 每次调用
logCount()都会触发useEffect没有依赖的场景,而且也会多余的触发一次rander。
其实更多的应该是避免这样的形式,我们应该思考,为什么会出现改变一个值,然后再去调用一个函数。大部分这种情况都是可以避免的。比如我已经明确的知道点击的时候需要count+1。那我们完全可以把count+1赋值为一个新变量在logCount中使用。setCount仅仅是为了渲染页面新的count。类似下面这样:
function App() {
const [count, setCount] = useState<number>(0);
const logCount = ()=>{
const newCount = count + 1;
setCount(newCount);
console.log('count:',newCount);
}
return (
<div>
{count}
<div onClick={() => {
logCount();
}}>点击count+1
</div>
</div>
);
}
本篇文章仅作为分享,总之想要学好hook一定要明白执行的顺序和触发的时机,这样才能更好的架构项目。多用debug调试,你会发现很多之前不理解的问题突然就豁然开朗。