跳至主要内容

70 篇文章 含有標籤「frontend engineer」

檢視所有標籤

整合 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

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

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

· 閱讀時間約 3 分鐘
kdchang

前言

在 React 中,useRef 是一個非常實用的 Hook,它提供了一個方法來在元件重新渲染之間保留值。不同於 useState,當 useRef 的值改變時不會觸發元件重新渲染,這使它成為追蹤狀態變化、儲存 DOM 參考或避免不必要重新渲染的理想選擇。

本文將深入說明 useRef 的三種主要用途:

  1. 避免重新渲染的狀態儲存
  2. 直接操作 DOM 元素
  3. 追蹤先前的狀態值

重點摘要

  • useRef 可用來在元件間保留值而不觸發重新渲染。

  • 常用於:

    • 儲存不可變的值或變數
    • 直接存取 DOM 元素(透過 ref
    • 記錄先前的狀態(例如輸入欄位的歷史值)
  • useRef() 回傳一個包含 .current 屬性的物件。

  • 修改 ref.current 不會導致畫面重繪,因此對效能影響小。


實際範例

範例 1:避免重新渲染的計數器

使用 useRef 追蹤畫面渲染次數(如果用 useState 反而會導致無限循環):

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

function App() {
const [inputValue, setInputValue] = useState('');
const count = useRef(0);

useEffect(() => {
count.current = count.current + 1;
});

return (
<>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<h1>Render Count: {count.current}</h1>
</>
);
}

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

說明:

  • useRef(0) 初始化 count.current 為 0。
  • 每次渲染後,useEffect 更新 count.current
  • 即使值變了,畫面不會重新渲染,這正是 useRef 的特性。

範例 2:存取 DOM 元素

在 React 中,大部分的 DOM 操作應交由框架管理,但在特定情況下,我們需要手動聚焦、選取或操作元素,這時 useRef 就派上用場。

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

function App() {
const inputElement = useRef();

const focusInput = () => {
inputElement.current.focus();
};

return (
<>
<input type="text" ref={inputElement} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}

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

說明:

  • 使用 ref={inputElement} 將 input 元素指向 useRef() 的回傳值。
  • 呼叫 inputElement.current.focus() 來讓輸入框聚焦。

範例 3:追蹤先前的狀態值

如果你想知道某個值在上一次渲染的狀態是什麼,可以使用 useRef 搭配 useEffect 來達成。

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

function App() {
const [inputValue, setInputValue] = useState('');
const previousInputValue = useRef('');

useEffect(() => {
previousInputValue.current = inputValue;
}, [inputValue]);

return (
<>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<h2>Current Value: {inputValue}</h2>
<h2>Previous Value: {previousInputValue.current}</h2>
</>
);
}

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

說明:

  • useRef 儲存上一個 inputValue
  • 每次 inputValue 改變時,useEffect 都會更新 previousInputValue.current
  • 在畫面上同時顯示目前值與上一個值。

總結

useRef 是 React 中一個簡單卻強大的工具,適用於以下情境:

  • 維持不影響渲染的狀態資料(例如計數器、定時器 ID、外部資料等)
  • 直接操作 DOM 元素(例如設定焦點、自動滾動)
  • 追蹤先前的狀態值或變數(例如表單內容變化)

理解並善用 useRef 可以幫助你更靈活地處理 React 元件中的各種非視覺狀態,讓應用程式更穩定、效能更佳。下次當你不需要觸發重新渲染時,請記得考慮使用 useRef


如果你需要我幫你將此內容轉換為 Markdown、部落格文章格式或簡報稿,也可以告訴我。

參考文件

  1. React Custom Hooks

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

· 閱讀時間約 3 分鐘
kdchang

前言

在 React 應用程式中,當需要在多個巢狀元件之間共享資料時,傳遞 props 是最基本的做法。但當元件層級變深,這樣的資料傳遞會變得繁瑣且難以維護,這種情況被稱為「props drilling」。為了解決這個問題,React 提供了 Context API,搭配 useContext Hook 可以讓你在不需要一層層傳遞 props 的情況下,輕鬆地在深層元件中讀取共享的狀態。


重點摘要

  • React Context 是一種全域狀態管理的工具。
  • 可以搭配 useState 使用,實現跨元件樹狀結構的資料共享。
  • 「prop drilling」是指一層層傳遞 props,容易造成程式碼混亂。
  • 使用 Context 包裝需要共享資料的元件樹,可避免不必要的傳遞。
  • useContext Hook 用於在任意元件中讀取指定 Context 的值。

問題背景與傳統寫法

假設我們有一個使用者名稱 user 的狀態,我們希望在最上層的元件設定這個狀態,並在最底層的第 5 個元件中使用它。傳統的做法會是逐層傳遞:

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

function Component1() {
const [user, setUser] = useState('Jesse Hall');

return (
<>
<h1>{`Hello ${user}!`}</h1>
<Component2 user={user} />
</>
);
}

function Component2({ user }) {
return (
<>
<h1>Component 2</h1>
<Component3 user={user} />
</>
);
}

function Component3({ user }) {
return (
<>
<h1>Component 3</h1>
<Component4 user={user} />
</>
);
}

function Component4({ user }) {
return (
<>
<h1>Component 4</h1>
<Component5 user={user} />
</>
);
}

function Component5({ user }) {
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}

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

雖然只有第 1 和第 5 個元件需要這個資料,但第 2 到第 4 個元件也被迫傳遞 props,造成不必要的耦合。


解決方案:使用 React Context 與 useContext Hook

步驟一:建立 Context

import { createContext } from 'react';

const UserContext = createContext();

這段程式碼建立了一個新的 Context,用來傳遞 user 的狀態。


步驟二:使用 Provider 包住需要資料的元件樹

function Component1() {
const [user, setUser] = useState('Jesse Hall');

return (
<UserContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 />
</UserContext.Provider>
);
}

UserContext.Provider 負責提供資料給子元件。所有被包住的子元件都能透過 useContext 取得 user 的值。


步驟三:在需要的元件中使用 useContext 取得資料

import { useContext } from 'react';

function Component5() {
const user = useContext(UserContext);

return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}

只需一行即可取得 Context 的值,避免層層傳遞 props


完整範例程式碼

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

// 建立 Context
const UserContext = createContext();

function Component1() {
const [user, setUser] = useState('Jesse Hall');

return (
<UserContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 />
</UserContext.Provider>
);
}

function Component2() {
return (
<>
<h1>Component 2</h1>
<Component3 />
</>
);
}

function Component3() {
return (
<>
<h1>Component 3</h1>
<Component4 />
</>
);
}

function Component4() {
return (
<>
<h1>Component 4</h1>
<Component5 />
</>
);
}

function Component5() {
const user = useContext(UserContext);

return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}

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

總結

透過 useContext Hook 搭配 Context API,你可以有效管理全域或區域性的狀態,避免繁瑣的 props 傳遞,讓元件間的溝通更加清晰與高效。這種模式特別適用於需要在多個巢狀元件中共享資料的情境,例如主題切換、登入狀態管理、使用者資料等。

熟練掌握 React Context 與 useContext,將大幅提升你在開發大型 React 應用的能力與維護性。

參考文件

  1. React Custom Hooks