iT邦幫忙

2021 iThome 鐵人賽

DAY 2
2
Modern Web

用30天更加認識 React.js 這個好朋友系列 第 2

Day2-React Hook 篇-認識 useState

  • 分享至 

  • xImage
  •  

今天我們來認識一個相當常使用的 hook: useState。

語法

const [currentValue, setCurrentValue] = useState(initialValue);
  • currentValue 是存放 state 的值
  • setCurrentValue 是用來設定 state 值
  • initialValue 是 state 的初始值

語法相當的簡單,其概念源自於解構賦值,我們把範例的 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>
  );
}

特性

1. 更新 state 時會 re-render 元件

注意: 更新 state 要使用 immutable 的寫法。

參考 為什麼更新 React 中的 state 要用 immutable 的寫法? 什麼是 immutable? 該如何寫才會是 immutable?

2. 不會立即更新,像非同步但又不是 Javascript Call stack Event loop 的那種非同步

由於 react 有 state batch update 的特性,也就是在多次觸發同步事件去更新 state 時,會合併成一次的更新,元件只會重新渲染一次,減少了不必要的渲染。

根據此點特性,state 是透過 batching 去更新值,因此設定新的值給 state 後,馬上 console 印出來的值還是更新前的值。

以下範例中,點擊按鈕一下有三個 state 會更新,但只會有一次 re-render,這個就是 state batch update 的特性所導致。
state batch update 範例

那如果要馬上取得更新後的值怎麼做?

解法 1: 多建一個 useEffect

這篇 Andy Chang 大大寫的文章中有提到範例:
https://ithelp.ithome.com.tw/articles/10257994

解法 2: 使用 Custom hook useStateRef

這個還挺好用的,state 結合 ref,馬上就取得更新後的 state 值。
useStateRef(npm 網站)

透過 flushSync() 避免 state batch 更新

這裡補充一段 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();

其他知識

補充 1. setCount(count + 1) 和 setCount(prev => prev + 1) 的不同

兩者差異在 React 的新版文件說明得很清楚,前者因為 state batch update 的特性,多個 state 集中處理後再重新渲染,count 在該次 render 的值永遠都還是 0,所以最後加完還是 1,後者是取更新後的 state 繼續加,所以會出現 3。

程式碼範例(codesandbox)

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,也都是採用後者方式更新

補充 2. 2023/1/4 補充-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 的更新是透過 Queue 的資料結構去做處理的

補充 3. 撰寫 state 的原則

讀者有沒有想過 state 要怎麼寫會比較好維護?比較好讀懂?

所以這裡就來補充一下撰寫 state 的幾個要注意的點:

1. 將類似性質的 state 合併成一個 state,集中管理

例如我們將 x, y 合併成一個 state 代表 position,就不用兩個 state 了。

const [position, setPosition] = useState({ x: 0, y: 0 });

2. 避免同樣的資料在不同的 state 中都出現,可能會導致更新不同步

例如我們可以透過 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;

3. 避免重複的 state

例如其實不需要多加一個 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]);
}

4. 因為 state 要避免 mutation,而過度巢狀的 state,會不好做更新,所以可以的話盡量避免

補充 4. 2023/5/24 補充-useState 使用 hashMap 資料結構儲存的處理

初始化:
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'))
  }

補充 5. 2023/05/31 補充-陣列和物件的 state 更新

這兩者的更新在 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>
    </>
  );
}

上一篇
Day1-鐵人賽大綱 & 為什麼要使用 Hook?
下一篇
Day3-React Hook 篇-認識 useEffect
系列文
用30天更加認識 React.js 這個好朋友33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言