iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Modern Web

30 days of React 系列 第 16

Day 16 - 在 state 中更新物件

  • 分享至 

  • xImage
  •  

昨天學習了 React 的 batching 的概念、「替代」和「做」的區別、當次渲染的邏輯,今天要來學習如何在 state 中更新物件。學習前讓我們再次回憶起之前學過的 useState 語法。

const [state, setState] = useState(initialState);
  • state: 當前的狀態,渲染中會對應到作為初始值所傳入的參數。
  • setState: set function 用來更新以及觸發渲染。

回顧完這些東西,實際來看看例子。

創建新物件

首先我們先預設有傳入 useState 的初始值為一個物件,物件裡指定了座標 x: 0與 y:0 ,也就是座標的起始值。

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

若我們想要讓指定的目標隨著滑鼠移動而更新座標,使用以下的作法能夠完成嗎?

import { useState } from "react";
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0,
  });
  return (
    <div
      onPointerMove={(e) => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: "relative",
        width: "100vw",
        height: "100vh",
      }}>
      <div
        style={{
          position: "absolute",
          backgroundColor: "red",
          borderRadius: "50%",
          transform: `translate(${position.x}px, ${position.y}px)`,
          left: -10,
          top: -10,
          width: 20,
          height: 20,
        }}
      />
    </div>
  );
}

這邊我們使用的方法是

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

所以當點擊時做的動作事去改動 position 的值,但這樣做是無效的,原因是我們在改動的是前一個渲染的資料。因為 positon 對應到的是當前的狀態,也就是作為初始值所傳入的參數,去改動它並不會有作用。在這邊做的動作稱為 mutation 這樣做就像「吃飽飯後再改變用餐順序一樣」。

這邊也有一個重要的觀念:將放入狀態的任何 JavaScript 物件視為唯讀(read-only)。

這個行不通的話,要做的事情就更明顯了,應該要想辦法觸法渲染才能達到目的,我們可以透過

創建新的物件並將它傳進可以觸發渲染的的 state setting function 當中,在這邊也就是 setPosition

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

透過這樣做,我們告訴 React

  1. positon換成這個物件
  2. 再次渲染

複製物件

剛剛的方法是利用創建新物件來達到觸發渲染,但如果必須保留舊物件的資料的情況,譬如更新其中一欄的表格欄位,其餘欄位保持之前的資料,該如何處理?mutation 的方法在此一樣不能運作,若要保有舊的資料,勢必必須將舊的資料也一併傳入 setting function 當中。

像這樣子:

setPerson({
  firstName: e.target.value, // New first name from the input
  lastName: person.lastName,
  email: person.email,
});

但若是 property 很多,在那邊手動複製,有點不妙。回憶起 JavaScript 的object spread的用法,就可以省去這些麻煩:

setPerson({
  ...person, // 複製舊的欄位
  firstName: e.target.value, //但覆蓋這個欄位的資料
});

但是程式碼看起來還是有重複的地方:

import { useState } from "react";

export default function Form() {
  const [person, setPerson] = useState({
    firstName: "Barbara",
    lastName: "Hepworth",
    email: "bhepworth@sculpture.com",
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value,
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value,
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value,
    });
  }

  return (
    <>
      <label>
        First name:
        <input value={person.firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={person.lastName} onChange={handleLastNameChange} />
      </label>
      <label>
        Email:
        <input value={person.email} onChange={handleEmailChange} />
      </label>
      <p>
        {person.firstName} {person.lastName} ({person.email})
      </p>
    </>
  );
}

改善這樣子的程式碼可以透過方括號來達到靈活(dynamic) 應對各個欄位的資料。

譬如可以改成這樣子

function handleChange(e) {
  setPerson({
    ...person,
    [e.target.name]: e.target.value,
  });
}

之後在 return 的 jsx 中的onChange prop 當中設定handleChange即可。

參考資料

  • react 官方文件 - Updating Objects in State

上一篇
Day 15 - React 渲染:排隊任務
下一篇
Day 17 - 在 state 中更新陣列
系列文
30 days of React 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言