跳至主要内容

37 篇文章 含有標籤「react」

檢視所有標籤

前端 i18n 入門教學與注意事項整理筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在現今的全球化應用中,網站或產品若希望觸及更多用戶,提供多語系支援幾乎是必須的功能。這就是所謂的國際化(Internationalization,簡稱 i18n),意即在程式設計階段預先做好結構設計,使系統能根據不同語言與地區的需求,自動載入對應的文案、格式與顯示方式。

本篇筆記將說明前端 i18n 的核心觀念、開發時常見的注意事項,以及如何透過實際程式碼實作一個簡單的多語系功能,協助你快速掌握前端 i18n 的基本功。


重點摘要:i18n 實作注意事項

  1. 避免硬編碼文字:所有顯示文字應抽離為 key-value 翻譯檔,便於日後維護與翻譯。
  2. 使用成熟 i18n 套件:例如 React 的 react-i18next、Vue 的 vue-i18n
  3. 結構化管理翻譯檔案:依功能模組分類翻譯內容,避免 key 混亂或重複。
  4. 支援變數插值與格式化:例如姓名、時間、數字等內容應透過參數傳遞給翻譯函數。
  5. 避免字串拼接組合句子:不同語言語序不同,拼接容易導致語意錯誤。
  6. 設計 UI 時預留文字空間:不同語言的字串長度可能差異很大。
  7. 處理 RTL 語言與排版:如阿拉伯語需設定 direction: rtl,必要時翻轉 UI。
  8. 提供語系切換機制與偵測:可從 navigator.language、URL、cookie 判斷語系。
  9. 設計 fallback 機制:若某語系未翻譯的 key,應自動 fallback 至預設語系。
  10. 翻譯流程建議自動化與工具化:搭配翻譯平台(如 Lokalise、Crowdin)管理翻譯流程與品質。

實作範例:使用 React + react-i18next 實現簡單的 i18n 功能

假設我們有一個需要支援中英文切換的 React 專案,以下將一步步實作基本功能。

步驟一:安裝相關套件

npm install i18next react-i18next i18next-browser-languagedetector

步驟二:建立翻譯檔案(放在 src/locales/

src/locales/en/translation.json

{
"greeting": "Hello, {{name}}!",
"home": {
"title": "Welcome to the homepage"
}
}

src/locales/zh/translation.json

{
"greeting": "哈囉,{{name}}!",
"home": {
"title": "歡迎來到首頁"
}
}

步驟三:初始化 i18n 設定(src/i18n.js)

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import en from './locales/en/translation.json';
import zh from './locales/zh/translation.json';

i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
zh: { translation: zh },
},
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});

export default i18n;

步驟四:在應用入口引入 i18n 設定(例如 index.js)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './i18n';

ReactDOM.render(<App />, document.getElementById('root'));

步驟五:在元件中使用翻譯

import React from 'react';
import { useTranslation } from 'react-i18next';

function HomePage() {
const { t, i18n } = useTranslation();

const changeLanguage = (lang) => {
i18n.changeLanguage(lang);
};

return (
<div>
<h1>{t('home.title')}</h1>
<p>{t('greeting', { name: 'KD' })}</p>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('zh')}>中文</button>
</div>
);
}

export default HomePage;

預期畫面輸出

使用者進入頁面,根據瀏覽器語言自動載入對應語系,或透過按鈕切換語言:

歡迎來到首頁
哈囉,KD!
[English] [中文]

總結

i18n 是每個想要「走向國際」的產品所必備的基礎建設之一。透過妥善設計的翻譯架構與工具整合,不僅能提升使用者體驗,也有助於日後擴展新市場與新語系。

建議開發者在專案初期就規劃好 i18n 架構,並搭配良好的團隊流程與翻譯管理工具,將繁瑣的翻譯作業系統化,避免日後重構的成本。

React 效能優化入門教學筆記 | 學習筆記

· 閱讀時間約 6 分鐘
kdchang

前言

隨著前端應用日益龐大,單頁應用(SPA)在初次載入時常面臨 JavaScript 檔案過大、載入時間過久的問題,導致使用者等待時間過長、效能下降。為了解決這個問題,React 與現代建構工具(如 Webpack、Vite)提供了 Code Splitting(程式碼分割)與 Lazy Loading(延遲載入)兩種策略,協助開發者更有效地管理與優化應用程式的載入流程。

程式碼分割(Code Splitting)和惰性載入(Lazy Loading)都是用來優化網頁效能的方法,它們都旨在減少初始加載時間,但實現方式和目標略有不同。 程式碼分割是將程式碼分割成多個較小的塊,而惰性載入則是在需要時才加載這些塊。

一、程式碼分割(Code Splitting)

概念:

程式碼分割是將一個大型的 JavaScript 應用程式分割成多個較小的、獨立的塊,每個塊包含應用程式的一部分程式碼。 這些塊通常是根據路由、元件或功能來分割的。

目標:

主要目標是減少應用程式的初始加載時間,通過只加載使用者當前需要的程式碼塊,而不是一次性加載所有程式碼。

實現方式:

程式碼分割通常使用打包工具(如 Webpack、Rollup 等)和動態import()語法來實現。

使用時機:

在編譯時(build time)就進行分割。

二、惰性載入(Lazy Loading)

概念:

惰性載入是指在需要的時候才加載程式碼,而不是在應用程式初始化時就加載所有程式碼。

目標:

減少應用程式的初始加載時間,特別是對於大型應用程式或元件。

實現方式:

惰性載入通常使用 React.lazy 和 Suspense 元件來實現,也可以配合程式碼分割一起使用。

使用時機:

在執行時(runtime)才加載,通常是當使用者訪問某個路由、觸發某個事件或需要顯示某個元件時。

差異總結

特性程式碼分割 Code Splitting惰性載入 Lazy Loading
概念將程式碼分割成多個塊在需要時才加載程式碼
目標減少初始加載時間,優化效能減少初始加載時間,優化效能
實現方式打包工具,dynamic import()React.lazy, Suspense, dynamic import()
時機編譯時執行時
關聯性程式碼分割是惰性載入的基礎,惰性載入可以利用程式碼分割的結果。

重點摘要

  • Code Splitting(程式碼分割)

    • 是一種將整個應用程式切割成多個檔案的技術
    • 通常由 Webpack、Rollup 等建構工具自動處理
    • 可應用於 route-based 分割、component-based 分割等情境
    • 不代表一定是延遲載入,僅是結構上的切割
  • Lazy Loading(延遲載入)

    • 是一種執行時載入程式碼的策略
    • 常與 import() 搭配,直到實際使用時才載入
    • 通常透過 React.lazySuspense 實現元件的懶載入
    • 是 Code Splitting 的使用方式之一
  • 兩者關係

    • Code Splitting 是靜態建構階段的優化策略
    • Lazy Loading 是執行階段的載入行為
    • Lazy Loading 必須建立在已做 Code Splitting 的前提上
  • 效益

    • 減少主程式 bundle 的大小
    • 提升首次載入速度(First Contentful Paint)
    • 延遲不必要的資源載入,節省頻寬與記憶體

實際範例

範例一:傳統未分割的情況(單一 bundle)

// App.js
import HomePage from './HomePage';
import Dashboard from './Dashboard';

function App() {
return (
<>
<HomePage />
<Dashboard />
</>
);
}

這樣寫會導致 HomePage 和 Dashboard 在應用一開始就被載入,無論使用者有沒有看到這些元件。


範例二:使用 React.lazy 實現 Lazy Loading 與 Code Splitting

// App.js
import React, { Suspense } from 'react';

// Lazy Loading:只有在渲染時才動態 import
const HomePage = React.lazy(() => import('./HomePage'));
const Dashboard = React.lazy(() => import('./Dashboard'));

function App() {
return (
<Suspense fallback={<div>載入中...</div>}>
<HomePage />
<Dashboard />
</Suspense>
);
}

使用 React.lazy() 搭配 import() 會讓 Webpack 將這些元件建立為獨立的 chunk。 真正渲染時(如使用者切換頁面),才會觸發載入行為,減少初始 bundle 體積。


範例三:Route-based Code Splitting(React Router)

// AppRouter.js
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

export default function AppRouter() {
return (
<Router>
<Suspense fallback={<div>頁面載入中...</div>}>
<Switch>
<Route path="/home" component={Home} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Suspense>
</Router>
);
}

使用路由為單位切割頁面元件,是最常見的 Code Splitting 實務做法。


範例四:動態 import 實作非元件的延遲載入

// utils.js
export function heavyCalculation(input) {
// 假設這段計算非常耗時
return input ** 10;
}
// App.js
function handleClick() {
import('./utils').then(({ heavyCalculation }) => {
const result = heavyCalculation(5);
console.log(result);
});
}

在某些不需要立即執行的邏輯或大型工具函式庫,也可以透過 import() 動態載入來延遲其成本。


常見問題與補充

Q1:Code Splitting 是自動的嗎?

  • 大部分情況下需要手動設計入口點(如 React.lazyimport()),Webpack 才會建立分離的 chunk。

Q2:只有使用 React.lazy 才能 Lazy Load 嗎?

  • 不一定,import() 是底層機制,也可配合其他框架(Vue、Svelte)或工具(React Loadable)使用。

Q3:懶載入的元件可以預載嗎?

  • 可以,透過 import().then() 觸發一次即可放進瀏覽器快取,達到「預熱」效果。

總結

在前端應用越來越大型化的今天,掌握 Code Splitting 與 Lazy Loading 的差異與使用場景,已成為每位前端工程師的必備技能。Code Splitting 解決的是「結構上的模組分離」,Lazy Loading 則是「載入時機的延後」。兩者密不可分,但用法與思考層次不同。

實務上可先針對頁面級路由進行分割,再進一步優化元件級的載入、工具模組載入時機,逐步降低初始 bundle 體積,提升網站效能與使用者體驗。

參考文件

  1. React | 為太龐大的程式碼做 Lazy Loading 和 Code Splitting

React 效能優化入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

React 作為現代前端開發的主流函式庫之一,強調 UI 的組件化與狀態驅動式渲染。然而,隨著應用規模擴大與資料變得動態頻繁,React 應用可能出現重新渲染過多、載入過慢或記憶體占用過高等問題,影響使用者體驗與開發效率。為此,瞭解與掌握 React 的效能優化技巧,成為中高階開發者的重要功課。

本篇筆記將介紹 React 效能優化的核心原則與常見實作方式,搭配簡單的程式碼範例說明實際操作,協助你建立清晰的優化思維與實作經驗。


重點摘要

  • 避免不必要的重新渲染

    • 使用 React.memo 包裹純函式組件
    • 適當使用 useMemouseCallback 記憶運算結果或函式引用
  • Lazy loading(Code Splitting)

    • 使用 React.lazySuspense 實現組件按需載入
  • 列表渲染優化

    • 提供穩定的 key,避免 diff 錯誤導致重繪
    • 處理大量資料時可結合虛擬化工具(如 react-window
  • 狀態管理與邏輯分離

    • 將全域狀態與 UI 狀態分離,減少重渲染範圍
    • 減少 props 傳遞鏈,避免深層組件無謂更新
  • 避免 inline 宣告與函式

    • 每次 render 都會產生新函式或物件,導致子組件重新渲染
  • 效能分析與工具

    • 使用 React DevTools 的 Profiler 模組分析 render 開銷
    • 善用 Chrome DevTools、Lighthouse 等協助調校效能

實際範例

1. 避免不必要的渲染:使用 React.memo

// 子元件
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
console.log('Render:', todo.text);
return (
<li>
<input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} />
{todo.text}
</li>
);
});

若未使用 React.memo,即使 todo 資料未變,只要父層重新 render,TodoItem 就會跟著重新 render。使用 React.memo 可避免這種不必要的重新渲染。


2. 函式記憶:使用 useCallback

const onToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo))
);
}, []);

如果 onToggle 每次 render 都重新宣告,會導致 React.memo 判斷 props 改變,從而重新渲染子元件。使用 useCallback 可以保留函式參考的一致性。


3. 虛擬滾動列表:使用 react-window

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => <div style={style}>Row {index}</div>;

const MyList = () => (
<List height={300} itemCount={1000} itemSize={35} width={300}>
{Row}
</List>
);

react-window 提供虛擬滾動的能力,只 render 可視範圍內的項目,大幅減少 DOM 結點,提高大數據列表效能。


4. 懶載入元件:使用 React.lazy

import React, { Suspense } from 'react';

const Chart = React.lazy(() => import('./Chart'));

function Dashboard() {
return (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
);
}

將大型組件分割成懶載入模組,可避免初次載入體積過大,提升頁面初始加載速度。


5. 使用 Profiler 分析效能瓶頸

React DevTools 提供 Profiler 模組,可追蹤各元件 render 時間與次數,有助於識別過度渲染或效能低落的元件。

import { Profiler } from 'react';

<Profiler
id="TodoList"
onRender={(id, phase, actualDuration) => {
console.log(`${id} rendered in ${actualDuration}ms`);
}}
>
<TodoList todos={todos} />
</Profiler>;

結語

React 效能優化並非一蹴可幾,而是需隨著應用規模與需求不斷調整與改善的過程。透過理解 Virtual DOM 的運作原理、掌握各種 Hook 的特性,以及活用分析工具,我們可以更有策略地針對效能瓶頸逐步優化,打造流暢且可維護的使用者體驗。

建議從小型優化(如 React.memouseCallback)著手,並逐步引入懶載入與虛擬化等進階技巧,讓 React 應用能夠隨著功能擴展持續保持高效能。

如果你對特定效能問題有興趣,例如圖片載入優化、CSR vs SSR 效能比較等,也可以再深入探討不同的進階主題。

React 效能優化 SOP 檢核清單入門教學筆記 | 學習筆記

· 閱讀時間約 6 分鐘
kdchang

前言

在大型單頁應用(SPA)與複雜互動式介面中,效能瓶頸常常來自不必要的重新渲染、大型 bundle 導致的載入緩慢,以及過度操作 DOM 所造成的卡頓。建立一份標準化的「效能優化檢核清單」(SOP,Standard Operating Procedure),能夠在開發流程中明確指出應檢查的重點、落實最佳實踐,並透過持續監控與回饋,進一步強化團隊的效能意識與程式品質。

本篇筆記將依照從「程式撰寫到部署」的不同階段,提出具體的檢核項目,並搭配最常見的 React 效能優化技術範例,協助你快速掌握如何在日常開發與 Code Review 中落實效能優化。


重點摘要

  • 一、避免不必要的重新渲染

    • 使用 React.memo 包裹純函式元件
    • 針對函式與物件 props,使用 useCallbackuseMemo 進行記憶
    • 避免 JSX inline 宣告函式或物件
  • 二、State 管理與元件分離

    • 下放 state 至影響範圍最小的元件
    • UI 狀態(開關、Modal 等)與業務資料分離
    • 避免全域 context 過度包覆,導致大範圍 re-render
  • 三、列表與大量資料渲染優化

    • 確保 key 穩定(使用唯一 id,非 index)
    • 採用虛擬滾動(react-windowreact-virtualized
    • 分頁或懶加載機制
  • 四、Code Splitting 與懶載入

    • 使用 React.lazy + Suspense 分割大型元件
    • 路由層級拆分,動態 import()
    • 圖片與第三方資源延遲加載
  • 五、效能分析與監控工具

    • React DevTools Profiler:分析元件 render 次數與耗時
    • Lighthouse / Web Vitals:追蹤 FCP、LCP、TTFB 等指標
    • Bundle 分析(Webpack Bundle Analyzer、Source Map Explorer)
  • 六、CI/Code Review 效能檢查

    • 將檢核清單納入 Pull Request 模板
    • 自動化檢測 bundle size 變化
    • 定期性能測試腳本(Cypress、Playwright + Lighthouse)

實際範例

範例一:避免不必要的重新渲染

// ChildComponent.jsx
import React from 'react';

function ChildComponent({ data, onClick }) {
console.log('ChildComponent render');
return <div onClick={onClick}>{data.text}</div>;
}

export default React.memo(ChildComponent);
// ParentComponent.jsx
import React, { useState, useCallback, useMemo } from 'react';
import ChildComponent from './ChildComponent';

export default function ParentComponent({ initialData }) {
const [count, setCount] = useState(0);

// useMemo 記憶 data 物件,避免因父組件重新 render 而改變 reference
const data = useMemo(
() => ({
text: initialData,
}),
[initialData]
);

// useCallback 記憶函式,不會因為 count 變化而重新建立
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);

return (
<div>
<p>點擊次數:{count}</p>
<ChildComponent data={data} onClick={handleClick} />
</div>
);
}

檢核點

  • ChildComponent 是否用 React.memo 包裹?
  • data 物件是否用 useMemo
  • onClick 是否用 useCallback

範例二:列表虛擬化

// ListView.jsx
import React from 'react';
import { FixedSizeList as List } from 'react-window';

const Row = React.memo(({ index, style }) => <div style={style}>列表項目 #{index}</div>);

export default function ListView() {
return (
<List height={400} itemCount={10000} itemSize={35} width={'100%'}>
{Row}
</List>
);
}

檢核點

  • 是否針對長列表導入虛擬化?
  • itemSize 與 height 設定是否合理?

範例三:Code Splitting 與懶載入

// routes.jsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

export default function AppRouter() {
return (
<Router>
<Suspense fallback={<div>載入中...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Suspense>
</Router>
);
}

檢核點

  • 是否有針對路由或大型元件進行懶載入?
  • fallback UI 是否友善?

範例四:效能分析

import React, { Profiler } from 'react';
import TodoList from './TodoList';

function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} ${phase} 耗時:${actualDuration.toFixed(2)}ms`);
}

export default function App() {
return (
<Profiler id="TodoList" onRender={onRenderCallback}>
<TodoList />
</Profiler>
);
}

檢核點

  • 是否使用 Profiler 區隔並記錄核心元件耗時?
  • 是否定期檢視開發者工具數據?

總結

以上檢核清單涵蓋了從程式撰寫、元件切分,到效能分析與持續監控的各個面向。建議將此清單整合至 Pull Request 模板中,並在團隊中推廣效能優化文化。持續在日常開發中落實這些檢查,能確保應用在功能增長的同時仍保持流暢的使用者體驗,並降低潛在的性能退化風險。若需將本文轉為 Markdown、PDF 或 Notion 模板,歡迎隨時提出。

補充:React 效能優化 SOP 檢核清單

一、避免不必要的重新渲染

  • 是否使用 React.memo 包裹純函式元件?
  • 是否有使用 useCallback 記憶傳遞的函式 props?
  • 是否有使用 useMemo 記憶計算結果,避免重複計算?
  • 是否避免在 JSX 中直接宣告函式或物件(例如 inline style)?

二、State 管理與元件分離

  • 是否將狀態下放至最小影響範圍的元件中?
  • 是否避免使用不必要的 lifting state up?
  • 是否將 UI 狀態(如開關、hover 狀態)與全域狀態分離?

三、Props 傳遞與結構優化

  • 是否控制 props 深層傳遞導致的層層 re-render?
  • 是否 props 結構穩定、可預期?(避免 object/array 每次都變)

四、渲染大量資料時的處理

  • 是否針對長列表使用虛擬化工具(如 react-window, react-virtualized)?
  • 是否有合理使用 key(且為穩定值,例如 id 而非 index)?

五、資源載入與 Code Splitting

  • 是否使用 React.lazy + Suspense 實現元件懶載入?
  • 是否進行 route-based code splitting(使用動態 import)?
  • 是否有壓縮圖片、延遲圖片載入(lazy loading)?

六、效能監控與分析

  • 是否使用 React DevTools Profiler 檢查 render 頻率與時間?
  • 是否分析過 Lighthouse / Web Vitals 的效能指標?
  • 是否檢查 Bundle Size(Webpack 分析工具、SourceMap Explorer 等)?

七、避免常見陷阱

  • 是否避免每次 render 都新建匿名函式?
  • 是否避免重複 render 同一資料來源?
  • 是否避免過度依賴 context 導致全頁重 render?

八、開發階段優化習慣

  • 是否將效能優化納入 Code Review 檢查點?
  • 是否每個大型新元件都確認是否會引起不必要渲染?
  • 是否測試過主流程在弱網或低效能設備上的表現?

useSWR 入門教學筆記:打造高效、簡潔的資料請求方式 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在現代前端開發中,資料的取得與管理是不可或缺的一環。傳統上,我們可能使用 useEffect 搭配 fetchaxios 來處理資料請求,但這樣的方式不僅冗長,還需要手動管理 loading、error 狀態與快取邏輯。為了解決這些問題,Vercel 推出的 SWR(stale-while-revalidate)提供了一種簡潔、聲明式且高效的資料取得方式,特別適合搭配 React 應用開發。

本文將介紹 SWR 的核心觀念、使用方式與基本範例,幫助我們快速上手並應用於實務開發中。


重點摘要

  • SWR 是什麼?

    • SWR 是由 Vercel 開發的 React Hooks 函式庫,提供資料快取與同步機制。
    • 名稱來自 HTTP 快取策略 “stale-while-revalidate”,意指:先顯示舊資料,再重新驗證更新資料
  • 為什麼要使用 SWR?

    • 自動處理資料快取與重新驗證。
    • 簡化資料請求邏輯,減少樣板程式碼。
    • 支援多種進階功能(錯誤重試、revalidate on focus、polling 等)。
  • 基本用法

    • 使用 useSWR(key, fetcher) 進行資料請求。
    • key:唯一識別資料來源的 key,通常為 API 路徑。
    • fetcher:資料請求函式,可使用 fetchaxios 實作。
  • 常見功能

    • isLoadingerror 狀態管理。
    • 自動重試與重新整理資料。
    • 快取與全域共用資料(Shared cache)。
    • 手動重新驗證資料(revalidate)。
    • 支援 SSR、Pagination、Mutation 等進階功能。

實際範例:取得 GitHub 使用者資料

1. 安裝 SWR

npm install swr
# 或使用 yarn
yarn add swr

2. 撰寫 fetcher 函式

// libs/fetcher.ts
export const fetcher = (url: string) => fetch(url).then((res) => res.json());

3. 在元件中使用 useSWR

// pages/UserProfile.tsx
import useSWR from 'swr';
import { fetcher } from '../libs/fetcher';

const UserProfile = () => {
const { data, error, isLoading } = useSWR('https://api.github.com/users/octocat', fetcher);

if (isLoading) return <div>載入中...</div>;
if (error) return <div>載入失敗:{error.message}</div>;

return (
<div>
<h1>{data.name}</h1>
<p>GitHub:{data.login}</p>
<p>Followers:{data.followers}</p>
<img src={data.avatar_url} width={100} />
</div>
);
};

export default UserProfile;

4. 手動重新驗證資料

const { data, mutate } = useSWR('/api/data', fetcher);

// 手動刷新資料
const handleRefresh = async () => {
await mutate();
};

5. 搭配條件式載入

const shouldFetch = userId !== null;

const { data } = useSWR(shouldFetch ? `/api/users/${userId}` : null, fetcher);

6. 自訂快取與設定

import useSWR from 'swr';

const { data, error } = useSWR('/api/data', fetcher, {
refreshInterval: 10000, // 每 10 秒重新抓資料
revalidateOnFocus: true, // 回到畫面時自動刷新
dedupingInterval: 5000, // 阻止過於頻繁的 API 請求
});

總結

SWR 提供了一種優雅、聲明式的方式來管理 React 應用中的資料請求與快取,不僅能有效簡化程式碼,還能提高使用者體驗與應用效能。其彈性與擴充性也適合應用於中大型專案中。

當我們熟悉了 SWR 的基本用法後,接下來也可以進一步探索以下功能:

  • Mutation API:用於資料寫入後手動更新快取。
  • 依賴 key 的動態載入:搭配 router 參數動態請求資料。
  • 全域快取策略自訂(SWRConfig):統一設定所有請求的行為。

透過 SWR,我們不再需要手動處理快取與副作用邏輯,只需專注於資料的呈現與邏輯本身,是開發現代 React 應用的絕佳利器。

參考文件

  1. React Hooks for Data Fetching

React Context API 入門教學 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

React Context API 是 React 提供的一種方式,讓我們能夠在組件樹中傳遞資料,而不需要一層層地使用 props。Context API 可以解決多層嵌套組件的傳遞問題,讓我們在深層組件中輕鬆訪問到全局狀態。本文將介紹如何使用 React Context API,並提供一個簡單的範例來展示其實際應用。

什麼是 React Context API

React Context API 是 React 的一個內建功能,它可以讓我們在組件樹中共享資料,避免多層嵌套的 props 傳遞。Context 主要由三個部分組成:

  1. React.createContext():創建一個 Context 物件。
  2. Provider:這是 Context API 中的一個組件,它用來包裹整個應用,並提供一個全局的資料源。
  3. Consumer:這是用來訪問 Context 資料的組件,它能夠獲取 Provider 中傳遞的資料。

使用 Context 的目的,是為了避免將相同的資料層層傳遞到每個組件,這樣可以讓應用的資料流變得更加簡潔。

使用 Context API 的步驟

步驟 1: 創建 Context

首先,我們需要使用 React.createContext() 來創建一個 Context 物件。這個物件會返回一個 ProviderConsumer 組件,讓我們在應用中使用。

import React from 'react';

// 創建 Context
const MyContext = React.createContext();

步驟 2: 使用 Provider 來傳遞資料

Context 的 Provider 是用來包裹應用的,它會接收一個 value 屬性,這個屬性就是要共享給整個組件樹的資料。

const App = () => {
const [user, setUser] = React.useState({ name: 'John', age: 30 });

return (
<MyContext.Provider value={user}>
<UserProfile />
</MyContext.Provider>
);
};

在這個範例中,我們將一個 user 物件傳遞給 MyContext.Providervalue 屬性,這樣整個組件樹中的所有子組件都能夠訪問到這個 user 資料。

步驟 3: 使用 Consumer 來接收資料

在需要使用資料的地方,我們可以使用 MyContext.Consumer 來獲取資料。Consumerchildren 是一個函數,它會接收一個 value 參數,這個參數就是在 Provider 中傳遞的資料。

const UserProfile = () => {
return (
<MyContext.Consumer>
{(user) => (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
)}
</MyContext.Consumer>
);
};

在這個範例中,UserProfile 組件通過 Consumer 來訪問 MyContext 中的 user 資料,並渲染顯示用戶的名字和年齡。

步驟 4: 使用 useContext Hook (React 16.8 及以上)

React 16.8 引入了 useContext Hook,這樣我們可以更方便地在函數組件中使用 Context,而不需要使用 Consumer。這樣的寫法更加簡潔,並且避免了過多的嵌套。

import React, { useContext } from 'react';

const UserProfile = () => {
const user = useContext(MyContext);

return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
);
};

使用 useContext 可以直接從 Context 中獲取資料,而不需要使用 Consumer。這使得代碼更簡潔,並提高了可讀性。

實際範例

下面是一個完整的範例,展示了如何使用 React Context API 來管理應用中的全局狀態。這個範例將包括一個用戶資料的管理,並能夠在多個組件中共享這些資料。

import React, { useState, useContext } from 'react';

// 創建 Context
const MyContext = React.createContext();

const App = () => {
const [user, setUser] = useState({ name: 'John', age: 30 });

return (
<MyContext.Provider value={user}>
<div>
<UserProfile />
<AgeUpdater />
</div>
</MyContext.Provider>
);
};

const UserProfile = () => {
const user = useContext(MyContext);

return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
);
};

const AgeUpdater = () => {
const user = useContext(MyContext);
const setUser = useState()[1];

const updateAge = () => {
setUser({ ...user, age: user.age + 1 });
};

return (
<div>
<button onClick={updateAge}>Increase Age</button>
</div>
);
};

export default App;

範例解析

  1. App 組件:在 App 組件中,我們使用 useState 定義了一個 user 資料,並通過 MyContext.Provider 將資料提供給下層組件。
  2. UserProfile 組件UserProfile 使用 useContext 來讀取 MyContext 中的資料,並顯示用戶的名字和年齡。
  3. AgeUpdater 組件:這個組件同樣使用 useContext 來讀取和更新 user 資料。我們在這裡定義了一個按鈕,當按下時,會更新 user 的年齡。

Context API 的優缺點

優點:

  1. 簡化資料傳遞:當我們需要在多層嵌套的組件中共享資料時,使用 Context 可以避免繁瑣的 props 傳遞。
  2. 可擴展性:Context 非常適合用於應用中的全局狀態管理,像是用戶認證、語言設置、主題樣式等。

缺點:

  1. 重新渲染問題:當 Provider 中的資料變更時,所有使用該 Context 的組件都會重新渲染。對於大型應用來說,這可能會影響性能。
  2. 狀態過度共享:Context 主要用於共享全局資料,如果將太多不相關的資料放入同一個 Context,可能會使代碼變得難以維護。

總結

React Context API 是一個強大的工具,可以幫助我們管理應用中的全局狀態。在適當的情況下使用 Context 可以大大簡化代碼,避免深層嵌套的 props 傳遞。但也需要謹慎使用,避免過多不必要的資料共享,從而影響性能和可維護性。在開發中,我們可以根據具體需求來選擇是否使用 Context API,並搭配其他狀態管理工具(如 Redux 或 Zustand)來管理更複雜的應用狀態。

參考文件

  1. 用 React Context API 實作跨組件傳值的功能

React useMemo 與 useCallback 差異介紹與入門教學 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

在使用 React hooks 進行開發時,useMemouseCallback 是兩個常被提及的性能優化工具。它們都屬於 記憶化(memoization) 技術,用來避免不必要的重算與重渲染。然而,很多初學者在理解這兩者的用途與差異時常感到困惑。這篇文章將從概念出發,並搭配實際範例,幫助你掌握 useMemouseCallback 的核心用途與實作方式。


一、共通點:記憶化

React 在每次組件渲染時,預設會重新執行所有函式與表達式。當某些值(如計算結果、函式)在依賴未改變的情況下不需要重新產生,我們可以利用記憶化來優化效能。

這就是 useMemouseCallback 的主要功能:根據依賴陣列(dependency array)決定是否重建值或函式


二、差異概念總覽

Hook主要用途回傳內容使用時機
useMemo記憶「計算結果」任意值計算過程昂貴,避免重複運算
useCallback記憶「函式定義」函式傳遞函式給子元件,避免不必要的 re-render

換句話說:

  • useMemo(fn, deps)const value = memoized(fn)
  • useCallback(fn, deps)const callback = memoized(() => fn)

三、useMemo 範例

假設我們有一個需要進行繁重計算的函式,例如統計某個資料集合中的數值:

import React, { useState, useMemo } from 'react';

function slowFunction(num) {
console.log('Running slow function');
let result = 0;
for (let i = 0; i < 1e7; i++) {
result += num;
}
return result;
}

function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [input, setInput] = useState(1);

const computedValue = useMemo(() => {
return slowFunction(input);
}, [input]);

return (
<div>
<h2>Expensive Calculation</h2>
<p>計算結果:{computedValue}</p>
<input
type="number"
value={input}
onChange={e => setInput(Number(e.target.value))}
/>
<button onClick={() => setCount(c => c + 1)}>重新渲染 ({count})</button>
</div>
);
}

在這個例子中,若不使用 useMemo,只要任何 state 改變(例如點擊按鈕改變 count),整個組件都會重新執行 slowFunction,導致效能問題。透過 useMemo,只有 input 改變時才會重新計算,其他情況會重複使用上次的計算結果。


四、useCallback 範例

有時候我們會將函式作為 props 傳遞給子元件。如果每次重新 render 都產生新的函式實例,會導致子元件誤以為 props 改變,而重新渲染。這時就可以用 useCallback

import React, { useState, useCallback, memo } from 'react';

const ChildButton = memo(({ onClick }) => {
console.log('ChildButton rendered');
return <button onClick={onClick}>點我</button>;
});

function ParentComponent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);

const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

return (
<div>
<h2>useCallback Demo</h2>
<p>Count: {count}</p>
<ChildButton onClick={handleClick} />
<button onClick={() => setOther(o => o + 1)}>改變其他狀態</button>
</div>
);
}

在這個範例中,ChildButton 是經過 memo 包裹的元件,只有在 props.onClick 改變時才會重新渲染。使用 useCallback 確保 handleClick 函式在 [](無依賴)下只會創建一次,即使 other 改變,ChildButton 也不會重新渲染。


五、常見錯誤與注意事項

  1. 過度使用會反效果
    useMemouseCallback 本身也有記憶成本,不建議過度使用。只有在你確定函式或運算昂貴,或造成子元件重 render 才需要用。

  2. 依賴陣列要正確
    記得將函式中引用的變數正確加入依賴陣列中,否則會造成記憶結果與預期不符。

  3. 搭配 React.memo 效果更明顯
    useCallback 通常與 memoPureComponent 搭配,否則即使函式地址一樣,也無法避免重 render。


六、總結

當你在開發中遇到效能瓶頸或元件不必要地重複渲染時,才是使用 useMemouseCallback 的好時機。舉例來說:

  • 在表格過濾、排序等涉及大量資料處理的畫面中,可以用 useMemo 優化計算。
  • 在表單中將函式傳遞給 input 元件時,使用 useCallback 可避免整個表單重 render。

記住一個原則:不要為了使用 hook 而使用,而是根據實際效能需求進行優化。如果你的應用很小或尚未遇到效能問題,先專注於撰寫可讀性高、邏輯清楚的程式碼,這才是最重要的。

參考文件

  1. 如何優化 React 元件的渲染效能,並避免渲染陷阱

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

· 閱讀時間約 3 分鐘
kdchang

1. JSX 是什麼?

JSX(JavaScript XML)是一種語法擴展,主要用於 React 函式庫中,它允許在 JavaScript 代碼中撰寫類似 HTML 的語法,使 UI 組件的編寫更加直觀。JSX 並非原生 JavaScript,需要透過 Babel 轉譯成標準 JavaScript 代碼。

2. 為什麼使用 JSX?

JSX 提供了一種更加可讀、直觀的方式來描述 UI 結構,相比於傳統的 JavaScript DOM 操作,它更簡潔易懂。此外,JSX 具備以下優勢:

  • 可讀性高:類似 HTML 的語法使 UI 組件結構清晰。
  • 與 JavaScript 無縫結合:可在 JSX 中嵌入 JavaScript 表達式。
  • 更安全:React 會自動處理 XSS(跨站腳本攻擊),確保數據安全。
  • 高效渲染:React 使用虛擬 DOM 最小化真實 DOM 更新,提高性能。

3. 基本語法

3.1 基本範例

JSX 允許我們在 JavaScript 代碼中使用類似 HTML 的語法來描述 UI:

const element = <h1>Hello, JSX!</h1>;
ReactDOM.render(element, document.getElementById('root'));

3.2 JSX 中的 JavaScript 表達式

JSX 允許在 {} 中插入 JavaScript 表達式,例如變數、函式調用等。

const name = "Alice";
const element = <h1>Hello, {name}!</h1>;

3.3 JSX 屬性

JSX 屬性與 HTML 類似,但有些屬性名稱需使用 camelCase,例如 className 取代 classhtmlFor 取代 for

const element = <input type="text" placeholder="輸入文字" className="input-box" />;

3.4 JSX 內聯樣式

內聯樣式需使用 JavaScript 對象,且屬性名稱為 camelCase。

const style = { color: 'blue', fontSize: '20px' };
const element = <p style={style}>這是一段藍色文字</p>;

4. JSX 中的條件與循環

4.1 條件渲染(if...else)

JSX 本身不支援 if...else,需使用三元運算子或變數。

const isLoggedIn = true;
const element = isLoggedIn ? <h1>歡迎回來!</h1> : <h1>請登入</h1>;

4.2 使用 && 運算符

const messages = ['新訊息1', '新訊息2'];
const element = (
<div>
{messages.length > 0 && <p>你有 {messages.length} 則未讀訊息。</p>}
</div>
);

4.3 迴圈渲染(map)

JSX 可透過 map 方法來動態渲染列表。

const list = ['Apple', 'Banana', 'Cherry'];
const element = (
<ul>
{list.map((item, index) => <li key={index}>{item}</li>)}
</ul>
);

5. JSX 與 React 組件

JSX 可與 React 組件結合使用,提升 UI 開發的模組化程度。

5.1 函式型組件

function Welcome(props) {
return <h1>你好,{props.name}</h1>;
}

const element = <Welcome name="小明" />;
ReactDOM.render(element, document.getElementById('root'));

5.2 類別型組件

class Welcome extends React.Component {
render() {
return <h1>你好,{this.props.name}</h1>;
}
}

const element = <Welcome name="小明" />;
ReactDOM.render(element, document.getElementById('root'));

6. 總結

JSX 是 React 開發的重要語法,它能夠讓 UI 描述更加直觀,並與 JavaScript 無縫整合。透過學習 JSX,我們可以更高效地撰寫可重用的 React 組件,提高開發效率。

Backend For Frontend(BFF)入門教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

前言

當今前端與後端開發越趨分離,加上行動裝置、多樣化的使用者端(Web、App、IoT 等)快速成長,Backend for Frontend(BFF) 架構逐漸成為微服務與現代應用中不可或缺的一環。本文將介紹 BFF 的基本概念、優缺點、使用情境與實際範例,幫助你快速理解並在專案中應用。

一、什麼是 Backend For Frontend?

Backend For Frontend(簡稱 BFF) 是一種後端架構模式,其核心理念是:每種前端應用(Web、Mobile App、Smart TV)都由各自專屬的後端服務來支援,這個後端只為對應的前端量身打造。

傳統系統中,前端直接呼叫後端 API 或微服務,常會遇到以下問題:

  • 回傳資料過多(浪費頻寬)
  • 回傳資料不夠(還要多次請求)
  • 錯誤格式(不適合前端解析)
  • 缺乏聚合邏輯(需要組裝多個 API 回應)

BFF 則專門為特定前端處理這些問題,讓資料更適合顯示、傳輸與渲染,減少耦合與重複工作。


二、BFF 架構示意圖

          +---------------+          +-------------------+
Web App → | BFF for Web | → REST | User Service |
+---------------+ +-------------------+

+----------------+ | REST / RPC
Mobile → | BFF for Mobile | → GraphQL| Order Service |
+----------------+ |

Product Service

每一個 BFF 可以:

  • 聚合多個微服務的資料
  • 轉換資料格式供前端使用
  • 處理權限與驗證邏輯(例如 token 驗證)
  • 實作快取策略(如 CDN, Redis)
  • 提供更穩定的 API 給前端

三、什麼情況適合使用 BFF?

  1. 有多種前端平台:Web 和 App 須分別調整 API。
  2. 資料需聚合多個來源:例如組合訂單資料與商品詳細資訊。
  3. 前端需要特別的資料結構或格式轉換
  4. 希望讓前端開發獨立部署、測試、開發
  5. 希望簡化前端邏輯,把複雜邏輯搬到後端處理

四、BFF 優點與缺點

優點

  • 前後端明確分工,降低耦合
  • 加快前端開發速度
  • 可根據裝置特性量身打造 API
  • 改善效能(避免多餘資料,提升快取)
  • 更容易實施權限控管與安全策略

缺點

  • 增加部署與維護成本(每個前端都需對應 BFF)
  • 需要資源維護 BFF 團隊
  • 需協調不同 BFF 的資料一致性與邏輯重複問題

五、實際範例:Node.js 建立一個 BFF

假設有以下微服務:

  • GET /users/:id 回傳使用者資料
  • GET /orders/user/:id 回傳該用戶的訂單清單

現在我們為 Web 建立一個 BFF,統一提供 /profile/:id 接口,回傳使用者基本資訊與訂單清單。

BFF 架構(Node.js + Express)

// bff/web-server.js
const express = require('express');
const axios = require('axios');
const app = express();

app.get('/profile/:id', async (req, res) => {
const userId = req.params.id;

try {
const [userRes, ordersRes] = await Promise.all([
axios.get(`http://users-service/users/${userId}`),
axios.get(`http://orders-service/orders/user/${userId}`)
]);

res.json({
user: userRes.data,
orders: ordersRes.data
});
} catch (error) {
console.error(error);
res.status(500).send('資料取得失敗');
}
});

app.listen(3000, () => {
console.log('BFF for Web running at http://localhost:3000');
});

使用說明

  • 前端只需呼叫 GET /profile/123 即可拿到整合後的資料。
  • BFF 內部負責跟不同服務串接、組裝、錯誤處理與轉換格式。

六、BFF 和 GraphQL 的關係

GraphQL 本身也常用於 BFF 的實作方式之一。透過 GraphQL:

  • 前端可以自己定義要的欄位(防止 over-fetch)
  • 可以統一查詢不同來源資料(類似 BFF 的聚合邏輯)
  • 可搭配 Apollo Server 快速搭建

BFF 可以使用 REST,也可以使用 GraphQL,取決於前後端的需求與團隊熟悉度。


七、延伸應用:BFF 搭配微前端(Micro Frontend)

當前端本身也是多團隊分工、模組化(例如大型企業的後台管理系統),BFF 可以依照模組拆分,例如:

  • Order 模組對應一個 BFF
  • Admin 模組對應另一個 BFF

讓後端資料與前端 UI 模組相對應,利於組織協作與部署。


八、總結

面向傳統 APIBFF 架構
資料格式後端決定針對前端定製
跨平台支援不佳良好
前端彈性
擴充性
成本中~高(需維護 BFF)

BFF 並不是萬靈丹,但在多平台、多樣前端需求的專案中,非常實用,能夠有效地降低溝通成本、提升開發效率,並提供更佳的 API 使用體驗。

如果你的團隊正在建構大型應用系統,或前端開發頻繁改動,不妨考慮導入 BFF 架構,讓前後端協作更流暢、更可維護。

參考文件

  1. Backend For Frontend 前端模式的後端
  2. 前端開發者該負責寫 API Endpoints 嗎?The Backend For Frontend Pattern (BFF) In Microservices World
  3. Backends for Frontends pattern
  4. Backend for frontend (BFF) pattern— why do you need to know it?

React Redux 介紹入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

1. 什麼是 Redux?

Redux 是 JavaScript 應用程式的狀態管理工具,最常與 React 搭配使用。它提供一個單一的全域狀態樹,使應用程式的狀態變更更可預測、可測試、可維護。Redux 遵循 單向數據流不可變狀態 的概念,適合管理大型應用的複雜狀態。


2. Redux 核心概念

Redux 的運作基礎主要由三個部分組成:

  1. Store(存儲狀態的地方)
    • 整個應用程式的狀態存儲在單一的 Store 中。
  2. Action(動作)
    • Action 是一個 JavaScript 物件,描述了「發生了什麼事」。通常包含 type 屬性來表明事件類型,並可能包含 payload 傳遞額外數據。
  3. Reducer(狀態變更邏輯)
    • Reducer 是一個純函式,接收當前狀態與 Action,根據 Action 類型來決定如何更新狀態。

3. 安裝 Redux 和 React-Redux

在 React 專案中安裝 Redux 及 React-Redux:

npm install @reduxjs/toolkit react-redux

Redux Toolkit(RTK)是官方推薦的 Redux 工具包,它提供更簡潔、易用的 API 來簡化 Redux 使用。


4. 創建 Redux Store

store.js 檔案中,使用 configureStore 創建 Store:

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

const store = configureStore({
reducer: {
counter: counterReducer,
},
});

export default store;

在這裡,我們將 counterReducer 設定為 counter 這個 slice 的 reducer。


5. 創建 Slice(Reducer + Action)

counterSlice.js 檔案中,使用 createSlice 定義 counter 相關的狀態與 reducer:

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

這裡的 createSlice 幫助我們:

  • 定義狀態的初始值(initialState
  • 自動生成 reducer
  • 自動產生對應的 Action

6. 設置 Store 提供給 React

index.js 檔案中使用 Provider 讓整個 React 應用可以存取 Redux Store:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

這樣 App 及其所有子元件都可以存取 Redux Store。


7. 在 React 組件中使用 Redux

Counter.js 中使用 Redux Store 來讀取與修改 counter 的值:

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementByAmount } from "./counterSlice";

const Counter = () => {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();

return (
<div>
<h1>計數器:{count}</h1>
<button onClick={() => dispatch(increment())}>增加</button>
<button onClick={() => dispatch(decrement())}>減少</button>
<button onClick={() => dispatch(incrementByAmount(5))}>增加 5</button>
</div>
);
};

export default Counter;

在這個範例中:

  • useSelector(state => state.counter.value) 取得 Store 內的 counter 值。
  • useDispatch() 取得 Redux 的 dispatch 函式,發送 Action 來更新狀態。
  • 點擊按鈕時,對應的 Action 會發送給 Reducer,修改狀態。

8. 使用 Redux DevTools 進行除錯

Redux Toolkit 內建支援 Redux DevTools,可以在瀏覽器中查看 Redux 狀態變更的歷史紀錄,方便除錯。安裝 Redux DevTools,並開啟瀏覽器的 Redux 分頁來觀察 Action 和 State 變化。


9. 總結

Redux 提供了一個強大的狀態管理方式,適合中大型應用。在 Redux Toolkit 的幫助下,開發 Redux 應用變得更加直觀與簡潔。本篇介紹了 Redux 的基本概念,並透過實際範例展示如何在 React 中整合 Redux。如果你的應用狀態複雜,需要跨組件共享,Redux 會是很好的選擇。

參考文件

  1. redux vs useContext, useReducer,該怎麼選擇?
  2. The Problem with React's Context API
  3. React 狀態管理套件比較與原理實現 feat. Redux, Zustand, Jotai, Recoil, MobX, Valtio
  4. 利用 React Context API + useReducer 實作 Redux