第 5 章:State (狀態) 的介紹與使用

在上一章中,我們學習了 Props,它們允許父元件將資料傳遞給子元件。然而,Props 是唯讀的。如果一個元件需要管理其內部的、會隨時間或使用者互動而改變的資料,我們就需要使用 State。

State 代表了元件內部隨時間變化的資料。與 Props 不同,State 是由元件自己管理和控制的,並且當 State 改變時,React 會自動重新渲染元件以反映這些變化。

5.1 什麼是 State?

想像一個開關元件,它有兩種狀態:「開」和「關」。或者一個計數器元件,它需要記錄目前的計數值。這些會改變的值就是元件的 State。

  • State 是私有的:它完全由元件自己控制。
  • State 是可變的:與唯讀的 Props 不同,State 可以被修改。
  • State 的改變會觸發重新渲染:這是 React 的核心機制之一。當你更新一個元件的 State 時,React 會安排該元件及其子元件的重新渲染。

5.2 Props vs. State

特性 Props (屬性) State (狀態)
資料來源 由父元件傳遞 由元件自身管理
可變性 唯讀 (子元件不能修改) 可變 (元件可以修改自己的 State)
生命週期 在元件的整個生命週期中通常保持不變 (除非父元件傳遞新的 Props) 可以在元件的生命週期中改變,通常響應使用者互動或網路請求
用途 用於配置和自訂化元件的行為和外觀 用於儲存和管理元件內部的動態資料,控制元件的行為
誰擁有資料 父元件擁有資料,子元件接收資料 元件自身擁有和管理資料

簡單來說:Props 是元件的「設定」,而 State 是元件的「記憶體」。

5.3 在函式元件中使用 State:useState Hook

在 React 16.8 版本之後,Hooks 的引入徹底改變了函式元件的編寫方式。useState Hook 讓我們可以在函式元件中使用 State。

如何引入 useState

import React, { useState } from 'react';

useState 的基本語法:

useState 是一個函式,它接收一個參數作為 State 的初始值,並返回一個包含兩個元素的陣列:

  1. 目前 State 的值 (current state value)。
  2. 一個更新該 State 的函式 (updater function)。
const [stateVariable, setStateVariable] = useState(initialValue);
  • stateVariable:你為 State 選擇的名稱。
  • setStateVariable:一個函式,通常命名為 set 加上 State 變數的名稱 (例如 setCount, setName)。呼叫此函式將更新 State 並觸發重新渲染。
  • initialValue:State 的初始值。它可以是任何 JavaScript 資料型別 (數字、字串、布林值、陣列、物件等)。這個初始值只在元件首次渲染時使用。

範例:一個簡單的計數器元件

// Counter.js
import React, { useState } from 'react';

function Counter() {
  // 使用 useState Hook 來宣告一個名為 count 的 state 變數,初始值為 0
  // count 是目前的 state 值
  // setCount 是更新 count 的函式
  const [count, setCount] = useState(0);

  // 事件處理函式,用於增加計數
  const handleIncrement = () => {
    setCount(count + 1); // 更新 count 的值
  };

  // 事件處理函式,用於減少計數
  const handleDecrement = () => {
    setCount(count - 1); // 更新 count 的值
  };

  return (
    <div>
      <h2>計數器</h2>
      <p>目前的計數: {count}</p>
      <button onClick={handleIncrement}>增加 (+)</button>
      <button onClick={handleDecrement}>減少 (-)</button>
      <button onClick={() => setCount(0)}>重設</button> {/* 直接在 JSX 中使用箭頭函式更新 state */}
    </div>
  );
}

export default Counter;

在這個例子中:

  1. useState(0) 初始化 count0
  2. 當使用者點擊「增加 (+)」按鈕時,handleIncrement 函式被呼叫。
  3. setCount(count + 1) 更新 count 的值。React 會偵測到 State 的變化,並重新渲染 Counter 元件,顯示新的計數值。

使用多個 State 變數:

一個函式元件可以擁有多個 State 變數,只需多次呼叫 useState 即可。

import React, { useState } from 'react';

function UserProfile() {
  const [name, setName] = useState('訪客');
  const [age, setAge] = useState(null);
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const handleLogin = () => {
    setIsLoggedIn(true);
    setName('React 使用者');
    setAge(3); // 假設 React 使用者 3 歲
  };

  const handleLogout = () => {
    setIsLoggedIn(false);
    setName('訪客');
    setAge(null);
  };

  return (
    <div>
      <h2>使用者設定檔</h2>
      {isLoggedIn ? (
        <div>
          <p>姓名: {name}</p>
          <p>年齡: {age !== null ? age : '未知'}</p>
          <button onClick={handleLogout}>登出</button>
        </div>
      ) : (
        <div>
          <p>請先登入。</p>
          <button onClick={handleLogin}>登入</button>
        </div>
      )}
    </div>
  );
}

export default UserProfile;

State 更新函式的行為 (Setter Function Behavior):

  • 取代 State:當你使用 useState 的更新函式 (例如 setCount) 時,它會 取代 舊的 State 值,而不是像類別元件中的 this.setState() 那樣進行合併 (特別是當 State 是物件時)。如果你希望保留物件中的其他部分,你需要手動合併。

    const [user, setUser] = useState({ name: 'Alice', age: 30 });
    
    const updateUserName = (newName) => {
      // 如果 State 是物件,更新時需要手動合併
      setUser(prevUser => ({ ...prevUser, name: newName }));
    };
    
  • 函式更新 (Functional Updates):如果新的 State 需要基於先前的 State 計算得出,你可以向更新函式傳遞一個函式。這個函式會接收先前的 State 值作為參數,並返回新的 State 值。這在 State 更新依賴於先前值時非常有用,尤其是在處理非同步操作時,可以避免因為閉包導致的舊 State 值問題。

    const handleIncrementFiveTimes = () => {
      for (let i = 0; i < 5; i++) {
        // 如果這樣寫,count 可能不會如預期增加 5 次,因為 setCount 是非同步的
        // setCount(count + 1);
    
        // 使用函式更新可以確保每次都是基於最新的 state
        setCount(prevCount => prevCount + 1);
      }
    };
    

5.4 在類別元件中使用 State (簡介)

雖然現代 React 更推薦使用函式元件和 Hooks,但了解類別元件如何處理 State 仍然有助於理解舊有程式碼或某些特定場景。

在類別元件中,State 是一個物件,儲存在 this.state 中。

1. 初始化 State:

通常在建構函式 (constructor) 中初始化 State。

import React from 'react';

class ClassCounter extends React.Component {
  constructor(props) {
    super(props); // 必須先呼叫 super(props)
    // 初始化 state
    this.state = {
      count: 0,
      message: "你好"
    };
  }

  // ...
}

2. 讀取 State:

透過 this.state.propertyName 來讀取 State。

render() {
  return (
    <div>
      <p>計數: {this.state.count}</p>
      <p>訊息: {this.state.message}</p>
    </div>
  );
}

3. 更新 State:this.setState()

使用 this.setState() 方法來更新 State。this.setState() 會將傳入的物件與目前的 State 進行「淺合併」(shallow merge)。

handleIncrement = () => {
  // 更新 count,message 保持不變
  this.setState({ count: this.state.count + 1 });
};

handleChangeMessage = (newMessage) => {
  this.setState({ message: newMessage });
};

this.setState() 的重要特性:

  • 不要直接修改 State:永遠不要像 this.state.count = 10; 這樣直接修改 State,因為這不會觸發重新渲染,且可能導致非預期行為。
  • State 更新可能是非同步的:React 可能會將多個 setState() 呼叫批次處理以提高效能。因此,你不應該依賴 this.statethis.props 來計算下一個 State。
  • 函式更新:與 useState 類似,如果新的 State 依賴於先前的 State 或 Props,你可以向 this.setState() 傳遞一個函式,而不是一個物件。這個函式會接收 prevStateprevProps (或 props) 作為參數。

    this.setState((prevState, props) => ({
      count: prevState.count + props.incrementStep
    }));
    
  • 回呼函式this.setState() 接受一個可選的第二個參數,它是一個回呼函式,會在 setState 完成並且元件重新渲染之後執行。

    this.setState({ count: 10 }, () => {
      console.log('State 更新完成,新的 count 是:', this.state.count); // 這裡會是 10
    });
    

5.5 State 使用規則

無論是使用 useState 還是 this.setState,都有一些重要的規則需要遵守:

  1. 不要直接修改 State (Do Not Modify State Directly)

    • 對於 useState:總是使用 setXxx 函式來更新。
    • 對於 this.state:總是使用 this.setState()。 直接修改 State (例如 count = 5;this.state.value = 5;) 不會觸發重新渲染,並且可能在後續更新中被覆蓋。
  2. State 更新可能是非同步的 (State Updates May Be Asynchronous) React 為了效能考量,可能會將多次 State 更新批次處理。因此,如果你需要在更新後立即讀取 State 的值,它可能還不是最新的。

    • 對於 useState:如果新 State 依賴舊 State,使用函式更新形式 setXxx(prevState => ...)
    • 對於 this.state:如果新 State 依賴舊 State,使用函式更新形式 this.setState((prevState, props) => ...)。如果需要在更新後執行操作,使用 setState 的回呼函式。
  3. State 更新是合併的 (State Updates are Merged - 主要針對類別元件的物件 State)

    • 當你使用 this.setState() 更新一個包含多個屬性的物件 State 時,React 會將你傳遞的物件與現有 State 進行淺合併。例如,如果 this.state{ count: 0, theme: 'dark' },呼叫 this.setState({ count: 1 }) 會使 this.state 變成 { count: 1, theme: 'dark' } (theme 屬性被保留)。
    • 對於 useState,如果 State 是一個物件,更新函式 (如 setUser) 會 取代 整個物件。你需要自己處理合併邏輯,通常使用展開運算子 (...):
      const [user, setUser] = useState({ name: 'A', role: 'admin' });
      setUser(prevUser => ({ ...prevUser, name: 'B' })); // role 被保留
      // setUser({ name: 'B' }); // 這樣會導致 role 消失
      

5.6 何時使用 State?

當元件需要記住某些資訊,並且這些資訊會隨著時間或使用者互動而改變時,就應該使用 State。例如:

  • 表單輸入的值
  • 開關的開/關狀態
  • 計時器的目前時間
  • 從 API 獲取的資料
  • 下拉式選單是否展開
  • 目前選擇的項目

如果一個值不需要改變,或者可以從 Props 或其他 State 計算得出,那麼它可能不需要成為一個獨立的 State。

5.7 狀態提升 (Lifting State Up)

有時候,多個元件需要共享和操作相同的 State。在這種情況下,React 的常見模式是將共享的 State「提升」到它們最接近的共同父元件中。然後,父元件透過 Props 將 State 和修改 State 的函式傳遞給相關的子元件。

我們將在後續章節更詳細地探討這個重要的概念,因為它是構建複雜 React 應用的關鍵。

5.8 總結

State 使得我們的 React 元件能夠擁有「記憶」並對互動做出反應。

  • State 是元件私有的、可變的資料。
  • 在函式元件中,我們使用 useState Hook 來管理 State。
  • useState 返回目前 State 和一個更新它的函式。
  • 在類別元件中,State 儲存在 this.state 中,並透過 this.setState() 更新。
  • 永遠不要直接修改 State;State 更新可能是非同步的。
  • 當元件需要追蹤隨時間變化的資料時,使用 State。
  • 「狀態提升」是一種在多個元件間共享 State 的重要模式。

掌握 Props 和 State 是理解 React 的核心。它們共同構成了 React 元件的資料管理基礎。在接下來的章節中,我們將學習更多關於元件生命週期和如何處理使用者事件的知識。

results matching ""

    No results matching ""