回到部落格

2023-12-26

【React 新手村】千萬不要這樣使用useState!

【React 新手村】千萬不要這樣使用useState!

原文發表於 Medium

下面的案例有個錯誤,您可以把它找出來嗎?

import React, { useState } from 'react';

function ExampleComponent() {  
  
  const [count, setCount] = useState(0);  
  
  const increment = () => {  
    setCount(count + 1);  
  };  
  
  const decrement = () => {  
    setCount(count - 1);  
  };  
  
  return (  
    <div>  
      <p>Count: {count}</p>  
      <button onClick={increment}>Increment</button>  
      <button onClick={decrement}>Decrement</button>  
    </div>  
  );  
}  
  
export default ExampleComponent;

答案是 setCount(count + 1) 這段寫錯了。

正確使用Setter

對於useState有深入的了解之後,我才知道,使用useState的setter時,若要在當前值的基礎上變更成新值,我們應該要使用下面這個形式

setCount(prevCount => prevCount - 1);

在setter中使用Arrow Function入參,useState會自動把state的當前值再次入參給這個Arrow Function,接著就看我們要return什麼東西了。在這樣的形式中,我們才能真正確認setter抓到的值是真的當前值。

但很多網路上的教學或者一些文章中使用useState都不是使用這樣的形式,我個人猜想可能是因為方便,因為若不是在複雜、頻繁地變換state之下,基本上不會有問題。(例如在這個案例中,這個錯誤就不會造成問題。)
但不得不說,這確實是個隱憂,可能會在未來成為一個不定時炸彈。下面就給各位一個使用錯誤的useState setter時會出錯的案例:

import React, { useState, useEffect } from "react";

function Counter() {  
  const [count, setCount] = useState(0);  
  
//不正確的按鈕  
  const incrementIncorrect = () => {  
    setCount(count + 1);  
    setCount(count + 1);  
  };  
  
//正確的按鈕  
  const incrementCorrect = () => {  
    setCount((prevCount) => prevCount + 1);  
    setCount((prevCount) => prevCount + 1);  
  };  
  
  return (  
    <div>  
      <p>Count: {count}</p>  
      <button onClick={incrementIncorrect}>不正確按鈕</button>  
      <button onClick={incrementCorrect}>正確按鈕</button>  
    </div>  
  );  
}  
  
export default Counter;

在這個組件中,我們設定了兩個按鈕,分別綁定了正確與不正確使用useState的handler。乍看之下,兩個按鈕按了都會讓count值加2,但實際上,不正確的按鈕只會加1,而正確的按鈕才是加2。

哪個當前值?

會出現這樣的Bug,是因為我們使用不正確的用法時,setter會把state當作從外部來的值,若要針對state當前值做連續操作時,每一段操作都只會抓到函式創建時的值,而忽略掉執行時的各項變化過後的值;而若是使用正確的Arrow Function入參的方法,我們就可以針對useState內部的真實當前值進行操作。我們用下方的code來說明更容易理解:

const incrementIncorrect = () => {
setCount(count + 1); //0+1
setCount(count + 1); //0+1
setCount(count + 1); //0+1
setCount(count + 1); //0+1
setCount(count + 1); //0+1
//count值以最後一個setter為準,因此無論使用多少次steCount,得到的新值還是1。

  };  
  
const incrementCorrect = () => {  
    setCount((prevCount) => prevCount + 1); //0+1  
    setCount((prevCount) => prevCount + 1); //1+1  
    //count值以最後一個setter為準,因此是2。  
  };

這邊跟我過去談過的閉包概念有關。在使用handler時,都會形成閉包,當我們使用錯誤的方法時,閉包就會把count的值先「包」起來,所以count從閉包創立之後就不會變了。然而在使用Arrow Function時,我們並沒有把count值包死,而且透過內部的入參,將真正的當前值帶入到每次的setter呼叫中。(傳送門:閉包的概念【 JS小劇場 】-Closure

為了避免在未來踩到坑,我們還是堅持用Arrow Function入參,透過正確的方式來操作useState,會是比較保險的方法!若有補充或指正的知識,還請各位先進在留言區指教唷!

希望大家在code路上天天有所得,我們一起成長並進!