iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Modern Web

react 學習記錄系列 第 9

[Day9]我的 react 學習記錄 - react event 綁定 & useState

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單說明 react 裡面的事件綁定跟透過 useState hook 來管理狀態。


JavaScript 的事件綁定

JavaScript 裡事件綁定的方式有三種。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 方法一 inline -->
    <button id="btn1" onclick="sayHi()">Button 1</button>
    <button id="btn2">Button 2</button>
    <button id="btn3">Button 3</button>

    <script>
      function sayHi() {
        console.log("Hi!!!");
      }

      // 方法二 node.onclick = event
      document.querySelector("#btn2").onclick = sayHi;

      // 方法三 node.addEventListener(event, function)
      document.querySelector("#btn3").addEventListener("click", sayHi);
    </script>
  </body>
</html>

過去大多數的情況都是 addEventListener 方式來綁定事件,但是當我們在寫 React 的時候大多數的情況並不會針對 DOM 做操作,因此不太會使用 document 的相關語法,React 官方建議是使用 inline 的語法做事件的綁定,像下面這樣。

function App() {

  function sayHi(){
    console.log("Hi!")
  }

  return (
    <div>
      <button onClick={sayHi}>Button</button>
    </div>
  );
}

注意到我這邊寫的是 onClick 而不是 onclick,並不是手誤打錯字。

這是因為我們現在寫的並不是 html 而是 JSX 所以為了避免語法衝突跟解析錯誤,在 react 裡面 inline 事件綁定的寫法必須要依照 camelCase 的寫法。

另外還有一點不同,
html 的 inline 會放入一個呼叫 function 的 string 像這樣,onclick="sayHi()"
但是在 react 裡面我們要放入一個 function 來綁定事件,onClick={sayHi}


useState

簡單講完了在 react 裡面如何綁定事件,那接下來要介紹我們第一個 hook - useState 了!

當用戶跟網頁互動時,不管是點擊按鈕或是輸入內容都會更改網頁當前的狀態。
在 react 裡面可以使用 useState 來管理我們網頁的狀態。

Syntax

const [state, setState] = useState(initialState);

initialState: 可以是任意型別,如果是 function,react 會執行 function 並保留下 return value 作為初始值,且不會再次執行。
state: 當前最新的 state,在第一次 render 時會跟 initialState 相同。
setState: set function,用來更新狀態,可以想成 updateState(new value)。

注意事項

  • hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡,如果有需要可以建立一個新的子元件,放在子元件裡。
  • 如果 initialState 是一個 function,必須要是 pure function,不能有 side effect。
  • useState 回傳的內容是一個 array,慣例上會使用陣列解構的方式把東西取出來,命名的規則上會使用 camelCase 的方式命名,set function 的命名會依照 state 的變數名稱前面加上 set。e.g., [number, setNumber][count, setCount]

Counter

講完了語法來看 code 吧!

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  function addOne() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Count:{count}</h1>
      <button onClick={addOne}>Add one</button>
    </div>
  );
}

基本上就是一個每次按按鈕會加 1 的計數器而已。

count-1

這樣就完成了。

第一次看到這一段 code 我就在想怎麼這麼麻煩,宣告一個變數然後直接加 1 不就好了嗎?
像這樣:

function App() {
  let count = 0;

  function addOne() {
    count += 1
      console.log(count); // 觀察數字變化
  }

  return (
    <div>
      <h1>Count:{count}</h1>
      <button onClick={addOne}>Add one</button>
    </div>
  );
}

執行過會發現雖然 console 出來的數字有改變,但是畫面上永遠都會是 0,數字不會增加。

count2

那是因為 react 不知道它需要更新畫面,react 只有在 state 改變的時候才會更新畫面,當 state 沒有改變時自然畫面不會更新。

所以一定要使用 useState 來做跟畫面相關的狀態管理。


State 改變時發生什麼事情

多下幾個 console 稍微看一下畫面更新時 react 是怎麼運作的。

function App() {
  const [count, setCount] = useState(0);

  function addOne() {
    setCount(count + 1);
      console.log("in addOne:", count)	
  }
  console.log("before return:", count)
  return (
    <div>
      <h1>Count:{count}</h1>
        {console.log("in jsx:", count)}
      <button onClick={addOne}>Add one</button>
    </div>
  );
}

count3

可以看到畫面第一次出現時出現順序是 before return -> in jsx 並且數字跟 count 是相同的。

每次點擊數字更新時出現的順序是 in addOne -> before return -> in jsx,但是會發現 addOne 的 count 會是舊的,這是因為 setState 的更新是 非同步 的,並不會當下馬上就更新 state 的 value。

react 會把這一次事件所觸發的所有 setState 都執行完畢後再一次更新 state 裡面的 value。

當 state 改變觸發 react 更新畫面時 react 會由上而下一行一行的執行元件裡面的每一行 code。
更新畫面這個動作又稱為 re-render。

如果希望在一次的事件裡多次觸發相同的 SetState 該怎麼處理呢?
如果像這樣在一個 function 裡面執行多次 SetState 已經知道是不可行的。

function App() {
  const [count, setCount] = useState(0);

  function addOne() {
    setCount(count + 1);
  }

  function addThree() {
    // 會失敗 結果會是只有執行最下面的 count + 2
    setCount(count + 1); count + 1
    setCount(count + 3); count + 3
    setCount(count + 2); count + 2
  }

  return (
    <div>
      <h1>Count:{count}</h1>
      <button onClick={addOne}>Add one</button>
      <button onClick={addThree}>Add three</button>
    </div>
  );
}

每一次 react 更新畫面的時候當下每個變數的的 value 都是已經決定好的,可以想像成是一個 snapshot,react 會記住當下每一個變數的 value 是什麼。

那假設有一個狀況依照用戶的選擇的數量跟次數去增加 count,像是 count + (數次 * 次數)

function App() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  function addNumber() {
    setNumber(number + 1);
  }

  function addOne() {
    setCount(count + 1);
  }

  function addNumberTimes() {
    for (let i = 0; i < number; i++) {
      addOne();
    }
  }

  return (
    <div>
      <h1>Count:{count}</h1>
      <h1>Number:{number}</h1>

      <button onClick={addOne}>Add one</button>
      <button onClick={addNumber}>number</button>
      <button onClick={addNumberTimes}>Add number times</button>
    </div>
  );
}

像是這樣,測試過會發現即使 number 是 10,當點下 Add number times 的時候count 還是只會 +1,前面提到每次的 render 的時候變數的值都是已經決定好的,所以對 react 來說就像是執行了 10 次相同的 count + 1,所以當我們點擊 Add number times 的時候 count 還是只會 +1。

count4

如果要上面的 code 作用只需要稍微改變一下 setCount 裡面的呼叫方法。

function addOne() {
  setCount((prevCount) => prevCount + 1);
}

這樣 Add number times 會依照我們預期的作用了!

count5

上面有提到 setState 的執行是非同步的,所以 value 並不會馬上更新,但是更新過後的 value 會被暫時保留下來,當 setState 放入的參數是一個 function 時可以收到暫時被保留下來的 value,命名的規則上會習慣用 prevState 的方式來命名,透過這個方式就可以取得暫時被保留下來的value,這樣就可以在一次的事件裡重複觸發相同的 setState 來更新 state。

接著講講 setState 在處理 array 跟 object 時可能遇到的情況。


Object

先看看假設我的 state 是一個 object 的時候要如何更新 value,如果希望 state 跟 input 標籤做綁定的時候會使用 onChange 事件來處理。

import { useState } from "react";

interface Info {
  name: string;
  age: string;
}

function App() {
  const [info, setInfo] = useState<Info>({ name: "", age: "" });

  function handleChange({ target }: React.ChangeEvent<HTMLInputElement>) {
    setInfo({ ...info, [target.name]: target.value });
  }

  return (
    <div>
      <h1>Info:</h1>
      <label htmlFor="name">
        Name:
        <input
          type="text"
          autoComplete="off"
          name="name"
          value={info.name}
          onChange={handleChange}
        />
      </label>
      <br />
      <label htmlFor="name">
        Age:
        <input
          type="text"
          autoComplete="off"
          name="age"
          value={info.age}
          onChange={handleChange}
        />
      </label>
    </div>
  );
}

count6

如果對解構賦值或是 object 裡面 key 的寫法還不太熟悉的話看到這個語法可能會覺得怪怪的,
{ ...info, [target.name]: target.value }... 的作用在於把本來 object 或是 array裡面的 value 一個一個取出來,這個動作又稱為解構,...也稱作展開運算子或其餘運算子。

另外在 object 裡面如果希望 key 可以是一個變數,而不是寫死的 value 的話可以用中括號[]把希望當作 key 的變數包起來就可以把變數當作 key 了。

{ ...info, [target.name]: target.value }這個動作的意思是建立一個新的物件,裡面的內容要跟 info 裡面一樣,如果 target.name 的 value 是 info 裡面已經有的 key 就把那個 key 的 value 更新成 target.value,如果 target.name 的 key 不在 info 裡面就新增一個 key 跟 value 到新的物件裡。

看起來非常複雜,為什麼不能簡單一點 info[target.name] = target.value 就好了呢?像這樣。

function handleChange({ target }: React.ChangeEvent<HTMLInputElement>) {
  info[target.name] = target.value;
  console.log("info:", info); // 觀察用
  setInfo(info);
}

count7

從 console 可以看到,按下鍵盤的時候 value 是有更新但是 state 沒有更新,為什麼呢?

前面提到 react 會在 setState 更新的時候更新畫面,那 react 怎麼知道傳進去 setState 裡面的 value 跟本來的 state 是否相同呢?

react 用 Object.is() 來做比較,當比較結果不同的時候才會執行 re-render 更新畫面。

透過 Object.is() 比較 object 或是 array 時,雖然內容有改變但是儲存的記憶體位址相同的情況下就會判斷是相同的東西,所以 setState 並不會觸發 re-render 更新畫面。

所以才會需要透過 { ...info, [target.name]: target.value } 的方式來更新 state。


Array

接著看看 array 的情形。

import { useState } from "react";

function App() {
  const [list, setList] = useState<string[]>([]);
  const [value, setValue] = useState<string>("");

  function handleChange({ target }: React.ChangeEvent<HTMLInputElement>) {
    setValue(target.value);
  }

  function handleAddItem() {
    if (!value) return;
    list.push(value);
    console.log("list:", list); // 觀察用
    setList(list);
  }

  return (
    <div>
      <h1>List</h1>
      <input type="text" value={value} onChange={handleChange} />
      <button onClick={handleAddItem}>Add item</button>
      <ul>
        {list.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

會發現雖然 list 有更新但是畫面卻沒有更新。

count8

因為 Array.push() 這個 method 是去更新原本那個 array 的內容並不是建立一個新的 array 再儲存,所以當 react 用 Object.is() 在比較時結果會是同一個東西,所以一樣可以使用 ... 來解決這個狀況。

function handleAddItem() {
  if (!value) return;
  setList([...list, value]);
}

conut9

這樣就可以如我們預期的作用了。


Storing information from previous renders - react document
State as a Snapshot - react document

下一篇簡單講講 react 是怎麼運作的跟 key 的作用。
如果內容有誤再麻煩大家指教,我會盡快修改。

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


上一篇
[Day8]我的 react 學習記錄 - 渲染 variable & props 傳遞
下一篇
[Day10]我的 react 學習記錄 - react 如何運作跟 key 是什麼
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言