iT邦幫忙

2023 iThome 鐵人賽

DAY 28
0
Modern Web

react 學習記錄系列 第 28

[Day28]我的 react 學習記錄 - immer

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單介紹 immer。


Immer

在 react 專案裡面我們會希望可以確保所有狀態都是 immutable 的,盡量去避免直接操作現有的變數,如果有需要更改狀態,就重新建立一個新的 array 或是 object。

因為在 react 裡面大多數的比較跟判斷都是 Object.is() 來執行的,如果直接用 someArray.push() 或是 someObject.key = "value" 的方式來修改變數的話有可能會造成 react 沒有正確更新畫面,造成錯誤,所以我們會盡量確保所有的變數跟改動都是 immutable 的。

但是當遇到較為複雜的狀態時 immutable 真的很麻煩,而且非常影響程式碼的可讀性。

用 immer 官方文件的範例來看,假設有一個 array 的 todo list,裡面放著項目的 title 跟是否已經達成的狀態。

const baseState = [
  {
    title: "Learn TypeScript",
    done: true,
  },
  {
    title: "Try Immer",
    done: false,
  },
];
function App() {
  const [state, setState] = useState(baseState);

  return (
    <div>
      <h1>Immer</h1>
      <ul>
        {state.map(({ title, done }) => (
          <li key={title}>
            <p>title:{title}</p>
            <p>status: {done ? "done" : "idle"}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

在觸發某個事件的時候,需要新增一個項目並且把其中一個既有項目的達成狀態更改為 true。

function App() {
  const [state, setState] = useState(baseState);

  function handleState() {
    const nextState = state.slice(); // 透過 slice 進行第一層淺拷貝
    // 透過展開運算符更新內部物件。
    nextState[1] = {
      ...nextState[1],
      done: true,
    };
    // 在後面新增物件
    nextState.push({ title: "Tweet about it", done: false });
    // 更新 state
    setState(nextState);
  }
  
  return (
    <div>
      <h1>Immer</h1>
      <button onClick={handleState}>change</button>

      <ul>
        {state.map(({ title, done }) => (
          <li key={title}>
            <p>title:{title}</p>
            <p>status: {done ? "done" : "idle"}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

因為我的 state 是一個 array,所以在進行 setState 要特別注意是不是回傳一個新的 array,以及裡面有修改到的東西是不是回傳一個新的 array 或 object。

immer-1

為了確保我的 todo list 是 immutable 的,必須要像上面一樣經過非常多的步驟,來一個個的更新 state 裡面的 array 或是 object。

雖然在使用 slice() 進行淺拷貝的時候就可以確定元件會進行 re-render 了,即使裡面的物件沒有透過 ... 來進行處理也沒關係,但是如果我把 todo 的資料透過 props 傳到子元件時,然後子元件又被 memo 進行處理的時候就有可能發生沒有 re-render 的情形。

type Props = { data: { title: string; done: boolean } };

const Item = memo(function Item({ data }: Props) {
  return (
    <>
      <p>title:{data.title}</p>
      <p>status: {data.done ? "done" : "idle"}</p>
    </>
  );
});

function App() {
  const [state, setState] = useState(baseState);

  function handleState() {
    const nextState = state.slice(); // 透過 slice 進行第一層淺拷貝
    // 直接修改 nextState[1] 的 done
    nextState[1].done = true;
    // 在後面新增物件
    nextState.push({ title: "Tweet about it", done: false });
    // 更新 state
    setState(nextState);
  }

  return (
    <div>
      <h1>Immer</h1>
      <button onClick={handleState}>change</button>
      <ul>
        {state.map((data) => (
          <Item key={data.title} data={data} />
        ))}
      </ul>
    </div>
  );
}

在這裡我把 todo list 的整份資料都傳遞到子元件,並且在 handleState 裡面透過 mutate 的方式直接修改 todo 裡面 index 為 1 的狀態。

immer-2

會發現第三項 Tweet about it 有出現在畫面上,代表狀態有成功更新觸發 re-render,但是第二項 Try Immer 的 status 並沒有更新,因為當我們用 mutable 的方法更新資料時 react 用 object.is() 在比較時會認為是相同的資料,所以就不會對 <Item /> 進行 re-render,導致畫面顯示的結果是錯誤的。

我們可以使用 immer 透過簡單的語法來協助我們做到 immutable 的狀態管理。


Install

npm install immer
or
yarn add immer


Syntax

const nextState = produce(baseState, recipe: (draftState) => void): nextState

使用 immer 時絕大多數的情況下只會使用 immer 所提供的 produce function,produce 接收兩個參數 baseStaterecipe

baseState: 一個你希望它保持 immutable 的東西,通常是一個較為複雜的 array 或是 object。

recipe: 一個 function,這個 function 會接收到一個 draftState 參數,我們要把這個 draftState 當作我們的 baseState 來使用,當我們透過 mutate 的方式來改變這個 draftState 的時候,immer 會記錄下我們所有的改動,並且在最後回傳一個新的 value 給我們,而不改變本來的 baseState

講完語法,就來看看 immer 要怎麼使用吧。

import { useState } from "react";
import { produce } from "immer";

const baseState = [
  {
    title: "Learn TypeScript",
    done: true,
  },
  {
    title: "Try Immer",
    done: false,
  },
];
function App() {
  const [state, setState] = useState(baseState);

  function handleState() {
    const nextState = produce(state, (draft) => {
      draft[1].done = true;
      draft.push({ title: "Tweet about it", done: false });
    });
    console.log("state", Object.is(state, nextState));
    console.log("state[0]", Object.is(state[0], nextState[0]));
    console.log("state[1]", Object.is(state[1], nextState[1]));

    setState(nextState);
  }

  return (
    <div>
      <h1>Immer</h1>
      <button onClick={handleState}>change</button>

      <ul>
        {state.map(({ title, done }) => (
          <li key={title}>
            <p>title:{title}</p>
            <p>status: {done ? "done" : "idle"}</p>
          </li>
        ))}
      </ul>
    </div>
  );

在 handleState 裡面我還另外多下了三個 console.log 用來觀察新舊狀態的結果是否有不同。

immer

注意到了嗎?被新增項目的的最外層 array 以及被修改了屬性的 state[1]Object.is 的比較結果是 false,但是沒有被修改到的 state[0] 的比較結果為 true,immer 會把我們對 draftState 的操作記錄下來,盡量的減少影響的範圍,只對需要的 object 的做建立的動作,最後回傳一個全新的物件給我們,而且不會對我們傳進去的 baseState 做任何的修改來做到 immutable 的狀態管理。

immer 真的非常好用,用過之後真的有點回不去的感覺,當狀態的層數較多,稍微複雜的時候就會想要使用 immer 來協助管理。


immer document

下一篇簡單介紹 SSR 跟 next.js
如果內容有誤再麻煩大家指教,我會盡快修改。

這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day27]我的 react 學習記錄 - react hook form
下一篇
[Day29]我的 react 學習記錄 - SSR & Next.js
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言