今天我們來認識一個相當常使用的 hook: useState。
const [currentValue, setCurrentValue] = useState(initialValue);
語法相當的簡單,其概念源自於解構賦值,我們把範例的 useState(0) 印出來看,可以看到 useState 回傳一個陣列,第一個參數是 state 的初始值,第二個名字叫 dispatchAction,意思可以想到是去修改 state 的函式。

import { useState } from 'react';
export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(pre => pre + 1)}>
        Click me
      </button>
    </div>
  );
}
注意: 更新 state 要使用 immutable 的寫法。
參考 為什麼更新 React 中的 state 要用 immutable 的寫法? 什麼是 immutable? 該如何寫才會是 immutable?
由於 react 有 state batch update 的特性,也就是在多次觸發同步事件去更新 state 時,會合併成一次的更新,元件只會重新渲染一次,減少了不必要的渲染。
根據此點特性,state 是透過 batching 去更新值,因此設定新的值給 state 後,馬上 console 印出來的值還是更新前的值。
以下範例中,點擊按鈕一下有三個 state 會更新,但只會有一次 re-render,這個就是 state batch update 的特性所導致。
state batch update 範例
那如果要馬上取得更新後的值怎麼做?
這篇 Andy Chang 大大寫的文章中有提到範例:
https://ithelp.ithome.com.tw/articles/10257994
這個還挺好用的,state 結合 ref,馬上就取得更新後的 state 值。
useStateRef(npm 網站)
這裡補充一段 React V18 新出的 API flushSync(),透過它可以解除 state 的 batch update,如有特殊的情境需求可以使用。
例如以下程式碼,在呼叫 api 後 Promise resolve 後呼叫兩個 setState 函式,不過透過 flushSync() 的作用,它們就不會進行批次渲染了。
const onFetchSomeData = () => {
  axios.get(...).then((res) => {
    ReactDOM.flushSync(() => {
      setData(res.data); // 立刻重渲染
      setFlag((f) => !f); // 立刻重渲染
    });
  });
}
另一個應用的例子是假如我們在 todolist 中新增一個 todo,功能是希望能夠滑到 todolist 的最底部,此時為了及時取到更新的 todo 才能順利的移到底部的話,就可以使用 flushSync。
flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
兩者差異在 React 的新版文件說明得很清楚,前者因為 state batch update 的特性,多個 state 集中處理後再重新渲染,count 在該次 render 的值永遠都還是 0,所以最後加完還是 1,後者是取更新後的 state 繼續加,所以會出現 3。
export default function App() {
  const [count1, setCount1] = useState(0);
  useEffect(() => {
    setCount1(count1 + 1);
    setCount1(count1 + 1);
    setCount1(count1 + 1);
  }, []);
  const [count2, setCount2] = useState(0);
  useEffect(() => {
    setCount2((prev) => prev + 1);
    setCount2((prev) => prev + 1);
    setCount2((prev) => prev + 1);
  }, []);
  return (
    <div>
      Current count1: {count1}
      <!-- 1 -->
      <br />
      Current count2: {count2}
      <!-- 3 -->
    </div>
  );
}
在更新物件、陣列型別的 state,也都是採用後者方式更新
setCount(prev => prev + 1) 背後的原理在上面的程式碼中,有段:
useEffect(() => {
  setCount2((prev) => prev + 1);
  setCount2((prev) => prev + 1);
  setCount2((prev) => prev + 1);
}, []);
可以看到每個 setState 函式內都有一個函式 prev => prev + 1,在 React 的底層運行中,這些函式都會被加入到 Queue 的資料結構中,所以你可以想像 Queue 裡面有三個函式等待執行,第一個函式執行完後,回傳的值會再丟給它下一個函式做為 prev 傳入去 + 1,最終結果就是 3。
再看看另一種情況: 假設初始 count = 0,點按鈕會結果會是?
<button onClick={() => {
  setCount(n + 5);
  setCount(n => n + 1);
}}>Increase the number</button>
這裡一樣 React 底層會設定 Queue 去儲存: [count + 5] => [(count + 5) => (count + 5) + 1],也是一樣的概念,第二個 setCount 取的函式參數為上一個 setCount 的回傳值,所以可以將 n 看作 count + 5,所以最終結果就是 6。
讀者有沒有想過 state 要怎麼寫會比較好維護?比較好讀懂?
所以這裡就來補充一下撰寫 state 的幾個要注意的點:
例如我們將 x, y 合併成一個 state 代表 position,就不用兩個 state 了。
const [position, setPosition] = useState({ x: 0, y: 0 });
例如我們可以透過 firstName 和 lastName 去組成 fullName,所以就不用再多新增該 state。
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 不必要: 多餘的 state 和 useEffect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// 直接組成即可
const fullName = firstName + ' ' + lastName;
例如其實不需要多加一個 state 去儲存被選中的物品,可以改成用 id 去儲存。
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);
// 調整後
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
以下的範例也出現多餘的 state 和 useEffect,直接用一個變數儲存即可。
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // 不必要
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);
  // 較好的做法
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
}
初始化:const [someMap, setSomeMap] = useState(new Map());
更新:setSomeMap(new Map(someMap.set('someKey', 'a new value')));
redux 更新範例:
case 'SomeAction':
  return {
    ...state,
    yourMap: new Map(state.yourMap.set('someKey', 'a new value'))
  }
這兩者的更新在 React 官網都寫蠻清楚的:
https://react.dev/learn/updating-objects-in-state
https://react.dev/learn/updating-arrays-in-state
const initialState = [
  {id: 1, name: 'Alice', country: 'Austria'},
  {id: 2, name: 'Bob', country: 'Belgium'},
];
const [employees, setEmployees] = useState(initialState);
// ✅ Add an object to a state array
const addObjectToArray = obj => {
  setEmployees(current => [...current, obj]);
};
// ✅ Update one or more objects in a state array
const updateObjectInArray = () => {
  setEmployees(current =>
    current.map(obj => {
      if (obj.id === 2) {
        return {...obj, name: 'Sophia', country: 'Sweden'};
      }
      return obj;
    }),
  );
};
// ✅ Remove one or more objects from state array
const removeObjectFromArray = () => {
  setEmployees(current =>
    current.filter(obj => {
      return obj.id !== 2;
    }),
  );
};

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
    }
  });
  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }
  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }
  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
    </>
  );
}
具體的情況是這樣的,以下做了兩個情況的測試:
第一個測試:
useState 定義了一個狀態,初始值是字串 "1"。"1"。第二個測試:
"1",然後我在按鈕點擊時,每次都把它設置為 "2"。"2",之後如果狀態已經是 "2" 了,就不該再渲染。"1" → "2" 的轉變時,渲染了 兩次,然後之後才不再渲染。為什麼會有這樣的行為,可參考以下資料:
Bug: SetState with same value, rerenders one more time #28779
useState not bailing out when state does not change
練習題:22. useState()