Skip to main content

React Hook 分析 - useState

前言

什麼是「狀態 state」? 我認為可以用「驅動畫面的資料」做為理解-可以 記憶目前狀態,但可能會改變,改變狀態後,畫面也會隨之 改變

我們可以看看有哪些情況可以適用 state

  1. 頁碼 : 在畫面顯示 目前 是第幾頁,按下一頁之後,目前頁碼顯示也會隨之 改變
  2. 商品列表 : 在畫面顯示 目前 商品列表,但是點選其他分類後,商品列表也會隨之 改變

這就是為什麼 React 很多人喜歡以計數器做為範例,因為簡單又可以體現 state 的性質

  1. 顯示目前數字點擊到多少
  2. 當點擊 +1 畫面的數字也要跟著 +1

component 根據 state 的內容進行 render ,而當 state 被使用者行為改變時,component 也會隨著 state 一起改變

可以說 state 跟元件之間是綁在一起的生命共同體

我認為把 render 翻譯為 *渲染* 有一點不精確,因為 render 只是 React **完成最新畫面的施工藍圖**,而非瀏覽器真正渲染出那個畫面,往後我會參考 PJChen 大大,將 render 稱為 **轉譯**

useState 基本使用

import { useState } from "react";

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

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

return <button onClick={handleClick}>Clicked {count} times</button>;
}

切記,hook 都要放在 function 的 top level,亦即不能被 for loop, if else 等包住,以免影響渲染運作

  • useState(0) :
    • 以 0 作為初始值,並回傳一個陣列
    • 裡面的兩個陣列物件,分別用 countsetCount 來解構賦值

count : state variable,保存元件渲染所需的狀態和資料

setCount : state setter function,用來改變 state,而 state 被改變時,會 re-render 該元件

countsetCount 是綁定一起、天生一對,我們不能藉由去更改 count 的內容來觸發畫面重新渲染,而是透過 setCount(3) 來將 count 的值改為 3,並觸發重新渲染,顯示最新的 state

setState 的更新方式

方法一 : 直接指定

setCount(count + 1);

第一次 setState

  • 取得 state 值 count 為 0
  • 將值指定更新為 0+1,也就是 setCount(0+1)

第二次 setState

  • 取得 state 值 count 為 1
  • 將值指定更新為 1+1,也就是 setCount(1+1)

這樣就可以達到累加的效果

要注意的是,如果像以下方法撰寫,很可能會有問題

import { useState } from "react";

export default function Counter() {
const [count, setCount] = useState(0);

return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}}
>
+3
</button>
</>
);
}

這種寫法,按下按鈕並不會 +3,只有 +1 而已,為什麼呢? 假設 count 目前為 0,其實這種寫法等價於

setCount(0+1) 「請把 state 設為 1」

setCount(0+1) 「請把 state 設為 1」

setCount(0+1) 「請把 state 設為 1」

因為 state 在 render 期間並不會被改變,這樣只是重複 3 次一樣的動作罷了,接下來就要介紹 updater function

方法二 : update function

我們可以傳入一個函式到 setState 中

setCount((n) => n + 1);

這個方法是把 state 的更新放進去 queue 中,最後才依序執行

import { useState } from "react";

export default function Counter() {
const [count, setCount] = useState(0);

return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setCount((n) => n + 1);
setCount((n) => n + 1);
setCount((n) => n + 1);
}}
>
+3
</button>
</>
);
}

和方法一不同的是,使用 updater function,按下按鈕就會直接 +3

queued updatenreturns
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

更詳細的機制,可以搜尋 useState batch update

Render 性質

  1. 觸發 render
    • 元件初始 render 會觸發,沒什麼懸念
    • 元件 ( 或是他的父元件 ) state 被改變,就會觸發 render
  2. 開始 render 元件
    • 開始編排所有 component 的施工藍圖
    • 從 root component 進入,以遞迴方式往下探尋子元件
    • React 的元件有 Functional Programming 的思想 - same input, same output
  3. 依據 DOM tree 的差異,開始變更 DOM 元素
    • React 只會根據和上一次 render 的差異進行 DOM 操作,而不是無腦重新生成,這是為了效能考量
  4. 瀏覽器實際「畫出來」

State 與 Render 的關係

import { useState } from "react";

export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+5
</button>
</>
);
}

在這個範例可以發現,原本數字為 0 ,但按下按鈕後,畫面顯示最新的數字 5,但是過了 3 秒之後,彈跳視窗顯示的還是舊的 state 「0」,而非 5,為什麼會這樣呢?

我們可以想像每一次元件進行 render 時,都是按照當時的 state 的狀態拍下一張照片 ( snapshot ), state 的值在 render 過程中永遠不會變,以下分步講解

初始 state 為 0,按下按鈕:

  1. setNumber(number + 5) 放在序列,render 完成後,將觸發 re-render ( 因為是 setState )
  2. 倒數計時,設定 3 秒後,會跳出 alert 數字 0 ( 因為現在的 state 還是 0 )
  3. 元件 render 完畢,觸發 re-render

re-render 完成,最後顯示 state 為 5,並且時間到,跳出 alert 數字 0

元件之間的 state 是獨立的

import Gallery from "./Gallery.js";

export default function Page() {
return (
<div className="Page">
<Gallery />
<Gallery />
</div>
);
}

<Gallery> 元件內部都有用到 state,但這兩個 <Gallery> 元件的 state 是獨立存在於 function 內部,不會互相影響

Reference

https://beta.reactjs.org/learn/render-and-commit

https://beta.reactjs.org/learn/state-as-a-snapshot