React State 與 useState

React 元件透過資料狀態的改變來更新 UI,而元件有兩種資料來源:

  1. 透過外部傳進來元件的 Props
  2. 元件內部自己維護的 State
每當 React 偵測到 Props 或 State 有更新時,就會自動重新渲染(re-render)UI 元件。

Props 對於元件是唯讀(read-only)的資料,而 State 是元件可以自由讀寫的。

useState Hook

在 Function Component 中,我們使用 useState 這個 Hook 來建立和管理狀態。

基本語法:

import { useState } from 'react'

const [state, setState] = useState(initialValue)
  • state:目前的狀態值
  • setState:更新狀態的函式
  • initialValue:狀態的初始值

簡單範例:計數器

import { useState } from 'react'

function Counter() {
  // 宣告一個 state 變數叫 count,初始值為 0
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>你點擊了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>點我</button>
    </div>
  )
}

這個範例中:

  1. useState(0) 建立一個初始值為 0 的狀態
  2. count 是目前的計數值
  3. setCount 是用來更新 count 的函式
  4. 當按鈕被點擊時,呼叫 setCount(count + 1) 將 count 加 1
  5. React 偵測到 state 改變,自動重新渲染元件

使用多個 State

你可以在一個元件中使用多個 useState

function UserForm() {
  const [name, setName] = useState('')
  const [age, setAge] = useState(0)
  const [email, setEmail] = useState('')

  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="姓名" />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="年齡"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
    </form>
  )
}

State 可以是各種資料型態

State 可以是任何 JavaScript 資料型態:

// 數字
const [count, setCount] = useState(0)

// 字串
const [name, setName] = useState('')

// 布林值
const [isOpen, setIsOpen] = useState(false)

// 陣列
const [items, setItems] = useState([])

// 物件
const [user, setUser] = useState({ name: '', age: 0 })

// null
const [data, setData] = useState(null)

更新物件和陣列 State

當 state 是物件或陣列時,你不能直接修改它們,而是要建立新的物件或陣列:

更新物件

const [user, setUser] = useState({ name: 'Mike', age: 25 })

// ❌ 錯誤:直接修改 state
user.age = 26

// ✅ 正確:建立新物件
setUser({ ...user, age: 26 })

// ✅ 也可以這樣寫
setUser((prevUser) => ({ ...prevUser, age: 26 }))

更新陣列

const [items, setItems] = useState(['apple', 'banana'])

// ❌ 錯誤:直接修改 state
items.push('orange')

// ✅ 正確:建立新陣列
setItems([...items, 'orange'])

// ✅ 移除項目
setItems(items.filter((item) => item !== 'banana'))

// ✅ 更新特定項目
setItems(items.map((item) => (item === 'apple' ? 'red apple' : item)))

State 更新是非同步的

React 為了效能考量,會將多個 setState 呼叫合併成一次更新(batching)。這意味著 state 的更新是非同步的,你不能在呼叫 setState 後立即拿到新的值:

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

  function handleClick() {
    setCount(count + 1)
    console.log(count) // 還是舊的值!
  }

  return <button onClick={handleClick}>Count: {count}</button>
}

使用函式更新器

當新的 state 需要依賴前一個 state 時,應該使用函式更新器(functional update):

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

  function handleClick() {
    // ❌ 可能有問題:如果連續點擊太快
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // count 可能只會 +1,而不是 +3
  }

  function handleClickCorrect() {
    // ✅ 正確:使用函式更新器
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
    // count 會正確地 +3
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClickCorrect}>+3</button>
    </div>
  )
}

函式更新器接收前一個 state 值作為參數,回傳新的 state 值。這確保了即使在批次更新中,每次更新都是基於最新的 state。

Lazy Initialization

如果初始 state 需要經過複雜計算,你可以傳入一個函式,這個函式只會在初次渲染時執行:

// ❌ 每次渲染都會執行 computeExpensiveValue
const [state, setState] = useState(computeExpensiveValue())

// ✅ 只在初次渲染執行
const [state, setState] = useState(() => computeExpensiveValue())

實際範例:

function TodoList() {
  // 從 localStorage 讀取初始資料,只在初次渲染時執行
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos')
    return saved ? JSON.parse(saved) : []
  })

  // ...
}

State 設計原則

1. 避免重複的 State

不要儲存可以從其他 state 或 props 計算出來的值:

// ❌ 不好:fullName 可以從 firstName 和 lastName 計算出來
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')

// ✅ 好:fullName 直接計算
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`

2. 避免巢狀過深的 State

盡量保持 state 結構扁平化:

// ❌ 巢狀太深,更新困難
const [state, setState] = useState({
  user: {
    profile: {
      name: 'Mike',
      address: {
        city: 'Taipei',
      },
    },
  },
})

// ✅ 較扁平的結構
const [userName, setUserName] = useState('Mike')
const [userCity, setUserCity] = useState('Taipei')

3. 將相關的 State 分組

如果多個 state 總是一起更新,考慮合併成一個物件:

// 如果 x 和 y 總是一起變動
const [position, setPosition] = useState({ x: 0, y: 0 })

// 而不是
const [x, setX] = useState(0)
const [y, setY] = useState(0)

完整範例:待辦清單

import { useState } from 'react'

function TodoApp() {
  const [todos, setTodos] = useState([])
  const [inputValue, setInputValue] = useState('')

  // 新增待辦事項
  function addTodo() {
    if (inputValue.trim() === '') return

    setTodos([
      ...todos,
      {
        id: Date.now(),
        text: inputValue,
        completed: false,
      },
    ])
    setInputValue('')
  }

  // 切換完成狀態
  function toggleTodo(id) {
    setTodos(todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)))
  }

  // 刪除待辦事項
  function deleteTodo(id) {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  return (
    <div>
      <h1>待辦清單</h1>

      <div>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="輸入待辦事項"
        />
        <button onClick={addTodo}>新增</button>
      </div>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>刪除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}