跳至主要内容

87 篇文章 含有標籤「前端工程」

檢視所有標籤

整合 Tailwind CSS 與 daisyUI 的入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、前言:為什麼選擇 Tailwind CSS 搭配 daisyUI?

在現代前端開發中,Tailwind CSS 提供了高度自訂、原子化的 CSS class,使得開發者能夠以更模組化與語意化的方式撰寫樣式。然而,Tailwind 並不內建 UI 元件,這就使得一些常見元件(如按鈕、表單、卡片)仍需手動組裝樣式。

這時,daisyUI 就成為極佳的搭檔。daisyUI 是一個基於 Tailwind CSS 架構的元件庫,提供豐富的預設元件樣式與主題切換能力,讓你能快速建構出一致、美觀的介面,並維持 Tailwind CSS 的開發哲學。


二、基本安裝流程

1. 初始化專案

首先建立一個新的專案,並安裝 Tailwind CSS。這裡以 Vite 為例:

npm create vite@latest my-app -- --template vanilla
cd my-app
npm install

接著安裝 Tailwind CSS 相關套件:

  1. tailwindcss 核心庫,提供 Tailwind 的 utility class
  2. postcss CSS 處理工具,Tailwind 用它來轉換 CSS
  3. autoprefixer 為 CSS 自動加上瀏覽器前綴,如 -webkit-
# 安裝相關套件
npm install -D tailwindcss postcss autoprefixer
# 初始化設定檔
# 建立 Tailwind CSS 的設定檔 tailwind.config.js
# 加上 -p 參數會同時建立 PostCSS 的設定檔 postcss.config.js
npx tailwindcss init -p

2. 設定 Tailwind

修改 tailwind.config.js

module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}

建立 ./src/style.css 並加上 Tailwind 的 directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

3. 安裝 daisyUI

npm install daisyui

然後在 tailwind.config.js 中加入插件:

module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [require("daisyui")],
}

可選:設定主題

daisyui: {
themes: ["light", "dark", "cupcake", "synthwave", "dracula"],
},

三、開始使用:元件實戰範例

現在你已成功安裝與整合 Tailwind CSS + daisyUI,接下來讓我們透過實際範例來掌握它們的用法。

範例一:按鈕樣式

daisyUI 提供多種按鈕樣式,只需使用 btn class:

<button class="btn btn-primary">主要按鈕</button>
<button class="btn btn-secondary">次要按鈕</button>
<button class="btn btn-accent">強調按鈕</button>
<button class="btn btn-outline">外框按鈕</button>

也可以搭配大小與形狀:

<button class="btn btn-sm btn-circle btn-error">X</button>
<button class="btn btn-lg btn-square btn-info">i</button>

範例二:表單與輸入框

<div class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">使用者名稱</span>
</label>
<input type="text" placeholder="請輸入名稱" class="input input-bordered w-full" />
</div>

<div class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">選擇國家</span>
</label>
<select class="select select-bordered">
<option disabled selected>選擇一個選項</option>
<option>台灣</option>
<option>日本</option>
<option>美國</option>
</select>
</div>

範例三:卡片元件

<div class="card w-96 bg-base-100 shadow-xl">
<figure><img src="https://placeimg.com/400/225/tech" alt="科技圖" /></figure>
<div class="card-body">
<h2 class="card-title">科技卡片</h2>
<p>這是一個搭配 Tailwind CSS 與 daisyUI 所設計的卡片元件。</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">查看更多</button>
</div>
</div>
</div>

範例四:主題切換

daisyUI 預設支援多主題切換,可透過修改 <html>data-theme 屬性達成:

<html data-theme="dracula">

或者透過 JS 切換:

document.documentElement.setAttribute("data-theme", "light")

四、整合應用:登入頁設計

我們結合 daisyUI 元件製作一個登入表單:

<div class="min-h-screen bg-base-200 flex items-center justify-center">
<div class="card w-full max-w-sm shadow-2xl bg-base-100">
<div class="card-body">
<h2 class="text-2xl font-bold text-center mb-4">登入系統</h2>
<div class="form-control">
<label class="label">
<span class="label-text">電子郵件</span>
</label>
<input type="email" placeholder="you@example.com" class="input input-bordered" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">密碼</span>
</label>
<input type="password" placeholder="請輸入密碼" class="input input-bordered" />
</div>
<div class="form-control mt-6">
<button class="btn btn-primary">登入</button>
</div>
</div>
</div>
</div>

整合 Tailwind CSS 和 daisyUI 後這個表單完全不需要寫自訂 CSS,就能擁有一致的樣式與響應式設計。


五、總結與延伸學習

整合 Tailwind CSS 和 daisyUI 的好處在於:

  • 可以保有 Tailwind CSS 的彈性與控制權。
  • daisyUI 提供現成且風格統一的元件,開發速度大幅提升。
  • 可輕鬆切換主題,實現暗色模式等功能。
  • 支援任意框架,React、Vue、Svelte、Alpine.js 都適用。

Web 前端效能優化入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

在網頁開發中,效能優化是一個不可忽視的重要課題。無論是企業網站、單頁應用(SPA),或是電商平台,效能表現都直接影響使用者體驗與轉換率。

本篇筆記將介紹 Web 前端效能優化的核心概念、常見策略與實務範例,幫助你為專案建立良好的基礎。


為什麼需要前端效能優化?

前端效能不佳會導致:

  • 首次載入時間過長
  • 使用者等待過久,產生跳出行為
  • SEO 表現不佳(Google 會參考 LCP、CLS、FCP 等指標)

透過有效的優化策略,我們能讓網站更快、更穩、更吸引人。


效能優化的三個層面

  1. 載入效能(Loading Performance):提升頁面初始載入速度。
  2. 互動效能(Interaction Performance):優化點擊、滑動等互動的流暢度。
  3. 渲染效能(Rendering Performance):減少重繪、重排與動畫卡頓。

一、減少不必要的資源請求

使用 CDN

透過 CDN(Content Delivery Network)可將靜態資源分發到全球節點,加速載入。

<!-- 使用 Google Fonts CDN -->
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet" />

合併與壓縮資源(Minify & Bundle)

使用打包工具如 Webpack、Vite,可以:

  • 將 JS / CSS 壓縮(Minify)
  • 移除註解與空白
  • 合併多個檔案減少 HTTP 請求數量

範例(Webpack 設定簡略):

// webpack.config.js
module.exports = {
mode: 'production', // 自動啟用壓縮
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
};

二、圖片與多媒體優化

適當圖片格式

  • 使用 WebPAVIF 取代 JPEG / PNG,可減少檔案體積 25% 以上
  • SVG 適用於圖示與 icon,解析度不會失真

延遲載入圖片(Lazy Loading)

<img src="thumbnail.jpg" loading="lazy" alt="延遲載入圖片" />

或搭配 JavaScript 實現滾動載入。


三、有效使用 Cache(快取)

設定 Cache-Control 標頭

在伺服器上設定靜態資源快取策略:

Cache-Control: max-age=31536000, immutable

適用於版本化的資源檔案(如 main.123abc.js),可快取一年不變。


四、精簡 CSS 與 JavaScript

移除未使用的 CSS(Tree Shaking)

使用工具如 PurgeCSSTailwindCSS JIT Mode 可自動剔除沒用到的樣式。

PurgeCSS 使用方式(簡略):

const purgecss = require('@fullhuman/postcss-purgecss');

module.exports = {
plugins: [
purgecss({
content: ['./**/*.html'],
}),
],
};

延遲載入 JS(Defer / Async)

<script src="main.js" defer></script>
  • defer: 等 DOM 解析完才執行,不阻塞渲染
  • async: 載入完成即執行,適合非必要腳本(如 GA)

五、避免過度重排與重繪

使用 class 切換取代 style 修改

重複直接操作 DOM style 屬性會導致效能下降,改用 CSS class 控制樣式較佳。

// 不推薦
element.style.width = '100px';
element.style.height = '50px';

// 推薦
element.classList.add('resized');
.resized {
width: 100px;
height: 50px;
}

使用 transformopacity 進行動畫

避免透過 topleftwidth 等影響 layout 的屬性來做動畫。

/* 推薦做法:使用 transform */
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: scale(1.05);
}

六、最佳化 DOM 結構與渲染

  • 減少過深的 DOM 巢狀結構
  • 使用虛擬滾動(Virtual Scroll)載入大量列表
  • 避免頻繁操作 DOM,應該一次性改動(使用 DocumentFragment 或 requestAnimationFrame)

七、使用開發工具檢查效能

Chrome DevTools

  1. Lighthouse:提供整體效能建議
  2. Performance Panel:檢查 JS 執行、動畫、Layout shift 等問題
  3. Network Panel:觀察資源大小、載入順序與快取狀態

總結與建議實作順序

若你剛開始進行專案的效能優化,可以依照以下順序著手:

  1. 壓縮與合併 JS / CSS
  2. 圖片格式轉換與 Lazy Load
  3. CDN 部署靜態資源
  4. 移除未使用樣式與延遲載入腳本
  5. 改善動畫與 DOM 操作
  6. 導入快取策略
  7. 使用 Lighthouse 檢查並優化問題

效能優化並非一次性工作,而是一個持續調整的過程。建議我們可以從專案開始就納入效能考量,將它當成基本開發原則來實踐。

參考文件

  1. Core Web Vitals: An everyday explanation (Taiwanese with English subtitles)

Web 資訊安全入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

在現代網頁應用中,資安問題層出不窮,從簡單的跨站攻擊(XSS)到複雜的憑證竊取、身份偽造(CSRF),都可能導致個資外洩、系統遭入侵甚至企業商譽受損。

本篇筆記將從實務角度出發,介紹幾種常見 Web 資安風險及其防範策略,讓你能以最基本的方式保護網站與使用者安全。


一、常見 Web 資安威脅類型

1. XSS(Cross-Site Scripting,跨站腳本攻擊)

原理:攻擊者將惡意腳本注入至網站,當其他使用者瀏覽該頁面時,惡意腳本便會在其瀏覽器上執行,例如竊取 cookie、冒充使用者操作等。

實例:

<!-- 攻擊者輸入的留言內容 -->
<script>alert('你被 XSS 攻擊了');</script>

若網站未正確處理輸入,這段 JavaScript 就會直接被執行。

防範方式:

  • 對所有輸入進行 轉義(Escape)
  • 使用前端框架自動編碼機制(如 React 的 JSX)
  • 避免使用 innerHTML 插入未清洗的資料

範例(JavaScript 轉義文字):

function escapeHtml(str) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

2. CSRF(Cross-Site Request Forgery,跨站請求偽造)

原理:攻擊者引導使用者點擊惡意連結或載入圖片,使其在未察覺情況下對已登入的網站發送請求,進而竄改資料或觸發操作。

實例:

<!-- 攻擊者網站中隱藏圖片,觸發某網站的轉帳請求 -->
<img src="https://bank.com/transfer?amount=1000&to=attacker" />

如果使用者已登入 bank.com 且未防範 CSRF,就可能無意間轉帳。

防範方式:

  • 後端驗證 CSRF Token
  • 使用 SameSite Cookie 屬性
  • 僅允許 POST / PATCH / DELETE 操作變更狀態
  • 驗證 Referer / Origin 標頭(有風險)

範例(Express + CSRF Token):

const csrf = require("csurf");
const cookieParser = require("cookie-parser");

app.use(cookieParser());
app.use(csrf({ cookie: true }));

app.get("/form", (req, res) => {
res.render("form", { csrfToken: req.csrfToken() });
});

3. SQL Injection(SQL 注入)

原理:透過惡意輸入拼接 SQL 查詢語句,導致資料庫查詢異常,甚至刪除資料。

實例:

-- 假設未使用參數化查詢
SELECT * FROM users WHERE username = '' OR 1=1;

攻擊者輸入 ' OR 1=1; -- 可繞過認證邏輯,取得所有帳戶資料。

防範方式:

  • 使用 ORM 或預處理語句(Prepared Statement)
  • 不拼接 SQL 字串,改用參數綁定
  • 限制資料庫帳號權限

範例(Node.js + MySQL):

const username = req.body.username;
const password = req.body.password;

const query = "SELECT * FROM users WHERE username = ? AND password = ?";
connection.query(query, [username, password], function (err, results) {
// 安全地查詢
});

4. 資料傳輸未加密(缺少 HTTPS)

原理:若網站使用 HTTP,使用者資料(如帳號密碼、金流資訊)在傳輸過程中可能被中間人攔截(Man-in-the-Middle attack)。

防範方式:

  • 強制使用 HTTPS(導入憑證)
  • 使用 HSTS(HTTP Strict Transport Security)
  • 移除 HTTP 存取(使用 301 重導)

範例(NGINX 強制 HTTPS):

server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}

5. 檔案上傳風險

原理:攻擊者透過檔案上傳功能傳入惡意腳本(如 .php.jsp),若伺服器未阻擋,可能導致遠端代碼執行。

防範方式:

  • 限制可上傳的檔案類型與副檔名
  • 不讓使用者可直接存取上傳目錄
  • 使用 UUID 改名,避免原檔名執行

附加安全性強化機制

HTTP 安全標頭設定(Security Headers)

使用如 Helmet(Express.js 中間件)快速加入以下標頭:

  • Content-Security-Policy:防止 XSS 和資源注入
  • X-Frame-Options:防止點擊劫持(Clickjacking)
  • X-Content-Type-Options:避免 MIME 類型混淆
  • Strict-Transport-Security:強制 HTTPS

範例(Node.js + Helmet):

const helmet = require("helmet");
app.use(helmet());

總結與建議實作順序

如果剛開始建立 Web 專案,可以按照以下步驟檢查安全性:

  1. 輸入驗證與輸出轉義,防範 XSS
  2. 加入 CSRF Token 機制
  3. 資料庫查詢全面使用預處理語句
  4. 部署 HTTPS 並強制重導
  5. 使用 Helmet 加強安全標頭
  6. 定期檢查依賴套件漏洞(如 npm audit

資訊安全並非一勞永逸,而是一種持續維護的工作。建議我們可以將資安意識融入日常開發流程中,隨時更新資安知識與工具。

PostCSS 入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、什麼是 PostCSS?

PostCSS 是一個 CSS 的轉換工具(CSS Transformer),本身不是 CSS 預處理器(如 Sass)或框架(如 Tailwind CSS),但它可以透過「外掛(plugin)」的方式強化你的 CSS 工作流程。

簡單來說,PostCSS 是一個平台,讓你可以用 JavaScript 編寫規則,自動處理 CSS,像是:

  • 自動加上瀏覽器前綴(autoprefixer)
  • 支援未來 CSS 語法(例如 Nesting)
  • 壓縮 CSS、移除重複樣式
  • 搭配 Tailwind CSS 進行原子化設計

二、PostCSS 的運作原理

PostCSS 的流程如下:

  1. 將原始 CSS 解析為 AST(抽象語法樹)
  2. 透過插件對 AST 進行操作
  3. 將修改後的 AST 轉回 CSS 輸出

你可以選擇使用官方插件、社群插件,甚至自己寫 plugin。


三、如何安裝與設定 PostCSS

安裝(使用 npm)

npm install -D postcss postcss-cli autoprefixer

建立 PostCSS 設定檔 postcss.config.js

module.exports = {
plugins: [
require('autoprefixer'),
],
};

這樣 PostCSS 就會讀取這個設定檔,並在處理 CSS 時自動加入瀏覽器前綴。


四、基本使用流程

假設你有一個檔案 src/style.css

/* src/style.css */
:root {
--main-color: #4f46e5;
}

.btn {
display: flex;
background-color: var(--main-color);
user-select: none;
}

執行 PostCSS 處理:

npx postcss src/style.css -o dist/style.css

這會根據你的 postcss.config.js 處理並輸出到 dist/style.css

輸出結果可能為:

:root {
--main-color: #4f46e5;
}

.btn {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
background-color: var(--main-color);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

這就是 autoprefixer 自動補完各種瀏覽器相容性的結果。


五、常用插件介紹與範例

1. autoprefixer(最常見)

自動加上 CSS 瀏覽器前綴

npm install -D autoprefixer

可搭配 .browserslistrcpackage.json 設定支援目標:

> 1%
last 2 versions
not dead

2. postcss-preset-env

支援未來 CSS 語法,例如 Nesting、變數、邏輯函數等

npm install -D postcss-preset-env

更新設定檔:

module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 1,
}),
],
};

然後你就可以寫類似這樣的 CSS:

.btn {
color: white;

&:hover {
color: black;
}
}

經過 PostCSS 處理後會被轉成瀏覽器可讀的標準語法。


3. cssnano(壓縮 CSS)

這個插件會讓 CSS 變得更小,適合生產環境使用:

npm install -D cssnano
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'default',
}),
],
};

六、PostCSS 與 Tailwind CSS 的關係

Tailwind CSS 是建立在 PostCSS 基礎上的一個插件。因此在安裝 Tailwind 時你會看到:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

這個指令會產生 postcss.config.js

module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Tailwind 本身就是一個 PostCSS 插件,會將你在 HTML 或 JSX 中寫的 class 轉換成實際的 CSS。


七、PostCSS 與其他工具整合

搭配 Vite

Vite 專案中只要有 postcss.config.js 檔案,會自動載入設定,不需額外安裝。

搭配 Webpack

webpack.config.js 加入:

module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
}

八、自訂 PostCSS 插件(進階)

你也可以自己撰寫插件來操作 CSS AST:

module.exports = () => {
return {
postcssPlugin: 'my-plugin',
Declaration(decl) {
if (decl.prop === 'color' && decl.value === 'red') {
decl.value = 'blue';
}
},
};
};
module.exports.postcss = true;

這個簡單的插件會把 color: red 自動換成 color: blue


九、總結

PostCSS 雖然不像 Sass 有自己的語法,也不像 Tailwind 有明確的設計架構,但它是現代前端 CSS 處理流程中不可或缺的工具,擁有高度彈性與強大生態系。

我們可以根據需求只加幾個插件,也可以像 Tailwind 那樣完全建構在它上面。尤其當你的專案需要支援多瀏覽器、使用最新 CSS 語法、進行建置壓縮時,PostCSS 是最佳解法之一。

如果你正在使用 Vite、Next.js、或是 Tailwind CSS,幾乎都已內建 PostCSS 支援。了解它的運作方式能幫你更精準地控制前端樣式處理流程。

React Custom Hooks 入門教學 | w3schools 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

在 React 應用程式中,隨著功能的增加與維護需求的提升,我們常常會遇到多個元件需要共享相同邏輯的情況。為了避免重複撰寫相似程式碼、增加維護成本,React 提供了一個優雅的解法:自訂 Hook(Custom Hook)

自 React 16.8 版本開始,我們能使用 Hook 來管理狀態與副作用,而自訂 Hook 則是讓我們可以把邏輯封裝起來,像一般函式一樣在多個元件中重複使用。這篇文章將介紹如何從實際範例中將撰寫資料抓取的邏輯封裝成一個 Custom Hook,並展示其應用方式。


重點摘要

  • React Hook 是一種能在函式元件中使用的功能,讓你能使用狀態(state)與其他 React 功能。
  • Custom Hook 是自訂的 Hook 函式,其命名必須以 use 開頭(例如:useFetch)。
  • Custom Hook 讓我們可以封裝與重複使用邏輯,而不重複撰寫相同的程式碼。
  • 可透過傳入參數來讓 Hook 更具彈性與重用性。
  • 可將如資料抓取(fetching)、表單邏輯、事件監聽等邏輯封裝成 Custom Hook。

實作範例

初始範例:在元件中撰寫抓取邏輯

以下是一個簡單的 React 元件 Home,它在載入時從 JSONPlaceholder 抓取待辦事項(todo)資料,並顯示在頁面上:

// index.js
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';

const Home = () => {
const [data, setData] = useState(null);

useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then((res) => res.json())
.then((data) => setData(data));
}, []);

return (
<>
{data &&
data.map((item) => {
return <p key={item.id}>{item.title}</p>;
})}
</>
);
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Home />);

這段程式碼可以正常運作,但若我們之後在其他元件中也需要類似的資料抓取功能,就會發現邏輯無法重用,這時就是 Custom Hook 派上用場的時候。


抽出邏輯:撰寫 Custom Hook

我們可以將 fetch 的邏輯抽出成一個名為 useFetch 的自訂 Hook,放在 useFetch.js 檔案中:

// useFetch.js
import { useState, useEffect } from 'react';

const useFetch = (url) => {
const [data, setData] = useState(null);

useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data));
}, [url]);

return [data];
};

export default useFetch;

這個 Hook 會接收一個 URL 作為參數,並返回從該 URL 抓取到的資料。


使用 Custom Hook 的元件

有了 useFetch 之後,我們就可以在 Home 元件中直接使用這個 Hook,如下所示:

// index.js
import ReactDOM from 'react-dom/client';
import useFetch from './useFetch';

const Home = () => {
const [data] = useFetch('https://jsonplaceholder.typicode.com/todos');

return (
<>
{data &&
data.map((item) => {
return <p key={item.id}>{item.title}</p>;
})}
</>
);
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Home />);

說明與分析

這個自訂 Hook 的設計有以下幾個特點:

  1. 命名規則:遵守 React 規範,Hook 函式以 use 開頭(此例為 useFetch),才能被 React 正確識別為 Hook。
  2. 封裝邏輯:將 useStateuseEffect 的邏輯封裝在 useFetch 中,不再重複撰寫於元件內。
  3. 提高重用性:只要傳入不同的 URL,就能從其他 API 抓取資料,使用方式彈性且簡潔。
  4. 維護簡便:若日後需要修改資料抓取的邏輯,只需在 useFetch.js 中調整一次,即可影響所有使用該 Hook 的元件。

總結

自訂 Hook 是 React 開發中非常實用的工具,讓我們能夠撰寫模組化、可重複使用且容易維護的邏輯。當你在多個元件中發現重複的邏輯(如資料抓取、訂閱事件、表單處理等),就可以考慮將其封裝成一個 Custom Hook,提升程式碼品質與開發效率。

透過本文介紹的 useFetch 範例,你應該能夠掌握如何將常見的副作用邏輯抽離為一個可重用的 Hook,也歡迎在實務中嘗試建立更多適合自己應用場景的自訂 Hook。


如果你想了解更多關於資料抓取的技術細節,可以進一步學習 JavaScript Fetch API 的用法,並搭配錯誤處理(如 .catch())與 loading 狀態等進行擴充。這將使你的 Custom Hook 更加完善。

參考文件

  1. React Custom Hooks

React Hooks useMemo 入門教學 | w3schools 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在 React 的開發中,我們常會遇到一些運算成本高昂(expensive)的函式,例如複雜的計算或操作大量資料。如果這些運算每次重新渲染都會重新執行,可能會導致效能低落,甚至造成使用者界面的卡頓或延遲。此時,我們可以使用 React 的 useMemo Hook,透過「記憶化」(memoization)技術,避免不必要的重新計算,從而提升效能。

本文將說明 useMemo 的用途、使用時機,並透過實際範例展示其效能優化的實際效果。


重點摘要

  • useMemo 是 React 提供的 Hook,用來「記憶化」一個計算結果,僅在依賴(dependencies)變動時才重新計算。

  • 它的語法為:useMemo(() => { return value }, [dependencies])

  • useMemo 適合用在:

    • 資源密集的計算
    • 必須避免重複執行的運算
  • useMemouseCallback 類似,但:

    • useMemo 回傳的是 記憶化的值
    • useCallback 回傳的是 記憶化的函式
  • 搭配依賴陣列使用,確保只在必要時重新計算


範例一:未使用 useMemo,效能不佳

以下是一個基本範例,展示若每次重新渲染都執行一次昂貴的計算函式 expensiveCalculation,會造成延遲的情形:

import { useState } from 'react';
import ReactDOM from 'react-dom/client';

const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = expensiveCalculation(count); // 每次 render 都執行

const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => [...t, 'New Todo']);
};

return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => (
<p key={index}>{todo}</p>
))}
<button onClick={addTodo}>Add Todo</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</div>
);
};

const expensiveCalculation = (num) => {
console.log('Calculating...');
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

問題說明:

當我們只是點擊「Add Todo」按鈕,實際上並未更動 count,但昂貴的 expensiveCalculation(count) 卻仍然被重新執行,造成整體效能低落。


範例二:使用 useMemo 優化效能

接下來我們使用 useMemo 來包裝昂貴的計算函式,使其僅在 count 變動時才重新執行:

import { useState, useMemo } from 'react';
import ReactDOM from 'react-dom/client';

const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = useMemo(() => expensiveCalculation(count), [count]);

const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => [...t, 'New Todo']);
};

return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => (
<p key={index}>{todo}</p>
))}
<button onClick={addTodo}>Add Todo</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</div>
);
};

const expensiveCalculation = (num) => {
console.log('Calculating...');
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

優化結果:

  • 當你按下「Add Todo」時,不再觸發 expensiveCalculation
  • 僅當 count 變動時,才會重新執行昂貴的計算
  • 大幅減少不必要的運算與效能浪費

總結

useMemo 是 React 提供用來優化效能的重要工具,尤其在面對昂貴計算或複雜資料處理時,能有效避免不必要的運算。雖然 useMemo 不該過度使用於簡單運算,但在大型應用中,對於效能的提升非常有幫助。

使用時機建議:

  • 當某個函式或邏輯的執行成本高昂
  • 該邏輯只需在某些依賴值變化時執行一次
  • 搭配 useMemo 可減少重複運算、提升 UI 響應速度

記得:不要為了使用而使用,只有在效能瓶頸或實際需求下,useMemo 才能發揮其最大效益。

參考文件

  1. React Custom Hooks

常見 Web Frontend 前端工程師面試準備題目清單

· 閱讀時間約 15 分鐘
kdchang
  1. 掌握基礎知識:在開始解決複雜問題之前,您需要確保對前端開發的基礎知識有扎實的理解,包括 HTML、CSS 和 JavaScript,以及它們如何協作來創建響應式和互動式的網頁。如果您認為自己在這些主題上仍需學習,請參考前端學習路線圖。

  2. 練習寫程式:通過小型專案或在 LeetCode 和 HackerRank 等平台上解決問題,來提升您的前端寫程式技能。專注於與前端開發相關的挑戰。

  3. 學習現代框架(framework)和函式庫(library):了解如 React、Angular 或 Vue.js 等流行的框架和函式庫。掌握這些工具對於現代前端開發職位至關重要。

  4. 熟悉開發工作流程中的基礎工具:確保您對基本工具和實踐(例如版本控制工具 Git、測試工具(單元測試和整合測試)以及建構工具如 Vite)感到熟悉。這些對於任何前端角色來說都至關重要。

  5. 理解 UI/UX 原則:掌握基本的設計和用戶體驗概念可以讓您在前端開發者中脫穎而出。學習無障礙設計、響應式設計,以及如何創建直觀的界面。

  6. 準備作品集:設計和開發作品集或是 side project 可以幫助你展現你在前端的技術能力。

  7. 研究不同公司和產業:透過了解您面試的公司業務和產品,表現出對公司的興趣。準備一些問題在面試時提出,展示您對該角色的重視。

  8. 提升溝通能力:雖然這並非特定於前端開發,但好的溝通能力可以幫助我們與設計師和 PM 在工作上更容易溝通協調。

前端工程面試問題清單

Beginner Level

1. 什麼是 HTML/CSS 中的 id 和 class 的差別?

id 是用於唯一標識單個 HTML 元素的識別符號。class 則是一個可重複使用的識別符號,可以應用於多個元素。

HTML 中 id 與 class 的區別

  • 當需要通過 CSS 或 JavaScript 操作單個元素時,應該使用 id
  • 當需要操作一組 DOM 元素時,應該使用 class

在 CSS 中:

  • #id 選擇具有該 id 的特定元素。
  • .class 選擇具有該 class 的所有元素。

2. 什麼是 CSS 中的盒模型(Box Model)?

CSS 盒模型描述了在 DOM 中為元素生成的矩形框。盒模型由以下幾層組成:

  1. 內容(Content): 最內層,用於顯示文字和圖片。
  2. 內邊距(Padding): 內容與邊框之間的空間。
  3. 邊框(Border): 包圍元素的外緣,位於內邊距的外側。
  4. 外邊距(Margin): 邊框外部的空間,用於將該元素與其他元素分隔開。

通過分別控制每一層,您可以定義使用者介面中每個元素的外觀。

3. CSS 中 inline、inline-block 和 block 元素的區別

在 CSS 中,inline、inline-block 和 block 元素的區別在於它們在網頁中的呈現方式:

  1. Inline(行內元素):

    • 不具有寬度或高度的屬性。
    • 不會換行,僅佔據其內容所需的寬度。
    • 範例:<span><a>
  2. Inline-block(行內區塊元素):

    • 與 inline 元素類似,不會換行,但允許設定寬度和高度。
    • 範例:<img>
  3. Block(區塊元素):

    • 會從新行開始,默認佔據可用寬度的整行。
    • 可以自訂寬度和高度。
    • 範例:<div><p>

4. HTML 結構中的 SEO 最佳實踐

純 HTML 方面:

  1. 使用語義化的 HTML: 使用 <header><nav><main><article><section><footer> 等標籤,提供有意義的結構。
  2. 正確的標題層級: 正確使用 <h1><h6>,每個頁面僅使用一次 <h1> 作為主標題,其次是 <h2><h3><h6>等。
  3. Meta 標籤: 包含相關的 <meta> 標籤,例如描述(description)、關鍵字(keywords)和視口(viewport),提供網站的元數據。
  4. 圖片的 Alt 屬性: 為圖片添加描述性的 alt 屬性,以提高無障礙性和搜尋引擎的理解能力。

其他最佳實踐:

  1. 內部連結: 使用內部鏈接連接網站內的內容,幫助搜尋引擎爬取並了解網站結構。
  2. 行動裝置友好設計: 以行動優先的方式設計網站和 CSS,確保響應式設計以提升用戶體驗和搜尋引擎排名。
  3. 快速載入時間: 優化圖片、使用高效的代碼並利用快取來提升頁面載入速度,載入速度越快,在搜尋結果中越具競爭力。

5. 什麼是文件物件模型(DOM)?

文件物件模型(Document Object Model,DOM)是用於網頁文件的 API,它將 HTML 網頁的結構表示為一棵樹,其中每個節點對應於文檔中的一部分(例如元素、屬性或文本)。


6. 如何為元素添加事件監聽器?

要為元素添加事件監聽器,首先需要通過文檔物件的某種方法(如 getElementById)獲取該元素,然後使用該物件的 addEventListener 方法。

此方法需要接收三個參數:事件名稱(如 clickkeyupmouseup 等)、事件處理函數,以及可選的 boolean(指示是否在捕獲階段觸發事件)。


7. null 和 undefined 的區別是什麼?

在 JavaScript 中:

  • undefined 是新變數的默認值,表示變數已被定義,但尚未賦值。
  • null 是一個值,表示沒有值沒有物件,需要由開發者明確賦值給變數。

8. cookies、sessionStorage 和 localStorage 的區別是什麼?

  • Cookies:
    小型資料片段,儲存於瀏覽器中,主要用於在 HTTP 請求之間保留資訊,例如用戶身份驗證、會話管理和追蹤用戶行為。

  • sessionStorage:
    用於臨時儲存,只能在同一個 session 中訪問(即瀏覽器視窗或標籤頁開著時)。當瀏覽器視窗關閉時,數據會丟失。

  • localStorage:
    sessionStorage 類似,但資料在瀏覽器窗口或標籤頁關閉後依然存在,提供長期存儲功能。相較於 cookieslocalStorage 的大小限制更大,更適合儲存大資料集。


9. 瀏覽器如何渲染網站?

瀏覽器渲染網頁的過程包括以下幾個步驟:

  1. 解析 HTML。
  2. 解析 CSS 並應用樣式。
  3. 計算頁面佈局中每個元素的位置。
  4. 將實際像素繪製到屏幕上,並將它們排序到不同的圖層中。
  5. 組合所有圖層,根據 z-index 值、不透明度等渲染到屏幕上。
  6. 執行 JavaScript 代碼。
  7. 加載非同步資源。

10. 什麼是媒體查詢(Media Queries)?

媒體查詢是 CSS 中的一項功能,允許前端開發者根據設備或視口的各種特性應用不同的樣式。例如,根據設備的寬度、高度、方向或類型設定不同的樣式。

通過媒體查詢,可以實現響應式設計,使樣式適應不同的屏幕尺寸和設備能力。


中級知識

1. em 和 rem 單位的區別

EM 和 REM 在 CSS 中的比較
它們都是相對單位,但相對於的基準不同:

  • em:相對於父元素的字體大小。如果父元素的字體大小是 20px,設定 font-size: 2em 則等於 40px。
  • rem:相對於頁面根元素(<html> 元素)的字體大小。例如,若 <html> 的字體大小是 16px,則 1rem 等於 16px。

2. 如何建立 Flexbox 布局

建立 Flexbox 布局需要兩個主要步驟:

  1. 設置容器元素:display: flex; 屬性應用到容器元素上。
  2. 設定子元素的 Flex 屬性: 對容器內的每個元素設置 Flexbox 屬性,例如 flex: 1

3. CSS 特殊性(Specificity)的解釋及其運作原理

CSS 特殊性用於決定在樣式衝突時應該應用哪一組樣式。它遵循以下優先順序:

  1. 行內樣式(Inline style): 擁有最高優先權,會覆蓋其他樣式。
  2. ID 選擇器: 僅次於行內樣式,覆蓋其他類型的樣式。
  3. 類別選擇器(Class-based selectors): 覆蓋類型選擇器,但低於 ID 選擇器和行內樣式。
  4. 類型選擇器(Type selectors): 優先級最低,會被其他選擇器覆蓋。

4. 如何建立 CSS Grid 布局

建立 Grid 布局的步驟:

  1. 將包含元素設置為 display: grid
  2. 使用 grid-template-rowsgrid-template-columns 屬性定義網格的結構。
  3. 將元素放置於網格容器內,並使用 grid-columngrid-row 屬性指定位置。

5. 什麼是閉包(Closures),以及如何使用它們?

閉包是指當一個函數定義在另一個函數內時,即使外部函數已執行完畢,內部函數仍然可以訪問外部函數的變數和參數。
閉包的用途:

  • 創建私有變數: 內部函數可以訪問,但外部無法直接存取。
  • 實現複雜對象: 創建只有內部上下文可用的豐富數據結構。

6. 什麼是事件委派(Event Delegation)?

事件委派是一種在父元素上定義事件處理器的技術,用來處理子元素觸發的事件。
事件委派原理:
當事件被觸發時,它會沿著 DOM 層次結構向上冒泡,直到到達父元素的事件處理器。


7. 什麼是 Promise,如何運作?

Promise 是 JavaScript 中用於表示非同步操作最終完成(或失敗)的對象。

  • 用途: 通過 Promise,可以處理非同步操作的成功結果或失敗情況。
  • 工作原理: Promise 提供 .then().catch() 方法,分別用於處理成功和失敗的結果。

8. 如何優化網站資源以加快加載時間?

根據資源類型,使用不同的優化技術:

  • CSS 和 JavaScript 文件: 最小化並壓縮代碼。
  • 圖片: 使用如 JPEGOptim 或 ImageOptim 等工具壓縮圖片,確保過程中不損失質量。

9. 什麼是 Service Workers?它們的用途是什麼?

Service Workers 是在網頁應用程序背景中執行的腳本,與網頁主線程分開運作,提供以下功能:

  • 離線快取。
  • 推送通知。
  • 背景同步。

10. 什麼是同源政策(Same-Origin Policy)?

同源政策是瀏覽器中的一項安全功能,用於防止網站從其他網站訪問數據(如導入腳本或 API 請求)。

  • 用途: 防止惡意腳本竊取其他網站的敏感數據(如 cookies、本地存儲或內容)。
  • 解決方案: 使用跨源資源共享(CORS)。服務器需指定允許訪問的域名,並且客戶端應發送正確的標頭,雙方即可進行互動,即使不在同一域名下。

進階知識

1. 什麼是 CSS 變數?什麼時候會用到?

CSS 變數類似於前端程式語言中的變數,可以由開發者設置並在整個 CSS 樣式表中重複使用。

  • 優點: 將全局使用的值(如顏色)集中管理。例如,CSS 框架常用變數設置常量(如將黑色設置為 #222 而非 #000)。
  • 應用場景: 當網站需要一致性的設計風格或易於更新的全局樣式時。

2. 如何實現 Critical CSS 優化網頁的加載時間?

Critical CSS 是指將關鍵 CSS 規則從 CSS 文件中移除,並內嵌到網站的 <head> 元素中:

  1. 方法: 將頁面渲染所需的關鍵樣式直接嵌入到 HTML 文件的 <head> 中。
  2. 好處: 關鍵樣式能立即加載,減少渲染時間;非關鍵樣式則隨後加載(如主 CSS 文件)。

3. JavaScript 中的事件循環(Event Loop)是如何運作的?

事件循環是 JavaScript 的核心概念,允許執行非同步代碼。
運作過程:

  1. 調用棧(Call Stack): JavaScript 使用單線程執行代碼,函數按順序添加到調用棧,執行完畢後移除。
  2. 非同步操作: 非同步操作由瀏覽器的 Web API 處理,從調用棧中移除,單獨執行。
  3. 任務隊列(Task Queue): 當非同步操作完成後,回調函數被放入任務隊列中等待執行。
  4. 事件循環: 檢查調用棧是否為空,若為空,將任務隊列中的回調函數推入調用棧執行。

4. JavaScript 中的非同步操作處理方式有哪些?

JavaScript 提供了 4 種主要方法處理非同步操作:

  1. 回調函數(Callbacks): 當非同步操作完成後調用指定函數。
  2. Promises: 表示非同步操作最終完成的結果,使用 .then().catch() 處理成功或失敗情況。
  3. Async/Await: Promise 的進化語法,讓非同步代碼看起來像同步代碼,易於閱讀和維護。
  4. 事件監聽器(Event Listeners): 當特定事件(如用戶操作)觸發時調用回調函數。

5. 如何在單頁應用程式中管理狀態?

在沒有使用函式庫或是框架(如 React 或 Vue.js)的情況下,管理狀態相對複雜,可考慮以下方法:

  1. 全局變數: 使用全域變數或全域對象集中管理狀態,但大型應用難以維護。
  2. 模組模式(Module Pattern): 將狀態封裝到模組內,提供清晰的 API 來管理狀態。
  3. 發布/訂閱模式(Pub/Sub Pattern): 基於事件驅動的架構,分離狀態變更邏輯,更靈活但更複雜。
  4. 狀態管理庫: 使用像 Redux 這樣的庫,幫助統一管理應用的狀態。

6. 虛擬 DOM 的運作方式及其優勢

運作方式:

  1. 將用戶界面複製到記憶體中的「虛擬 DOM」,這是一個輕量級的 DOM 副本。
  2. 當狀態變化時,創建新的虛擬 DOM 並與舊版本進行比較(Diff 算法)。
  3. 系統計算出最小的更新操作,僅修改需要變更的節點,減少真實 DOM 操作。

優勢:

  • 性能優化: 通過減少 DOM 更新次數,降低 UI 的重排和重繪成本。
  • 跨平台支持: 虛擬 DOM 提供了應用與渲染 API 的抽象層,支持跨平台實現。
  • 一致性: 確保 UI 與內部狀態同步,減少錯誤和不一致情況。

7. 什麼是伺服器端渲染(SSR)?何時使用?

伺服器端渲染(SSR): 是由伺服器生成完整的 HTML,並將其發送給客戶端,而非在客戶端動態生成內容(即客戶端渲染,CSR)。

適用場景:

  • 內容驅動型網站: 如新聞網站、部落格等需要快速呈現內容的網站。
  • SEO 重視應用: 如果網站依賴於搜索引擎流量,SSR 可以改善 SEO 表現。
  • 漸進式 Web 應用: 需要快速加載的應用可使用 SSR 初始渲染,並在客戶端進行後續交互。

8. 如何分析並改進 Web 應用性能?

需要監控的核心指標包括:

  • 首次內容繪製(FCP): 首次內容呈現所需時間。
  • 最大內容繪製(LCP): 最大內容元素呈現所需時間。
  • 可交互時間(TTI): 網頁完全可交互所需時間。
  • 總阻塞時間(TBT): 主線程被阻塞的總時間。
  • 累積佈局偏移(CLS): 測量視覺穩定性。

9. 什麼是內容安全政策(CSP)?如何提高應用的安全性?

CSP(Content Security Policy): 一種安全標準,用於防止跨站腳本(XSS)和代碼注入攻擊。它通過定義和執行允許的資源來源白名單來運作。

優勢:

  • 增強安全性: 有效防禦 XSS 和數據注入攻擊。
  • 更高控制權: 開發者可細化政策來控制內容來源。
  • 符合規範: 幫助達成 OWASP Top 10 等安全合規要求。

10. 什麼是 Tree Shaking?如何提升 Web 應用性能?

Tree Shaking: 是 JavaScript 模組打包工具(如 Webpack、Vite)用來移除未使用代碼的技術。

優勢:

  • 減少打包大小: 移除無用代碼,減少發送給客戶端的資源量,加速加載。
  • 性能提升: 更小的打包大小使解析和執行代碼更快,提升應用響應速度。
  • 資源最佳化: 開發者可撰寫模組化代碼,而無需擔心未使用的依賴影響打包體積。

參考文件

  1. github developer-roadmap
  2. Top 30 Popular Front End Developer Interview Questions
  3. 2020 資深前端工程師面試心得(4y, 100k+)
  4. 最常見的前端面試題目
  5. 前端工程師面試問題集 - H5BP
  6. 前端工程師面試問題集
  7. 前端面試問題
  8. 前端面試技術考實戰分享-JS 篇
  9. QNAP/雷技/Yahoo/Synology-前端工程師-面試經驗分享
  10. 2024 資深前端工程師面試心得
  11. JavaScript Questions

React Hooks useCallback 入門教學 | w3schools 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在 React 中,每次元件重新渲染時,元件內部定義的函式也會被重新建立。這會導致某些子元件即使其 props 沒有變動,卻仍然會被重新渲染,造成效能浪費。useCallback Hook 是 React 提供的工具,用來記憶(memoize)函式的參考,以避免不必要的重新渲染。它的使用情境與 useMemo 類似,不同之處在於:useMemo 記憶的是「值」,而 useCallback 記憶的是「函式」。


重點摘要

  • useCallback 是 React 提供的 Hook,可回傳一個記憶化的函式
  • 記憶化的概念就像是快取(caching),只在依賴值變更時才重新建立函式。
  • useCallback 可改善效能,避免函式在每次重新渲染時被重新定義。
  • 常與 React.memo 搭配使用,防止不必要的子元件重新渲染。
  • useMemo 類似,但 useMemo 回傳的是「值」,useCallback 回傳的是「函式」。
  • 若函式沒有依賴到外部變數,也可以將依賴陣列設為空陣列([]),表示永遠不重新建立。

問題說明:為何子元件會重渲染?

在下列範例中,即使 todos 陣列沒有變動,點擊 + 按鈕後,子元件 Todos 仍會重新渲染:

// index.js
import { useState } from 'react';
import ReactDOM from 'react-dom/client';
import Todos from './Todos';

const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);

const increment = () => {
setCount((c) => c + 1);
};

const addTodo = () => {
setTodos((t) => [...t, 'New Todo']);
};

return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
// Todos.js
import { memo } from 'react';

const Todos = ({ todos, addTodo }) => {
console.log('child render');
return (
<>
<h2>My Todos</h2>
{todos.map((todo, index) => (
<p key={index}>{todo}</p>
))}
<button onClick={addTodo}>Add Todo</button>
</>
);
};

export default memo(Todos);

上述例子中,我們使用了 React.memo 包裹 Todos 元件,理論上當 todos 不變時,它不該重新渲染。然而,當你點擊「+」按鈕後 Todos 仍然會重渲染,原因在於:

參考相等性(Referential Equality)

每次 App 元件重新渲染時,addTodo 函式都會重新建立,導致其記憶位置不同。雖然 Todosprops.todos 沒變,但 props.addTodo 是一個新的函式,因此 React.memo 偵測到 props 有變動,就會導致子元件重新渲染。


解法:使用 useCallback 記憶函式

為了解決上述問題,可以使用 useCallback 記憶 addTodo 函式,讓它只在 todos 改變時才重新定義:

// index.js
import { useState, useCallback } from 'react';
import ReactDOM from 'react-dom/client';
import Todos from './Todos';

const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);

const increment = () => {
setCount((c) => c + 1);
};

const addTodo = useCallback(() => {
setTodos((t) => [...t, 'New Todo']);
}, [todos]);

return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

此時,當點擊 + 按鈕時,addTodo 函式不再被重新建立,因此 Todos 元件也不會被不必要地重新渲染。


總結

  • useCallback 是優化 React 應用效能的重要工具,尤其是在元件傳遞函式給子元件時。
  • 若你搭配 React.memo 使用,請務必搭配 useCallback,以避免函式參考不一致而導致的重渲染。
  • 不過也不要過度使用 useCallback,因為它本身也有效能成本,應該根據實際需求與效能瓶頸來使用。

在實務中,對於常被重複渲染、或複雜邏輯包裝的子元件來說,useCallback 能有效避免不必要的重新渲染,是撰寫高效 React 應用程式時不可忽視的工具。

參考文件

  1. React Custom Hooks

React Hooks useReducer 入門教學 | w3schools 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在 React 中,useState 是管理狀態的基礎 Hook,但當應用的狀態邏輯越來越複雜,像是涉及多個欄位更新、交叉依賴或分支邏輯,使用 useState 可能變得難以維護。這時,React 提供的 useReducer Hook 是一個更合適的選擇。

useReducer 的概念與 Redux 類似,它讓你將狀態管理邏輯集中在一個 reducer 函式中,透過 dispatch 發送動作來更新狀態,使邏輯清晰、可維護性高,非常適合用於中型到大型的應用。


重點摘要

  • useReduceruseState 類似,但更適合處理複雜邏輯或多狀態管理。

  • 語法格式為:const [state, dispatch] = useReducer(reducer, initialState)

  • reducer 是一個函式,根據傳入的 action 物件決定如何更新狀態。

  • 所有狀態更新都透過 dispatch(action) 進行。

  • 初始狀態 initialState 通常是物件或陣列。

  • 適合用在:

    • 表單資料管理
    • Todo List 或清單類資料
    • 複雜的元件邏輯

實際範例:使用 useReducer 建立 Todo 狀態管理

以下範例展示如何透過 useReducer 管理一個 Todo List 的完成狀態切換邏輯。

import { useReducer } from 'react';
import ReactDOM from 'react-dom/client';

// 初始狀態:包含兩個代辦事項
const initialTodos = [
{
id: 1,
title: 'Todo 1',
complete: false,
},
{
id: 2,
title: 'Todo 2',
complete: false,
},
];

// reducer 函式:根據 action 決定如何更新狀態
const reducer = (state, action) => {
switch (action.type) {
case 'COMPLETE':
return state.map((todo) => {
if (todo.id === action.id) {
return { ...todo, complete: !todo.complete }; // 切換完成狀態
} else {
return todo;
}
});
default:
return state; // 沒有匹配的 action 則回傳原狀態
}
};

// 元件:顯示代辦事項清單
function Todos() {
const [todos, dispatch] = useReducer(reducer, initialTodos);

// 處理 checkbox 切換
const handleComplete = (todo) => {
dispatch({ type: 'COMPLETE', id: todo.id });
};

return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input type="checkbox" checked={todo.complete} onChange={() => handleComplete(todo)} />
{todo.title}
</label>
</div>
))}
</>
);
}

// 渲染應用
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Todos />);

說明

  • initialTodos 是初始狀態,為一個陣列,包含代辦事項的 idtitlecomplete 狀態。
  • reducer 是一個純函式,根據傳入的 action 型別(此處為 "COMPLETE")來更新對應的 todo 狀態。
  • dispatch 是用來觸發 reducer 的函式,只要呼叫 dispatch({ type: "COMPLETE", id: todo.id }),就會依據 id 切換該 todo 的完成狀態。

延伸說明

上述範例僅實作了完成狀態的切換,但實務中 useReducer 更能發揮作用,因為你可以整合所有的 CRUD 操作邏輯於同一個 reducer 裡,例如:

case "ADD":
return [...state, newTodo];
case "DELETE":
return state.filter(todo => todo.id !== action.id);
case "UPDATE":
return state.map(todo => todo.id === action.id ? { ...todo, title: action.title } : todo);

這樣一來,整個應用的狀態更新都統一由 reducer 管理,讓邏輯集中且更容易除錯與擴充。


總結

useReducer 是 React 提供用來處理複雜狀態邏輯的重要工具。當你遇到以下情況時,建議考慮使用 useReducer

  • 多個狀態變數需要統一處理
  • 狀態轉換邏輯複雜且重複
  • 想將狀態管理從元件中抽離以提升可讀性

透過 useReducer,你可以實現更模組化、可維護的應用狀態邏輯,使開發效率更高、錯誤更少。

參考文件

  1. React Custom Hooks