2023ItIron 30天React練功坊-攻克常見實務/面試問題 Day2: setState isn’t working correctly when called multiple times

tags: ItIron2023 react

前言

昨天我們用了一個非常基本的問題了解了關於react的render機制以及useState可能會碰到的一個小問題,這幾天我們會暫時離不開這個hook,馬上來看一下今天的問題吧!
順帶一提,我會盡可能地讓題目難度隨著時間遞增,所以如果你覺得這玩意真的他媽太簡單了,可以試著過幾天再回來追這系列文!

本日題目

馬上就開始吧,請觀察這個codesandbox內的程式碼。

今天我們因為某種原因,期望點擊按鈕後一口氣呼叫5次的setCount讓count的值遞增5,但實際上的執行結果卻出乎我們意料,每一次的點擊都只讓count增加1,仿佛我們只寫了一個setCount一般,你是否能解釋並修復這個異常,讓點擊後順利的遞增我們setCount的次數呢?

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

解答與基本解釋

看起來雖然是個簡單的useState問題,但也許答案並不是你想得那樣,我之前在碰到人向我提問這類問題時,他們一個很常見的誤解就是以為這是因為setState是非同步行為,實際上問題並沒有這麼玄妙,setState也100%是個同步的操作,僅是你對react渲染仍不夠瞭解而已!

最根本就根本的原因其實在於state在每一次的render其實都是維持相同的值(簡單說是因為在closure內),什麼? 你聽不懂? 加上log我想你就會清楚一些了。

const handleClick = () => {
  setCount(count + 1);
  console.log('count1', count) // count1 0
  setCount(count + 1);
  console.log('count2', count) // count2 0
  setCount(count + 1);
  console.log('count3', count) // count3 0 
  setCount(count + 1);
  console.log('count4', count) // count4 0 
  setCount(count + 1);
  console.log('count5', count) // count5 0 
};

注意到了嗎? 雖然你call setState了,但當下印出每次的值都是初始值0,這下你理解了,實際上上方的程式碼與下方的等價

const handleClick = () => {
  setCount(0 + 1); 
  setCount(0 + 1);
  setCount(0 + 1);
  setCount(0 + 1);
  setCount(0 + 1);
};

在下次render前,你count的值都不會有任何變動,值本身是immutable的。 既然當下的值是0,你每次去設0+1自然結果都會是1囉!了解原因後我想你就比較好辦了,我們要做的就是利用前一次的count值當基礎再加上1就行了,因此改為callback的形式就會一切正常囉!

const handleClick = () => {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
};

在這樣的情況下,每一次都會以前一次的count值作為基礎再加上1,另一方面react也會batch這些setState的操作,讓他們能在下一次的rerender前完成,最終就是一口氣看到5囉!

總結

今天我們稍微更深入的探討了react的render行為以及常見的誤解,在初學看來也許會覺得react有些不合理(事實也的確如此),但這就是react底層的運作機制,隨著題目繼續進行下去相信你對於整個渲染行為會有更深的理解,未來遇到類似的問題也會較為容易切入,我們明天見!

發表留言