React Hooks:讓你不必寫 class 就能使用 state 和其他 React 功能

React Hooks 示意圖

前言

React Hooks 是 React 16.8 中引入的革命性功能,它徹底改變了我們編寫 React 元件的方式。Hooks 讓開發者無需使用 class 就能在函數元件中使用 state 和其他 React 功能,簡化了程式碼結構並提高了程式碼的可重用性。

過去使用元件的方式

Class Component

在 Hooks 出現之前,如果我們需要在元件中使用 state 或生命週期方法,就必須使用 class component:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
 
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
 
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
 
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

Function Component

函數元件在 Hooks 之前只能用於簡單的、無狀態的元件:

function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}

Hooks 的引入

Hooks 讓我們可以在函數元件中使用 state 和其他 React 功能,無需轉換為 class component。主要解決了以下問題:

  1. 狀態邏輯重用困難 - 以前需要使用高階元件或 render props 模式
  2. 複雜元件難以理解 - 生命週期方法經常包含不相關的邏輯
  3. class 的學習成本 - 需要理解 JavaScript 中的 this 工作原理

State Hook (useState)

useState 是第一個也是最基礎的 Hook,它讓函數元件能夠擁有內部狀態:

import React, { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

Effect Hook (useEffect)

useEffect 讓你在函數元件中執行副作用操作,它結合了 class 元件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期方法的功能:

import React, { useState, useEffect } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  // 類似於 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 更新文件標題
    document.title = `You clicked ${count} times`;
  });
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

useEffect 的真正設計目的是用來將 React 元件與外部系統同步,而不是單純作為生命週期的替代品。

參考文章:

自定義 Hook (Custom Hook)

自定義 Hook 是一種遵循特定命名約定(以 "use" 開頭)的 JavaScript 函數,它可以調用其他 Hook:

import { useState, useEffect } from "react";
 
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
 
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);
 
  return width;
}
 
// 在元件中使用
function MyComponent() {
  const width = useWindowWidth();
  return <div>Window width: {width}</div>;
}

常見的自定義 Hook 示例

1. useMediaQuery

這個 Hook 讓你可以在 React 裡「偵測媒體查詢條件是否成立」,例如螢幕寬度是否小於 768px。 它會根據視窗大小自動更新狀態,讓你可以在元件裡用 isMobile 這種布林值,來做 RWD 或條件渲染。

適合場景:

  • 響應式設計(RWD)
  • 根據螢幕大小切換不同 UI 或功能
import { useState, useEffect } from "react";
 
function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
 
  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    const listener = () => setMatches(media.matches);
    media.addListener(listener);
    return () => media.removeListener(listener);
  }, [query, matches]);
 
  return matches;
}
 
// 使用示例
function Component() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return <div>{isMobile ? "Mobile View" : "Desktop View"}</div>;
}

2. usePagination

usePagination 就是一個 headless 的 Pagination 元件,他已經幫你處理好資料邏輯,讓使用的開發者只需要關注在渲染邏輯。

你只要給它一份資料和每頁要顯示幾筆,它就會自動幫你算出目前頁面、最大頁數、當前要顯示的資料,還有上一頁、下一頁、跳頁等方法。

適合場景:

  • 文章列表、商品列表等需要分頁的地方
  • 想要把分頁邏輯抽離 UI,讓元件更乾淨
import { useState } from "react";
 
function usePagination(data, itemsPerPage) {
  const [currentPage, setCurrentPage] = useState(1);
  const maxPage = Math.ceil(data.length / itemsPerPage);
 
  function currentData() {
    const begin = (currentPage - 1) * itemsPerPage;
    const end = begin + itemsPerPage;
    return data.slice(begin, end);
  }
 
  function next() {
    setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage));
  }
 
  function prev() {
    setCurrentPage((currentPage) => Math.max(currentPage - 1, 1));
  }
 
  function jump(page) {
    const pageNumber = Math.max(1, page);
    setCurrentPage(Math.min(pageNumber, maxPage));
  }
 
  return { next, prev, jump, currentData, currentPage, maxPage };
}
 
// 使用示例
function PaginatedList({ data }) {
  const { currentData, next, prev, currentPage, maxPage } = usePagination(
    data,
    5
  );
 
  return (
    <div>
      {/* 渲染 currentData */}
      <button onClick={prev} disabled={currentPage === 1}>
        Previous
      </button>
      <button onClick={next} disabled={currentPage === maxPage}>
        Next
      </button>
    </div>
  );
}

3. useDebounce

這個 Hook 幫你實現「防抖動」功能。防抖動是一種常用的最佳化技術,可以限制某個函數在短時間內被多次調用,從而減少效能消耗和不必要的重複操作。

一般來說,防抖動的實作原理是透過延遲執行函數,在指定的時間內如果再次觸發,則重新計時。這對於一些需要等待使用者輸入完成或減少請求發送頻率的場景非常有用。

適合場景:

  • 搜尋框、即時過濾等需要減少頻繁請求的場景
  • 想要優化效能、減少不必要的運算或 API 呼叫
import { useState, useEffect } from "react";
 
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
 
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
 
  return debouncedValue;
}
 
// 使用示例
function SearchInput() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 500);
 
  useEffect(() => {
    // 這裡可以執行搜索 API 調用
    console.log("Searching for:", debouncedQuery);
  }, [debouncedQuery]);
 
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

這些自定義 Hook 都是 React 開發中非常實用的範例,可以讓你的元件更簡潔、邏輯更好重用!

Hooks 的優點

  1. 更少的程式碼 - 消除了 class 的樣板程式碼,如 constructor 和 binding
  2. 簡化複雜的元件 - 將相關邏輯組織在一起,而不是分散在多個生命週期方法中
  3. 重用有狀態邏輯 - 通過自定義 Hook 輕鬆共享邏輯,無需高階元件或 render props
  4. 共享非視覺邏輯 - 可以在不改變元件層次結構的情況下共享邏輯

Hooks 的挑戰

  1. 學習曲線 - 特別是 useEffect 的正確使用需要時間掌握
  2. 常見陷阱 - 如 useCallbackuseMemo 的過度或不當使用
  3. 規則限制 - Hooks 只能在函數元件的最頂層調用,不能在條件或循環中使用

Hooks 使用規則

  1. 只在最頂層調用 Hook - 不要在循環、條件或巢狀函數中調用 Hook
  2. 只在 React 函數元件中調用 Hook - 不要在普通 JavaScript 函數中調用 Hook
  3. 自定義 Hook 必須以 "use" 開頭命名 - 這樣 React 才能識別並檢查 Hook 規則

結語

React Hooks 代表了 React 開發方式的重大進步,它簡化了狀態管理和副作用處理,使程式碼更加模塊化和可重用。雖然需要一些時間來適應新的思維模式,但一旦掌握,Hooks 將大大提高開發效率和程式碼品質。

隨著 React 生態系統的發展,Hooks 已經成為現代 React 開發的標準實踐,建議新專案都採用 Hooks 來編寫元件。