本篇將深入探討 React 19 的核心概念與實作技巧,從 Vite 專案結構開始,逐步介紹 JSX 語法、元件設計、Props 傳遞、事件處理到狀態管理。透過實際範例,你將學會如何建構可重用的元件、管理應用程式狀態,以及掌握現代 React 開發的最佳實踐。
了解專案環境 相較於 CDN 載入方式,使用 Vite 建立的 React 專案已經內建完整的開發環境。Vite 會自動處理 JSX 轉換、模組打包、熱更新等工作,讓我們專注在應用程式開發上。本章節將從 Vite 預設產生的專案結構開始,逐步了解 React 19 的核心觀念。
根元件(Root Component) React 應用程式採用 SPA(Single Page Application,單頁應用程式) 架構,整個網站只會載入一個 index.html 檔案,所有的頁面切換和內容更新都透過 JavaScript 動態完成,不需要重新載入頁面。
在 Vite 專案中,index.html 是整個應用的入口點,它只包含一個掛載點 <div id="root"></div> 和一個 JavaScript 模組引入 <script type="module" src="/src/main.jsx"></script>。React 會在這個掛載點渲染整個應用程式。
index.html <!doctype html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <link rel ="icon" type ="image/svg+xml" href ="/vite.svg" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > Vite + React</title > </head > <body > <div id ="root" > </div > <script type ="module" src ="/src/main.jsx" > </script > </body > </html >
為什麼使用 .jsx 副檔名?
.jsx 副檔名明確告訴 Vite 這個檔案包含 JSX 語法,需要進行特殊處理。雖然 Vite 也能處理 .js 檔案中的 JSX,但使用 .jsx 能讓檔案用途更清晰,也方便編輯器提供更好的語法高亮和自動補全。
main.jsx - 應用程式入口 /src/main.jsx 是整個 React 應用的入口檔案,負責初始化並渲染根元件。讓我們逐步了解每個部分的作用:
📌 核心概念說明:
StrictMode(嚴格模式)
React 19 的開發輔助工具,用於檢測潛在問題
檢測不安全的生命週期、過時的 API 使用
在開發模式下會故意執行元件兩次 ,幫助發現副作用問題
只在開發環境生效,生產環境會自動移除
createRoot(建立根節點)
React 18+ 引入的新 API,取代舊的 ReactDOM.render
啟用 Concurrent Mode(並行模式),支援更好的效能優化
語法:createRoot(DOM 元素).render(React 元件)
CSS 匯入
React 元件可以直接匯入 CSS 檔案
父元件的樣式會影響所有子元件(CSS 繼承特性)
Vite 會自動處理 CSS 的打包和熱更新
模組匯入簡化
Vite 支援省略 .jsx 副檔名
會自動依序尋找 .js、.jsx、.json 檔案
例:import App from './App' 會自動找到 App.jsx
虛擬 DOM 渲染
render 方法會建立虛擬 DOM 樹
React 透過 Diff 演算法比較變化
只更新實際改變的 DOM 部分,提升效能
/src/main.jsx import { StrictMode } from 'react' ; import { createRoot } from 'react-dom/client' ; import './index.css' ; import App from './App' ; createRoot (document .getElementById ('root' )).render ( <StrictMode > <App /> </StrictMode > );
React 18+ 重要變更:
const root = createRoot (document .getElementById ('root' ));root.render (<App /> ); ReactDOM .render (<App /> , document .getElementById ('root' ));
關鍵差異:
Concurrent Mode :React 18+ 預設啟用,支援自動批次更新、Suspense 等新特性
更好的效能 :可中斷渲染,優先處理使用者互動
未來兼容性 :React 19 的新功能都基於 createRoot API
JSX 轉換改進(React 17+):
從 React 17 開始,你不再需要在每個檔案中 import React:
import { useState, useEffect } from 'react' ;function MyComponent ( ) { return <div > Hello</div > ; }
React 16 以前,因為 Babel 編譯 JSX 時會直接呼叫 React.createElement,所以每個使用 JSX 的檔案都必須手動寫上 import React from 'react';,確保 React 在作用域。自 React 17 起,Babel 或 SWC 等現代編譯器會自動注入轉換函式,讓你不必再手動引入 React,JSX 可以直接使用,開發流程更簡潔。
App.jsx - 主應用元件 在 VSCode 中,你可以按住 Ctrl 並點擊 <App />,快速跳轉到 App.jsx 檔案。這是 React 應用的主要元件,所有的 UI 邏輯都從這裡開始。
React 採用元件樹(Component Tree) 的架構,每個元件都可以包含子元件,形成巢狀結構。在這個例子中,App 是 main.jsx 的子元件,而 App 內部又可以包含更多子元件。每個元件都有自己獨立的邏輯、狀態和渲染輸出。
App.jsx import { useState } from 'react' ; import reactLogo from './assets/react.svg' ; import viteLogo from '/vite.svg' ; import './App.css' ; function App ( ) { const [count, setCount] = useState (0 ); return ( <> {/* Fragment 空標籤:包裹多個元素但不產生額外 DOM */} <div > <a href ="https://vite.dev" target ="_blank" rel ="noopener noreferrer" > <img src ={viteLogo} className ="logo" alt ="Vite logo" /> </a > <a href ="https://react.dev" target ="_blank" rel ="noopener noreferrer" > <img src ={reactLogo} className ="logo react" alt ="React logo" /> </a > </div > <h1 > Vite + React</h1 > <div className ="card" > {/* 點擊按鈕時,將 count + 1 */} <button onClick ={() => setCount((count) => count + 1)}> count is {count} </button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more </p > </> ); } export default App ;
📌 App.jsx 關鍵概念解析:
useState Hook(狀態管理)
const [count, setCount] = useState (0 );
count:唯讀的狀態值,用於顯示
setCount:更新狀態的函式,呼叫後會觸發重新渲染
0:初始值,元件第一次渲染時的狀態
後續章節會詳細介紹
資源匯入
圖片模組化 :Vite 允許直接 import 圖片,會自動處理路徑
CSS 匯入 :元件專屬樣式,支援 CSS Modules、PostCSS 等
路徑規則 :
./assets/xxx - 相對路徑(src 目錄下)
/xxx - public 目錄下的檔案
函式元件結構
function ComponentName ( ) { return <div > ...</div > ; }
函式名稱必須大寫開頭(React 規範)
return 回傳 JSX,描述 UI 長什麼樣子
每次狀態改變,函式會重新執行
JSX Fragment(空標籤 <>...</>)
用途:包裹多個並列元素,但不產生額外的 DOM 節點
等同於 <React.Fragment>...</React.Fragment>
為什麼需要? React 元件的 return 只能回傳一個根元素
return ( <h1 > Title</h1 > <p > Content</p > ); return ( <> <h1 > Title</h1 > <p > Content</p > </> ); return ( <div > <h1 > Title</h1 > <p > Content</p > </div > );
export default(預設匯出)
每個檔案只能有一個 export default
匯入時可以自訂名稱:import MyApp from './App'
也可以使用具名匯出:export function App() {...}
target=”_blank” 安全性提醒:
使用 target="_blank" 開啟新分頁時,建議加上 rel="noopener noreferrer":
<a href="https://example.com" target="_blank" rel="noopener noreferrer" > Link </a>
noopener:防止新頁面存取 window.opener,避免潛在的安全風險
noreferrer:不發送 referrer 資訊,保護隱私
React 19 的 JSX 會自動添加 noopener,但明確寫出來更清楚。
程式碼品質管理:ESLint 與 Prettier 在團隊協作或大型專案中,統一的程式碼風格和即時的錯誤檢查至關重要。Vite 預設已經配置好 ESLint,但我們需要進一步優化設定,並整合 Prettier 來自動格式化程式碼。
工具
用途
範例
ESLint
程式碼品質檢查
檢測未使用的變數、潛在錯誤、不符合規範的寫法
Prettier
程式碼格式化
統一縮排、引號、分號、換行等排版風格
最佳實踐組合:
ESLint 負責「程式邏輯」的正確性
Prettier 負責「程式外觀」的美觀性
兩者互補,避免衝突
配置 ESLint Vite 建立的 React 專案已經預裝 ESLint 並提供基礎配置檔 eslint.config.js。這是 ESLint 9+ 的新格式(扁平化配置),取代了舊的 .eslintrc.js。
eslint.config.js 配置檔解析:
eslint.config.js import js from '@eslint/js' ; import globals from 'globals' ; import react from 'eslint-plugin-react' ; import reactHooks from 'eslint-plugin-react-hooks' ; import reactRefresh from 'eslint-plugin-react-refresh' ; export default [ { ignores : ['dist' ] }, { files : ['**/*.{js,jsx}' ], languageOptions : { ecmaVersion : 2020 , globals : globals.browser , parserOptions : { ecmaVersion : 'latest' , ecmaFeatures : { jsx : true }, sourceType : 'module' , }, }, settings : { react : { version : '19.2' } }, plugins : { react, 'react-hooks' : reactHooks, 'react-refresh' : reactRefresh, }, rules : { ...js.configs .recommended .rules , ...react.configs .recommended .rules , ...react.configs ['jsx-runtime' ].rules , ...reactHooks.configs .recommended .rules , 'react/jsx-no-target-blank' : 'off' , 'react-refresh/only-export-components' : [ 'warn' , { allowConstantExport : true }, ], }, }, ];
ESLint 9+ 扁平化配置說明:
新的 eslint.config.js 格式更簡潔、更容易理解:
✅ 使用 JavaScript ES Module 語法
✅ 配置以陣列形式組織,順序重要
✅ 每個物件代表一組規則配置
✅ 支援更靈活的條件配置
與舊格式的差異:
module .exports = { extends : ['eslint:recommended' , 'plugin:react/recommended' ], }; export default [ { rules : { ...js.configs .recommended .rules } }, ];
整合 ESLint 到 VSCode 光有 ESLint 配置檔還不夠,我們需要讓 VSCode 即時顯示錯誤提示。
步驟 1:安裝 VSCode ESLint 擴充套件
在 VSCode 擴充套件市場搜尋「ESLint 」(by Microsoft),點擊安裝。
步驟 2:配置專案級設定
在專案根目錄建立 .vscode/settings.json 檔案,啟用自動修復和驗證:
.vscode/settings.json { "editor.codeActionsOnSave" : { "source.fixAll.eslint" : "explicit" } , "eslint.validate" : [ "javascript" , "javascriptreact" ] }
專案設定共享:
建議將 .vscode/settings.json 加入 Git 版本控制,讓團隊成員有一致的開發環境設定。
在 .gitignore 中確保沒有忽略 .vscode/ 目錄:
# .gitignore # .vscode/* ← 註解或刪除這行
這樣其他人 clone 專案後,VSCode 會自動套用這些設定。
步驟 3:測試 ESLint 自動修復
在 src/App.jsx 故意添加多餘的分號:
function App ( ) { const [count, setCount] = useState (0 );; }
此時 ESLint 預設規則可能不會檢測這個錯誤。我們可以添加自訂規則:
eslint.config.js rules : { ...js.configs .recommended .rules , ...react.configs .recommended .rules , ...react.configs ['jsx-runtime' ].rules , ...reactHooks.configs .recommended .rules , 'react/jsx-no-target-blank' : 'off' , 'react-refresh/only-export-components' : [ 'warn' , { allowConstantExport : true }, ], 'no-extra-semi' : 'error' , },
儲存配置檔後,回到 App.jsx,錯誤提示會立即出現:
按下 Ctrl +S 儲存,ESLint 會自動移除多餘的分號。
ESLint 自動修復的魔法:
ESLint 可以自動修復的問題類型:
✅ 多餘的空格、分號
✅ 不一致的引號(單引號 vs 雙引號)
✅ 缺少的分號
✅ 未使用的 import
無法自動修復的問題(需要手動處理):
❌ 邏輯錯誤
❌ 未定義的變數
❌ React Hooks 規則違反
配置 Prettier Prettier 是一個固執己見的程式碼格式化工具,能自動統一程式碼風格,避免團隊成員因為排版風格不同而產生無意義的 Git diff。
步驟 1:安裝 Prettier 相關套件
pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier
套件說明:
prettier:Prettier 核心套件
eslint-config-prettier:關閉 ESLint 中與 Prettier 衝突的格式規則
eslint-plugin-prettier:將 Prettier 規則整合到 ESLint 中
-D 參數說明:
-D 是 --save-dev 的縮寫,表示安裝為開發依賴 (devDependencies)。這些工具只在開發時使用,打包後的生產環境不需要,可以減少最終打包體積。
{ "dependencies" : { "react" : "^19.0.0" } , "devDependencies" : { "prettier" : "^3.0.0" , "eslint" : "^9.0.0" } }
步驟 2:建立 Prettier 配置檔
在專案根目錄建立 .prettierrc 檔案:
.prettierrc { "semi" : true , "tabWidth" : 2 , "printWidth" : 100 , "singleQuote" : true , "trailingComma" : "es5" , "jsxSingleQuote" : false , "bracketSpacing" : true , "arrowParens" : "always" }
Prettier 配置選項說明:
選項
預設值
說明
範例
semi
true
語句結尾是否加分號
const x = 1; vs const x = 1
singleQuote
false
字串使用單引號
'hello' vs "hello"
tabWidth
2
縮排空格數
2 或 4
printWidth
80
每行最大字元數
80、100、120
trailingComma
"es5"
尾逗號規則
{ a: 1, }
bracketSpacing
true
物件括號內空格
{ foo } vs {foo}
arrowParens
"always"
箭頭函式括號
(x) => x vs x => x
React 專案推薦配置:
單引號(singleQuote: true):更簡潔
JSX 雙引號(jsxSingleQuote: false):符合 HTML 慣例
分號(semi: true):避免潛在錯誤
100 字元(printWidth: 100):平衡可讀性和寬螢幕利用率
步驟 3:整合 Prettier 到 ESLint
修改 eslint.config.js,將 Prettier 整合進來:
eslint.config.js import js from '@eslint/js' ;import globals from 'globals' ;import react from 'eslint-plugin-react' ;import reactHooks from 'eslint-plugin-react-hooks' ;import reactRefresh from 'eslint-plugin-react-refresh' ;import prettier from 'eslint-plugin-prettier' ; export default [ { ignores : ['dist' ] }, { files : ['**/*.{js,jsx}' ], languageOptions : { ecmaVersion : 2020 , globals : globals.browser , parserOptions : { ecmaVersion : 'latest' , ecmaFeatures : { jsx : true }, sourceType : 'module' , }, }, settings : { react : { version : '18.3' } }, plugins : { react, 'react-hooks' : reactHooks, 'react-refresh' : reactRefresh, prettier, }, rules : { ...js.configs .recommended .rules , ...react.configs .recommended .rules , ...react.configs ['jsx-runtime' ].rules , ...reactHooks.configs .recommended .rules , 'react/jsx-no-target-blank' : 'off' , 'react-refresh/only-export-components' : ['warn' , { allowConstantExport : true }], 'no-extra-semi' : 'error' , 'prettier/prettier' : 'error' , }, }, ];
避免 ESLint 和 Prettier 衝突:
由於 Prettier 會自動處理所有格式相關的問題(縮排、分號、引號等),我們應該讓 Prettier 完全接管格式化工作,ESLint 只負責程式邏輯檢查。
✅ 移除 no-extra-semi 等格式規則
✅ 添加 prettier/prettier 規則
✅ 使用 eslint-config-prettier 自動關閉衝突規則
步驟 4:更新 VSCode 設定
更新 .vscode/settings.json,設定 Prettier 為預設格式化工具:
.vscode/settings.json { "editor.codeActionsOnSave" : { "source.fixAll.eslint" : "explicit" } , "eslint.validate" : [ "javascript" , "javascriptreact" ] , "editor.formatOnSave" : true , "editor.defaultFormatter" : "esbenp.prettier-vscode" }
現在每次按 Ctrl +S 儲存時,會自動執行:
Prettier 格式化程式碼(縮排、引號、分號等)
ESLint 修復可自動修復的問題(unused imports、簡單邏輯錯誤)
步驟 5:批次格式化整個專案
在 package.json 添加格式化腳本:
package.json { "scripts" : { "dev" : "vite" , "build" : "vite build" , "lint" : "eslint ." , "preview" : "vite preview" , "format" : "prettier --write \"src/**/*.{js,jsx,css,json}\"" } }
執行格式化命令:
Prettier 會掃描 src 目錄下的所有 .js、.jsx、.css、.json 檔案並自動格式化。
ESLint + Prettier 完美組合總結:
工具
負責範圍
觸發時機
ESLint
程式邏輯錯誤、最佳實踐、React 規則
即時檢查 + 儲存時修復
Prettier
程式碼格式(縮排、引號、分號等)
儲存時自動格式化
工作流程:
寫程式碼 → ESLint 即時顯示錯誤(紅色波浪線)
按 Ctrl +S → Prettier 格式化 → ESLint 自動修復
提交前執行 pnpm format 確保所有檔案格式一致
React 核心語法與概念 環境配置完成後,讓我們深入學習 React 的核心語法和開發模式。
JSX 語法完全指南 JSX(JavaScript XML)是 React 的核心語法擴展,讓我們能在 JavaScript 中直接撰寫類似 HTML 的標記。Vite 會透過 SWC 自動將 JSX 編譯成標準的 JavaScript。
JSX 核心規則 1. 所有標籤必須閉合
<br /> <img src ="photo.jpg" alt ="Photo" /> <input type ="text" /> <br> <img src="photo.jpg" alt="Photo"> <input type="text">
2. 必須回傳單一根元素
function Component ( ) { return ( <h1 > Title</h1 > <p > Content</p > ); } function Component ( ) { return ( <> <h1 > Title</h1 > <p > Content</p > </> ); } function Component ( ) { return ( <div > <h1 > Title</h1 > <p > Content</p > </div > ); }
Fragment 的使用時機:
<Fragment> 或簡寫 <> 用於包裹多個元素但不產生額外 DOM。
<> <h1 > Title</h1 > <p > Content</p > </> import { Fragment } from 'react' ;items.map ((item ) => ( <Fragment key ={item.id} > <dt > {item.term}</dt > <dd > {item.description}</dd > </Fragment > ));
何時需要 key?
<> 簡寫不支援 key 屬性
在 map() 渲染列表時,必須提供 key
這時要用 <Fragment key={...}> 完整寫法
3. JavaScript 表達式需用 {} 包裹
function Greeting ({ name, age } ) { const greeting = `Hello, ${name} !` ; const nextYear = age + 1 ; const formatted = name.toUpperCase (); return ( <div > <h1 > {greeting}</h1 > <p > You are {age} years old</p > <p > Next year you'll be {nextYear}</p > <p > Uppercase name: {formatted}</p > {/* 條件渲染 */} <p > {age >= 18 ? 'Adult' : 'Minor'}</p > </div > ); }
4. HTML 屬性的 JSX 寫法
由於 JSX 是 JavaScript,某些 HTML 屬性名稱需要調整:
HTML
JSX
原因
class
className
class 是 JavaScript 保留字
for
htmlFor
for 是 JavaScript 保留字
onclick
onClick
事件處理採用駝峰命名
onchange
onChange
事件處理採用駝峰命名
tabindex
tabIndex
屬性採用駝峰命名
data-id
data-id
data-* 和 aria-* 保持 kebab-case
<div className="container" data-testid="main" > <label htmlFor ="username" > Username:</label > <input id ="username" type ="text" onChange ={handleChange} tabIndex ={1} /> </div> <div class ="container" > <label for ="username" > Username:</label > <input onclick ={handleChange} /> </div >
5. 內聯樣式使用物件
function StyledComponent ( ) { const boxStyle = { backgroundColor : 'blue' , fontSize : '16px' , padding : '20px' , borderRadius : '8px' }; return ( <div > {/* 雙層大括號:外層是 JSX 表達式,內層是物件 */} <div style ={{ color: 'red ', margin: '10px ' }}> Inline Style </div > {/* 使用預定義的樣式物件 */} <div style ={boxStyle} > Styled Box </div > </div > ); }
6. 多行 JSX 需用 () 包裹
function Component ( ) { return ( <div > <h1 > Title</h1 > <p > Content</p > </div > ); } function Component ( ) { return <div > <h1 > Title</h1 > </div > ;} function Component ( ) { return <h1 > Simple Title</h1 > ; }
JSX 實用技巧 條件渲染 function UserGreeting ({ isLoggedIn, username } ) { return ( <div > {isLoggedIn ? ( <h1 > Welcome back, {username}!</h1 > ) : ( <h1 > Please log in</h1 > )} </div > ); return ( <div > {isLoggedIn && <h1 > Welcome, {username}!</h1 > } {!isLoggedIn && <button > Login</button > } </div > ); if (!isLoggedIn) { return <LoginForm /> ; } return <Dashboard user ={username} /> ; }
條件渲染的陷阱:
{count && <p > Count: {count}</p > } {count > 0 && <p > Count: {count}</p > } {count !== 0 && <p > Count: {count}</p > } {text && <p > {text}</p > } {text.length > 0 && <p > {text}</p > } {Boolean (text) && <p > {text}</p > }
JSX 轉換工具 如果有大量 HTML 需要轉換成 JSX,可以使用官方提供的 HTML to JSX 線上轉換器 。
JSX 最佳實踐:
✅ 使用 className 而非 class
✅ 所有標籤都要閉合
✅ 事件處理使用駝峰命名(onClick、onChange)
✅ 條件渲染時避免 falsy 值陷阱
✅ 複雜邏輯抽取成變數或函式
✅ 保持 JSX 簡潔易讀
JSX 實作範例:重構 App.jsx 讓我們應用剛學到的 JSX 知識,重構 App.jsx,展示 JSX 的靈活性:
src/App.jsx import { useState } from 'react' ;import reactLogo from './assets/react.svg' ;import viteLogo from '/vite.svg' ;import './App.css' ;export default function App ( ) { const [count, setCount] = useState (0 ); const myBr = <br /> ; const imgVite = <img src ={viteLogo} className ="logo" alt ="Vite logo" /> ; const imgReact = <img src ={reactLogo} className ="logo react" alt ="React logo" /> ; const h1Title = 'Vite + React' ; const h1Element = <h1 data-id ="123" > {h1Title}</h1 > ; return ( <> <div > <a href ="https://vite.dev" target ="_blank" rel ="noopener noreferrer" > {imgVite} </a > {myBr} <a href ="https://react.dev" target ="_blank" rel ="noopener noreferrer" > {imgReact} </a > </div > {h1Element} <div className ="card" > <button onClick ={() => setCount((count) => count + 1)}> count is {count} </button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more </p > </> ); }
範例重點:
✅ JSX 可以存成變數(myBr、imgVite、h1Element)
✅ 變數可以在其他 JSX 中使用 {變數名}
✅ data-* 屬性保持 kebab-case 寫法
✅ 加上 rel="noopener noreferrer" 提升安全性
為什麼要把 JSX 存成變數?
重複使用 :避免重複撰寫相同的 JSX
邏輯分離 :複雜的 JSX 抽離出來,讓 return 更簡潔
條件渲染 :根據條件決定要顯示哪個 JSX
提升可讀性 :給 JSX 有意義的名稱
const loadingElement = <div > Loading...</div > ;const errorElement = <div > Error occurred</div > ;const contentElement = <div > Content loaded</div > ;return ( <> {isLoading && loadingElement} {hasError && errorElement} {!isLoading && !hasError && contentElement} </> );
React 渲染機制 在現代網頁開發中,React 透過 JSX(JavaScript 語法擴充)來描述 UI 樣貌,有效將「元件邏輯」與「畫面結構」結合,提升開發效率與可維護性。本節將帶你認識 React 如何把這些用 JS 編寫的元素型物件,映射並渲染到瀏覽器的實體 DOM,並說明核心渲染方法(如 createRoot)的基本運作流程。學會這一套渲染機制,是 React 應用開發的基礎!
虛擬 DOM 到實體 DOM React 使用 JSX 描述 UI,但瀏覽器只認識真實的 DOM 節點。React 透過以下流程將 JSX 轉換成實際的網頁內容:
graph LR
A["JSX 代碼"]
B["React 元素物件"]
C["虛擬 DOM"]
D["Diff 演算法"]
E["更新實體 DOM"]
A --> B --> C --> D --> E
關鍵角色:ReactDOM.createRoot
react-dom/client 提供的 createRoot API 負責:
建立 React 根節點
將 React 元件樹渲染到指定的 DOM 容器
管理後續的更新和重新渲染
基本渲染範例 我們在 main.jsx 中已經看過基本用法:
src/main.jsx import { StrictMode } from 'react' ;import { createRoot } from 'react-dom/client' ;import './index.css' ;import App from './App' ;const root = createRoot (document .getElementById ('root' ));root.render ( <StrictMode > <App /> </StrictMode > );
分步解析:
document.getElementById('root') - 找到 HTML 中的掛載點
createRoot(...) - 建立 React 根節點
root.render(...) - 將元件樹渲染到 DOM
多個根節點渲染 在特殊情況下,你可能需要在同一個頁面的不同位置渲染獨立的 React 應用:
index.html <body > <div id ="root" > </div > <footer id ="footer" > </footer > <script type ="module" src ="/src/main.jsx" > </script > </body >
src/main.jsx import { StrictMode } from 'react' ;import { createRoot } from 'react-dom/client' ;import App from './App' ;import Footer from './Footer' ;createRoot (document .getElementById ('root' )).render ( <StrictMode > <App /> </StrictMode > ); createRoot (document .getElementById ('footer' )).render ( <Footer /> );
多根節點的注意事項:
每個根節點都是獨立的 React 應用
它們之間無法直接共享狀態
僅在特殊場景使用(如漸進式遷移舊專案)
一般情況下,整個應用只需要一個根節點
推薦做法: 如果需要在頁面不同位置顯示內容,應該在單一根節點中透過元件組合完成:
function App ( ) { return ( <> <Header /> <Main /> <Footer /> </> ); } createRoot (document .getElementById ('root' )).render (<App /> );
React 18+ 渲染 API 變更 import ReactDOM from 'react-dom' ;ReactDOM .render (<App /> , document .getElementById ('root' ));import { createRoot } from 'react-dom/client' ;const root = createRoot (document .getElementById ('root' ));root.render (<App /> );
新 API 的優勢:
✅ 啟用 Concurrent Mode(並行渲染)
✅ 支援自動批次更新(Automatic Batching)
✅ 更好的 Suspense 支援
✅ 更優的效能和使用者體驗
渲染機制總結:
一次性渲染 :createRoot 和 render 通常只在應用啟動時呼叫一次
後續更新 :透過 useState、useReducer 等 Hook 觸發
自動優化 :React 會自動批次更新,只修改變化的 DOM 部分
StrictMode :開發環境下會故意執行兩次,幫助發現副作用問題
Component 元件系統 本單元聚焦於 React 最核心的「元件系統」。你將從觀念、命名規則到語法範例,一步步掌握如何定義、撰寫、組合各種「函式元件」。同時也會說明現代 React 開發中元件拆分、組裝、資料傳遞的實務技巧。學會元件,等於掌握 React 開發的基礎與關鍵!
元件是 React 應用的基本建構單元,就像樂高積木一樣,可以組合成完整的應用程式。元件封裝了:
UI 結構 :JSX 描述的介面外觀
資料邏輯 :State 和 Props 管理
互動行為 :事件處理函式
元件可以小到一個按鈕,也可以大到整個頁面。
函式元件(Function Components) 自從 React 16.8 推出 Hooks 開始,官方就推薦優先使用函式元件 + Hooks 這種現代開發方式。
基本語法 在 React 中,「元件」本質上就是由函式 來宣告的一段程式,用來描述畫面的某一部分。你只需要撰寫一個 JavaScript 函式,並回傳 JSX,就可以建立一個自訂的元件,不需要寫 class、繼承或額外語法。這讓 React 的元件開發變得簡單、直觀,也利於複用與維護。
function Welcome ( ) { return <h1 > Hello, World!</h1 > ; } const Welcome = ( ) => { return <h1 > Hello, World!</h1 > ; }; const Welcome = ( ) => <h1 > Hello, World!</h1 > ;
元件命名規則: 在撰寫 JSX 時,React 會根據標籤名稱自動判斷該標籤應被解讀為「React 元件」還是「原生 HTML 標籤」。以大寫字母開頭 的標籤會被視為 React 元件,而小寫字母開頭 的則會解讀為原生 HTML 標籤。若不小心將自訂元件以小寫命名,React 會嘗試尋找相應的 DOM 標籤,造成錯誤(找不到對應 HTML 標籤)。因此,請務必遵守元件命名規則,確保 React 能正確渲染元件。
✅ 必須以大寫字母開頭(Welcome、MyButton、UserProfile)
❌ 不能用小寫開頭(welcome、myButton)- React 會當成 HTML 標籤
元件名稱必須大寫,用來區分 React 元件和 HTML 標籤:
<MyButton /> → React .createElement (MyButton ) <button /> → React .createElement ('button' )
過時的 Class 元件 React 在 16.8 版本後強調現代函式元件(Function Component)和 Hooks 開發模式,傳統的 Class 元件語法漸漸被棄用。本節將帶你快速對比兩種語法差異,建議直接採用「函式元件+Hooks」這套未來趨勢寫法,寫起來更直覺、可讀性也高。
特性
函式元件(推薦)
Class 元件(已過時)
語法
簡潔直觀
複雜冗長
狀態管理
useState Hook
this.state
副作用處理
useEffect Hook
生命週期方法
this 綁定
不需要
容易出錯
效能
更優
較差
React 19 支援
✅ 完整支援
⚠️ 維護模式
function Counter ( ) { const [count, setCount] = useState (0 ); useEffect (() => { document .title = `Count: ${count} ` ; }, [count]); return ( <button onClick ={() => setCount(count + 1)}> Count: {count} </button > ); } class Counter extends React.Component { constructor (props ) { super (props); this .state = { count : 0 }; this .handleClick = this .handleClick .bind (this ); } componentDidUpdate ( ) { document .title = `Count: ${this .state.count} ` ; } handleClick ( ) { this .setState ({ count : this .state .count + 1 }); } render ( ) { return ( <button onClick ={this.handleClick} > Count: {this.state.count} </button > ); } }
Class 元件的未來:
雖然 React 仍支援 Class 元件,但官方已明確表示:
✅ 新專案應使用函式元件 + Hooks
⚠️ 舊專案可以漸進式遷移
❌ 不再投入新功能開發(如 Server Components)
📚 如果遇到舊專案,需要額外學習 Class 語法
本課程完全使用現代的函式元件方式教學。
元件組合規則 React 強調「小元件組合而成大型介面」的設計理念。能否妥善組織元件結構、避開常見錯誤,將直接影響開發的可維護性與效能。
觀念重點:
外部定義元件(最佳實踐) :例如 Header 若定義在元件外層,函式只建立一次,每次重新渲染只是重跑函式內容。子元件會隨父元件一起重新渲染,但元件實例(狀態)保持不變。
巢狀定義元件(常見陷阱) :如果在 App 內部重新宣告 Header,每次 App 渲染時,Header 也會被認為是全新的,React 會強制重新掛載(re-mount),造成狀態丟失與效能浪費。
總結 :元件應該都定義在外層,勿在其他元件內部宣告。
什麼是重新渲染?什麼是重新掛載?
重新渲染 (Re-render) :原本的元件實例存在,只會重新執行函式內容並更新畫面。
重新掛載 (Re-mount) :舊元件被完全移除(連同所有狀態),然後建立一個全新實例。
元件必須定義在外部作用域,切勿在其他元件內部宣告。這有助於狀態穩定與執行效能最佳化。
✅ 正確:扁平化定義元件
function Header ( ) { return <h1 > My App</h1 > ; } function Content ( ) { return <p > Welcome!</p > ; } function App ( ) { return ( <> <Header /> {/* Header 函數只建立一次,每次只是重新渲染*/} <Content /> {/* Content 函數只建立一次,每次只是重新渲染 */} </> ); }
❌ 錯誤:巢狀定義元件
function App ( ) { function Header ( ) { return <h1 > My App</h1 > ; } return <Header /> ; {} }
元件組合與重用 元件可以像樂高積木一樣組合,形成元件樹:
function Logo ( ) { return <img src ="/logo.png" alt ="Logo" /> ; } function Navigation ( ) { return ( <nav > <a href ="/" > Home</a > <a href ="/about" > About</a > </nav > ); } function Header ( ) { return ( <header > <Logo /> <Navigation /> </header > ); } function App ( ) { return ( <> <Header /> <main > Content...</main > <footer > Footer...</footer > </> ); }
元件樹結構:
App ├── Header │ ├── Logo │ └── Navigation ├── main └── footer
實作練習:將 App.jsx 拆分成多個元件 讓我們將原本的 Logo 區域獨立成多個小元件,展示元件組合的威力:
src/App.jsx import { useState } from 'react' ;import reactLogo from './assets/react.svg' ;import viteLogo from '/vite.svg' ;import './App.css' ;const alts = { vite : 'Vite Logo' , react : 'React Logo' , }; function ImgA ( ) { return <img src ={viteLogo} className ="logo" alt ={alts.vite} /> ; } function LinkA ( ) { return ( <a href ="https://vite.dev" target ="_blank" rel ="noopener noreferrer" > <ImgA /> </a > ); } function ImgB ( ) { return <img src ={reactLogo} className ="logo react" alt ={alts.react} /> ; } function LinkB ( ) { return ( <a href ="https://react.dev" target ="_blank" rel ="noopener noreferrer" > <ImgB /> </a > ); } function MyLogo ( ) { const myBr = <br /> ; return ( <div > <LinkA /> {myBr} <LinkB /> </div > ); } export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; const h1Element = <h1 data-id ="123" > {h1Title}</h1 > ; return ( <> <MyLogo /> {h1Element} <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <button onClick ={() => setCount((count) => count + 1)}> count is {count} </button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more </p > </> ); }
元件拆分的優勢:
可讀性提升 :每個元件職責單一,容易理解
可重用性 :ImgA、LinkA 可以在其他地方使用
易於維護 :修改圖片邏輯只需改動 ImgA 或 ImgB
測試友好 :可以單獨測試每個小元件
元件組合結構:
App ├── MyLogo │ ├── LinkA │ │ └── ImgA │ ├── <br /> │ └── LinkB │ └── ImgB ├── h1Element ├── div.card └── p.read-the-docs
元件拆分原則:
單一職責 :每個元件只做一件事
可重用性 :如果某段 UI 會重複出現,就抽成元件
命名清晰 :元件名稱應該清楚描述其用途
大小適中 :太大不好維護,太小過度複雜
何時該拆分元件?
✅ 相同的 UI 出現多次
✅ 某段邏輯複雜,影響可讀性
✅ 想要獨立測試某部分功能
✅ 團隊成員需要並行開發不同區塊
Props:元件間的資料傳遞 在 React 中,Props(properties 的縮寫)是元件之間傳遞資料的主要方式。就像函式可以接收參數一樣,React 元件可以接收 props 並根據這些資料來決定渲染的內容。
Props 基本概念 三個重要特性:
唯讀(Read-Only)
Props 是唯讀的,子元件不能修改接收到的 props
這確保了資料流的可預測性
單向資料流
資料只能從父元件傳遞到子元件
這種單向流動讓程式更容易理解和除錯
資料型別
可以傳遞任何 JavaScript 資料型別
包括:字串、數字、布林值、物件、陣列、函式等
Props 基本用法 讓我們從最簡單的例子開始:
function Greeting (props ) { return <h1 > Hello, {props.name}!</h1 > ; } function App ( ) { return <Greeting name ="Alice" /> ; }
當我們需要傳遞多個 props 時:
function UserCard (props ) { return ( <div > <h2 > {props.name}</h2 > <p > 年齡:{props.age}</p > <p > 城市:{props.city}</p > </div > ); } function App ( ) { return ( <UserCard name ="Alice" age ={25} city ="台北" /> ); }
注意事項:
字串可以直接用引號:name="Alice"
其他型別要用大括號:age={25}
布林值 true 可以省略值:isActive 等同於 isActive={true}
Props 解構寫法 在 React 中,我們常常使用解構賦值(Destructuring)來簡化 props 的使用。讓我們看看如何逐步改善程式碼:
function UserCard (props ) { return ( <div > <h2 > {props.name}</h2 > <p > 年齡:{props.age}</p > <p > 城市:{props.city}</p > </div > ); } function UserCard ({ name, age, city } ) { return ( <div > <h2 > {name}</h2 > <p > 年齡:{age}</p > <p > 城市:{city}</p > </div > ); } function UserCard (props ) { const { name, age, city } = props; return ( <div > <h2 > {name}</h2 > <p > 年齡:{age}</p > <p > 城市:{city}</p > </div > ); } function App ( ) { return <UserCard name ="Alice" age ={25} city ="台北" /> ; }
Props 傳遞各種資料類型 React 的 props 可以傳遞各種資料類型,讓我們逐一看看:
function BasicProps ({ name, // 字串 age, // 數字 isActive // 布林值 } ) { return ( <div > <h2 > {name}</h2 > <p > 年齡:{age}</p > <p > 狀態:{isActive ? '啟用' : '停用'}</p > </div > ); } function AdvancedProps ({ hobbies, // 陣列 address // 物件 } ) { return ( <div > <ul > {hobbies.map((hobby, index) => ( <li key ={index} > {hobby}</li > ))} </ul > <p > 地址:{address.city}, {address.country}</p > </div > ); } function ButtonWithHandler ({ onClick, // 函式 children // JSX } ) { return ( <button onClick ={onClick} > {children} </button > ); } function App ( ) { const userData = { name : "Alice" , age : 25 , isActive : true , hobbies : ["閱讀" , "寫程式" , "旅遊" ], address : { city : "台北" , country : "台灣" } }; const handleClick = ( ) => { console .log ("按鈕被點擊了!" ); }; return ( <div > {/* 傳遞基本型別 */} <BasicProps name ={userData.name} age ={userData.age} isActive ={userData.isActive} /> {/* 傳遞陣列與物件 */} <AdvancedProps hobbies ={userData.hobbies} address ={userData.address} /> {/* 傳遞函式和 JSX */} <ButtonWithHandler onClick ={handleClick} > 點擊我 </ButtonWithHandler > </div > ); }
Props 使用的最佳實踐
命名規範
Props 名稱使用小駝峰式命名(camelCase)
事件處理函式以 on 開頭:onClick、onChange
布林值 props 使用 is、has 開頭:isActive、hasError
預設值處理
function UserCard ({ name = "訪客" , age = 0 } ) { return <div > {name} ({age}歲)</div > ; }
型別檢查
開發時使用 PropTypes 或 TypeScript
避免傳遞錯誤的資料型別
資料整理
複雜的資料處理放在父元件
子元件保持簡單,專注於渲染
避免過度傳遞
只傳遞必要的 props
使用物件打包相關的 props
function UserProfile ({ name, // 字串 age, // 數字 isActive, // 布林值 hobbies, // 陣列 address, // 物件 onSave // 函式 } ) { return ( <div > <h2 > {name}</h2 > <p > Age: {age}</p > <p > Status: {isActive ? 'Active' : 'Inactive'}</p > <ul > {hobbies.map((hobby, index) => ( <li key ={index} > {hobby}</li > ))} </ul > <p > City: {address.city}</p > <button onClick ={onSave} > Save Profile</button > </div > ); } function App ( ) { const user = { name : 'Alice' , age : 25 , isActive : true , hobbies : ['Reading' , 'Gaming' , 'Coding' ], address : { city : 'Taipei' , country : 'Taiwan' } }; const handleSave = ( ) => { console .log ('Profile saved!' ); }; return ( <UserProfile name ={user.name} age ={user.age} isActive ={user.isActive} hobbies ={user.hobbies} address ={user.address} onSave ={handleSave} /> ); }
Props 語法注意事項:
字串可以不用大括號 :
<Greeting name="Alice" /> <Greeting age ={25} /> <Greeting isActive /> <Greeting isActive ={false} />
布林值簡寫 :
<Button disabled={true } /> <Button disabled />
JSX 作為 Props :
<Card header={<h1 > Title</h1 > } />
實作範例:重構元件使用 Props 讓我們改寫之前的範例,我們可以把 Img 的 src 值與 alt 值,利用 props 來傳遞給元件。但此時應該會出現 ESLint 的錯誤。
src\App.jsx import { useState } from 'react' ;import reactLogo from './assets/react.svg' ;import viteLogo from '/vite.svg' ;import './App.css' ;function ImgA (props ) { return <img src ={props.logo} className ="logo" alt ={props.txt} /> ; } function LinkA ( ) { return ( <a href ="https://vite.dev" target ="_blank" > <ImgA logo ={viteLogo} txt ="Vite Logo" /> </a > ); } function ImgB ({ logo, txt } ) { return <img src ={logo} className ="logo" alt ={txt} /> ; } function LinkB ( ) { return ( <a href ="https://react.dev" target ="_blank" > <ImgB logo ={reactLogo} txt ="React Logo" /> </a > ); } function MyLogo ( ) { const myBr = <br /> ; return ( <div > <LinkA > </LinkA > {myBr} <LinkB /> </div > ); } export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; const h1Element = <h1 data-id ="123" > {h1Title}</h1 > ; return ( <> <MyLogo /> {h1Element} <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
由於 Vite 的環境有 ESLint 而檢查預設開啟,因此我們必需對這兩個錯誤的元件定義 props 的型別以及是否必存在。透過套件 prop-types 來導入使用。
prop-types 是什麼? prop-types 是一個 React 官方維護的第三方套件,用來在開發階段「檢查元件 props 的型別和必填性」。透過設置每個元件的 propTypes 屬性,我們可以定義每個 props 應該是什麼型別(例如:string、number、array、object),以及是否一定要傳遞。這能幫助開發時及早發現資料型別錯誤,提升專案的穩定性與可維護性。
常見用途:
避免父層傳入錯誤型別資料,導致元件渲染異常
提升團隊開發時的明確溝通與自動化檢查
注意: prop-types 只會在開發模式下提供警告,生產環境(正式 build) 不會影響效能。如果你使用 TypeScript,則不需要再用 prop-types,因為型別檢查已被 TypeScript 覆蓋。
src\App.jsx import PropTypes from 'prop-types' ;function ImgA (props ) { return <img src ={props.logo} className ="logo" alt ={props.txt} /> ; } ImgA .propTypes = { logo : PropTypes .string .isRequired , txt : PropTypes .string , }; function LinkA ( ) { return ( <a href ="https://vite.dev" target ="_blank" > <ImgA logo ={viteLogo} txt ="Vite Logo" /> </a > ); } function ImgB ({ logo, txt } ) { return <img src ={logo} className ="logo" alt ={txt} /> ; } ImgB .propTypes = { logo : PropTypes .string .isRequired , txt : PropTypes .string , }; function LinkB ( ) { return ( <a href ="https://react.dev" target ="_blank" > <ImgB logo ={reactLogo} txt ="React Logo" /> </a > ); }
當然你可以關閉 ESLint 對 props 型別的檢查,大多數現在的新專案都會使用 TypeScript 來開發輔助錯誤檢查。只有以前的舊專案,在沒有導入 TypeScript 又需要輔助檢查錯誤時才可能利用去 props-types 做檢查。
eslint.config.js rules : { ...js.configs .recommended .rules , ...react.configs .recommended .rules , ...react.configs ['jsx-runtime' ].rules , ...reactHooks.configs .recommended .rules , 'react/jsx-no-target-blank' : 'off' , 'react-refresh/only-export-components' : ['warn' , { allowConstantExport : true }], 'no-extra-semi' : 'error' , 'prettier/prettier' : 'error' , 'react/prop-types' : 'off' , },
現在不需要 props-type 也不會發生錯誤了。
組合 Component React 的一大特色是可以將小元件組合成更大的元件。透過這種組合方式,我們可以:
重複使用相似的 UI 結構
讓程式碼更容易維護
提高程式碼的可讀性
讓我們透過實際範例,一步步學習如何組合元件:
步驟 1:建立基礎圖片元件
src\App.jsx function MyImg ({ logo, txt } ) { return <img src ={logo} className ="logo" alt ={txt} /> ; } function LinkA ( ) { return ( <a href ="https://vite.dev" target ="_blank" > <MyImg logo ={viteLogo} txt ="Vite Logo" /> </a > ); } function LinkB ( ) { return ( <a href ="https://react.dev" target ="_blank" > <MyImg logo ={reactLogo} txt ="React Logo" /> </a > ); } function MyLogo ( ) { return ( <div > <LinkA /> <br /> <LinkB /> </div > ); }
步驟 2:優化元件結構
觀察上面的程式碼,我們可以發現 LinkA 和 LinkB 結構非常相似,只是傳入的資料不同。 讓我們將它們合併成一個更通用的 MyLink 元件:
src\App.jsx function MyImg ({ logo, txt } ) { return <img src ={logo} className ="logo" alt ={txt} /> ; } function MyLink ({ logo, txt, link } ) { return ( <a href ={link} target ="_blank" > <MyImg logo ={logo} txt ={txt} /> </a > ); } function MyLogo ( ) { return ( <div > <MyLink logo ={viteLogo} txt ="Vite Logo" link ="https://vite.dev" /> <br /> <MyLink logo ={reactLogo} txt ="React Logo" link ="https://react.dev" /> </div > ); }
步驟 3:資料與元件的分離
在實際開發中,資料通常會從 API 或配置檔案中獲取。讓我們將資料與元件分離,使程式碼更容易維護:
src\App.jsx function MyImg ({ imgItem } ) { return ( <img src ={imgItem.img} className ="logo" alt ={imgItem.name} /> ); } function MyLink ({ linkItem } ) { return ( <a href ={linkItem.url} target ="_blank" > <MyImg imgItem ={linkItem} /> </a > ); } function MyLogo ( ) { const dataList = [ { name : 'Vite Logo' , img : viteLogo, url : 'https://vite.dev' , }, { name : 'React Logo' , img : reactLogo, url : 'https://react.dev' , }, ]; return ( <div > <MyLink linkItem ={dataList[0]} /> <br /> <MyLink linkItem ={dataList[1]} /> </div > ); }
元件組合的最佳實踐
單一職責原則
每個元件只負責一個功能
避免元件過於複雜
適時拆分大型元件
資料流向
Props 從父元件向下傳遞
保持資料流向清晰
避免過度傳遞 props
命名規範
元件名稱使用大寫開頭
Props 名稱使用小駝峰式
保持命名語意化
程式碼組織
相關元件放在同一目錄
共用元件獨立管理
適當的檔案結構
效能考量
避免不必要的渲染
適時使用 memo 優化
注意資料結構的設計
匯出匯入(Export & Import) 隨著專案成長,我們需要將元件拆分到不同的檔案中,這樣可以:
提高程式碼的可維護性
方便團隊協作開發
實現元件的重複使用
匯出匯入的兩種方式:
預設匯出(Default Export)
一個檔案只能有一個預設匯出
匯入時可以使用任意名稱
適合作為檔案的主要元件
命名匯出(Named Export)
一個檔案可以有多個命名匯出
匯入時需要使用大括號和原始名稱
適合匯出多個相關的元件或函式
讓我們看看如何將元件拆分到不同檔案:
步驟 1:建立元件檔案結構
src/ ├── components/ │ ├── MyLogo/ │ │ ├── MyLogo.jsx # 主要元件 │ │ ├── MyLink.jsx # 子元件 │ │ └── MyImg.jsx # 子元件 │ └── ... └── App.jsx
步驟 2:撰寫各個元件檔案
import MyLogo from './components/MyLogo/MyLogo' ;import './App.css' ;export default function App ( ) { return ( <div > <MyLogo /> {/* 其他內容。.. */} </div > ); }
import reactLogo from '@assets/react.svg' ;import viteLogo from '@assets/vite.svg' ;import { MyLink } from './MyLink' ;export default function MyLogo ( ) { const dataList = [ { name : 'Vite Logo' , img : viteLogo, url : 'https://vite.dev' , }, { name : 'React Logo' , img : reactLogo, url : 'https://react.dev' , }, ]; return ( <div > {dataList.map(item => ( <MyLink key ={item.name} linkItem ={item} /> ))} </div > ); }
import { MyImg } from './MyImg' ;export function MyLink ({ linkItem } ) { return ( <a href ={linkItem.url} target ="_blank" > <MyImg imgItem ={linkItem} /> </a > ); }
export function MyImg ({ imgItem } ) { return ( <img src ={imgItem.img} className ="logo" alt ={imgItem.name} /> ); }
步驟 3:使用路徑別名(Path Alias)
在大型專案中,我們常常需要處理複雜的檔案路徑。路徑別名可以幫助我們:
簡化匯入路徑
避免過長的相對路徑
提高程式碼可讀性
設定路徑別名的步驟
編輯 vite.config.js
import { defineConfig } from 'vite' ;import react from '@vitejs/plugin-react-swc' ;export default defineConfig ({ plugins : [react ()], resolve : { alias : { '@' : '/src' , '@assets' : '/src/assets' , '@components' : '/src/components' , }, }, });
使用別名匯入
import MyButton from '../../components/MyButton' ;import logo from '../../assets/logo.svg' ;import MyButton from '@components/MyButton' ;import logo from '@assets/logo.svg' ;
實際應用範例:
src\components\MyLogo\MyLogo.jsx import reactLogo from '@assets/react.svg' ;import viteLogo from '@assets/vite.svg' ;import { MyLink } from '@components/MyLogo/MyLink' ;export default function MyLogo ( ) { return ( <div > <img src ={viteLogo} className ="logo" alt ="Vite Logo" /> <img src ={reactLogo} className ="logo" alt ="React Logo" /> </div > ); }
匯出匯入的最佳實踐
檔案組織
相關元件放在同一目錄
使用 index.js 匯出公開 API
保持檔案結構清晰
命名規範
檔案名稱與元件名稱一致
使用 PascalCase 命名元件檔案
使用 camelCase 命名工具檔案
匯出策略
主要元件使用預設匯出
工具函式使用命名匯出
避免混用兩種匯出方式
路徑管理
使用路徑別名避免深層相對路徑
集中管理路徑別名配置
保持匯入路徑一致性
效能考量
使用動態匯入延遲載入
適當拆分程式碼
避免循環依賴
換行符號錯誤處理
如果發生大量的 Delete ␍ 錯誤,這是換行符號(Line Ending) 的問題。
問題原因:
Windows 系統 :使用 CRLF (\r\n) 作為換行符號
Unix/Linux/Mac :使用 LF (\n) 作為換行符號
Git 差異 :不同系統間的換行符號不一致會造成版本控制問題
錯誤症狀:
# 在 Git 中會看到類似這樣的錯誤 warning: LF will be replaced by CRLF in src/App.jsx The file will have its original line endings in your working directory
解決方法:
方法 1:VSCode 右下角切換
點擊 VSCode 右下角的 CRLF 或 LF
選擇 LF 並重新載入檔案
方法 2:全域設定(推薦)
.vscode/settings.json { "files.eol" : "\n" , "files.insertFinalNewline" : true , "files.trimFinalNewlines" : true }
方法 3:Git 設定
git config core.autocrlf false git config core.eol lf
預防措施:
團隊統一使用 LF 格式
在 .editorconfig 檔案中設定換行符號規則
使用 ESLint 檢查換行符號一致性
渲染列表(List Rendering) 在 React 中,我們經常需要將資料陣列轉換成元件列表來顯示。React 提供了簡單且強大的方式來處理列表渲染:
使用陣列直接渲染
JSX 可以直接渲染元件陣列
React 會自動展開陣列內容
使用 map 方法轉換
讓我們透過實際範例來學習列表渲染:方法 1:直接使用元件陣列
function SimpleList ( ) { const items = ['React' , 'Vue' , 'Angular' ]; return ( <ul > {[ <li > 第一個項目</li > , <li > 第二個項目</li > , <li > 第三個項目</li > ]} </ul > ); } function LogoList ( ) { const dataList = [ { name : 'Vite Logo' , img : viteLogo, url : 'https://vite.dev' , }, { name : 'React Logo' , img : reactLogo, url : 'https://react.dev' , }, ]; return ( <div > {[ <MyLink linkItem ={dataList[0]} > </MyLink > , <MyLink linkItem ={dataList[1]} /> ]} </div > ); }
重要:列表渲染中的 key
當我們執行上面的程式碼時,會在 console 看到警告:
Warning: Each child in a list should have a unique "key" prop.
為什麼需要 key?
React 需要 key 屬性來識別列表中的每個元素,這樣才能:
正確追蹤元素的變化
優化重新渲染的效能
維護元素的狀態
key 的特性:
在同一個列表中必須唯一
不會作為 props 傳遞給元件
應該保持穩定(不要使用隨機數或索引)
選擇 key 的原則:
✅ 使用資料的唯一識別(如 ID)
✅ 使用穩定且唯一的值
❌ 避免使用索引(index)作為 key
❌ 避免使用不穩定的值(如隨機數)
讓我們修改程式碼,加入適當的 key:
方法 2:使用 map 方法
function SimpleList ( ) { const items = ['React' , 'Vue' , 'Angular' ]; return ( <ul > {items.map((item, index) => ( <li key ={item} > {item}</li > ))} </ul > ); } function LogoList ( ) { const dataList = [ { id : 1 , name : 'Vite Logo' , img : viteLogo, url : 'https://vite.dev' , }, { id : 2 , name : 'React Logo' , img : reactLogo, url : 'https://react.dev' , }, ]; return ( <div > {dataList.map(item => ( <MyLink key ={item.id} // 使用唯一識別作為 key linkItem ={item} /> ))} </div > ); }
map 方法的優點:
更具彈性
可以對資料進行轉換
可以加入條件判斷
程式碼更容易維護
更好的效能
React 可以更有效地追蹤變化
減少不必要的重新渲染
更好的可讀性
列表渲染的進階技巧
條件渲染
{items.map (item => ( item.isVisible && ( <ListItem key ={item.id} data ={item} /> ) ))}
資料轉換
{items.map (item => ( <ListItem key ={item.id} title ={item.name.toUpperCase()} {...item } /> ))}
巢狀列表
{categories.map (category => ( <div key ={category.id} > <h2 > {category.name}</h2 > <ul > {category.items.map(item => ( <li key ={item.id} > {item.name}</li > ))} </ul > </div > ))}
實作範例:規劃為渲染列表 現在讓我們把之前的 Logo 元件改寫成使用列表渲染的方式:
src\component\MyLogo\MyLogo.jsx export default function MyLogo ( ) { const dataList = [ { id : 1 , name : 'Vite Logo' , img : viteLogo, url : 'https://vite.dev' , }, { id : 2 , name : 'React Logo' , img : reactLogo, url : 'https://react.dev' , }, ]; return ( <div > {dataList.map((val) => ( <MyLink key ={val.id} linkItem ={val} /> ))} </div > ); }
這個改寫有幾個重點:
加入 id 作為唯一識別
使用 map 方法轉換資料
設定 key 屬性避免警告
使用 div 包裹確保返回單一根元素
移除未使用的 myBr 變數
這樣的寫法不僅更簡潔且易於擴充,同時也符合 React 的基本原則:
每個元件必須返回單一根元素
列表項目需要唯一的 key
資料與渲染邏輯分離
作為 children 傳遞 在 React 中,children 是一個特殊的 prop,它代表元件標籤之間的內容。這個機制讓我們能夠建立更靈活的可重用元件。
children prop 的三個重要特性:
自動傳遞
React 自動收集標籤內的內容
不需要明確宣告或傳遞
可以包含任何有效的 JSX
彈性使用
可以傳遞文字、數字
可以傳遞其他 React 元件
可以傳遞複雜的 JSX 結構
容器模式
適合建立包裝元件
方便實現共用的 UI 結構
提高程式碼重用性
為什麼需要 children?
想像一下,如果沒有 children,我們要建立一個通用的卡片元件:
function Card ({ title, content, footer } ) { return ( <div className ="card" > <div className ="card-header" > {title}</div > <div className ="card-body" > {content}</div > <div className ="card-footer" > {footer}</div > </div > ); } <Card title="標題" content={<p > 內容</p > } footer={<button > 確定</button > } />
使用 children 後:
function Card ({ children } ) { return ( <div className ="card" > {children} </div > ); } <Card > <h2 > 我的標題</h2 > <p > 任何內容都可以</p > <img src ="圖片。jpg" /> <button > 甚至可以放按鈕</button > </Card >
這就是為什麼我們需要 children:
更靈活的內容傳遞
更直覺的元件使用方式
更好的程式碼可維護性
實作範例:使用 children 改寫標題元件 讓我們透過實際的例子來了解如何使用 children prop:
src\component\MyH1\MyH1.jsx const MyH1 = ({ children } ) => <h1 > {children}</h1 > ;export default MyH1 ;
然後在 App 元件中使用它:
src\App.jsx import { useState } from 'react' ;import './App.css' ;import MyLogo from './component/MyLogo/MyLogo' ;import MyH1 from './component/MyH1/MyH1' ;export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; return ( <> <MyLogo /> {/* {h1Element} //捨棄 */} <MyH1 > {h1Title}</MyH1 > {/*由此處上層決定 children 是什麼*/} <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
互動性 畫面上的某些內容會根據使用者輸入事件進行更新。例如 click 或 change value,在 React 中,隨時間操作變化的資料稱為 state 狀態。您可以為任何 component 元件新增 state 狀態,並根據需要更新它。在本章中,您將學習如何編寫處理互動、更新其狀態並隨時間顯示不同輸出的元件。
事件處理(Events) React 實作了一個跨瀏覽器的合成事件系統(SyntheticEvent),統一了各瀏覽器的事件處理差異,讓開發者不需要擔心瀏覽器相容性問題。
React 事件 vs HTML 事件
特性
HTML 事件
React 事件
命名方式
小寫 onclick
駝峰命名 onClick
事件處理
字串 "alert('Hi')"
函式 {handleClick}
阻止預設
return false
e.preventDefault()
事件物件
原生 Event
SyntheticEvent(跨瀏覽器)
<button onclick="alert('Clicked!')" >Click </button> <button onClick ={() => alert('Clicked!')}>Click</button >
在 React 中,我們有三種主要的事件處理方式:
內聯匿名函式 :適合簡單的邏輯處理
元件內部函式 :推薦的做法,讓程式碼更清晰且易於維護
傳遞給子元件 :在父元件中管理狀態,讓子元件保持純粹
function App ( ) { return ( <button onClick ={() => alert('Clicked!')}> Click Me </button > ); } function App ( ) { const handleClick = ( ) => { alert ('Clicked!' ); }; return <button onClick ={handleClick} > Click Me</button > ; } function MyButton ({ onClick, children } ) { return <button onClick ={onClick} > {children}</button > ; } function App ( ) { const handleClick = ( ) => { console .log ('Button clicked!' ); }; return <MyButton onClick ={handleClick} > Click Me</MyButton > ; }
常見錯誤:不要立即執行函式!
<button onClick={handleClick ()}>Click </button> <button onClick ={handleClick} > Click</button > <button onClick ={() => handleClick(123)}>Click</button >
常用的 React 事件 React 支援所有標準的 DOM 事件,這裡列出最常用的:
事件類型
事件名稱
說明
範例
滑鼠事件
onClick
點擊
<button onClick={handleClick}>點擊</button>
onDoubleClick
雙擊
<div onDoubleClick={handleDoubleClick}>雙擊</div>
onMouseEnter
滑鼠移入
<div onMouseEnter={handleEnter}>移入</div>
onMouseLeave
滑鼠移出
<div onMouseLeave={handleLeave}>移出</div>
onMouseMove
滑鼠移動
<div onMouseMove={handleMove}>移動</div>
onMouseDown
滑鼠按下
<button onMouseDown={handleDown}>按下</button>
onMouseUp
滑鼠放開
<button onMouseUp={handleUp}>放開</button>
鍵盤事件
onKeyDown
按鍵按下
<input onKeyDown={handleKeyDown} />
onKeyUp
按鍵放開
<input onKeyUp={handleKeyUp} />
onKeyPress
按鍵按下(已廢棄)
建議使用 onKeyDown
表單事件
onChange
輸入值改變
<input onChange={handleChange} />
onSubmit
表單提交
<form onSubmit={handleSubmit}>
onFocus
獲得焦點
<input onFocus={handleFocus} />
onBlur
失去焦點
<input onBlur={handleBlur} />
其他事件
onScroll
滾動
<div onScroll={handleScroll}>滾動</div>
onDrag
拖曳
<div onDrag={handleDrag}>拖曳</div>
onDrop
放置
<div onDrop={handleDrop}>放置</div>
onLoad
載入完成
<img onLoad={handleLoad} />
onError
載入錯誤
<img onError={handleError} />
實際範例:綜合事件處理
function EventDemo ( ) { const [message, setMessage] = useState ('請與我互動' ); const [position, setPosition] = useState ({ x : 0 , y : 0 }); const handleClick = ( ) => setMessage ('你點擊了按鈕!' ); const handleMouseEnter = ( ) => setMessage ('滑鼠移入了!' ); const handleMouseLeave = ( ) => setMessage ('滑鼠移出了!' ); const handleMouseMove = (e ) => { setPosition ({ x : e.clientX , y : e.clientY }); setMessage (`滑鼠位置:${e.clientX} , ${e.clientY} ` ); }; const handleKeyDown = (e ) => { setMessage (`按下了鍵:${e.key} ` ); }; const handleChange = (e ) => { setMessage (`輸入內容:${e.target.value} ` ); }; return ( <div > <h3 > {message}</h3 > <p > 滑鼠座標:{position.x}, {position.y}</p > <button onClick ={handleClick} > 點擊我</button > <div onMouseEnter ={handleMouseEnter} onMouseLeave ={handleMouseLeave} onMouseMove ={handleMouseMove} style ={{ padding: '20px ', border: '1px solid #ccc ', margin: '10px 0 ' }} > 滑鼠互動區域 </div > <input onChange ={handleChange} onKeyDown ={handleKeyDown} placeholder ="輸入文字或按鍵" /> </div > ); }
事件物件(Event Object) React 事件處理函式會自動接收一個 SyntheticEvent 物件,它封裝了原生 DOM 事件,提供跨瀏覽器的一致性:
屬性
說明
範例
event.type
事件類型
"click", "change", "keydown"
event.target
觸發事件的元素
<button>, <input>
event.currentTarget
綁定事件的元素
通常與 target 相同
event.clientX / event.clientY
滑鼠相對於視窗的座標
150, 200
event.pageX / event.pageY
滑鼠相對於頁面的座標
150, 200
event.screenX / event.screenY
滑鼠相對於螢幕的座標
150, 200
event.preventDefault()
阻止預設行為
阻止表單提交、連結跳轉
event.stopPropagation()
阻止事件冒泡
防止事件向上傳播
實際範例:事件物件使用
function EventObjectDemo ( ) { const [log, setLog] = useState ([]); const addLog = (message ) => { setLog (prev => [...prev, `${new Date ().toLocaleTimeString()} : ${message} ` ]); }; const handleClick = (event ) => { addLog (`點擊事件 - 類型:${event.type} ` ); addLog (`目標元素:${event.target.tagName} ` ); }; const handleFormSubmit = (event ) => { event.preventDefault (); addLog ('表單提交被阻止' ); }; const handleInputChange = (event ) => { addLog (`輸入值:${event.target.value} ` ); }; const handleKeyDown = (event ) => { if (event.key === 'Enter' ) { addLog ('按下了 Enter 鍵' ); } }; return ( <div > <h3 > 事件日誌</h3 > <div > {log.map((item, index) => ( <div key ={index} > {item}</div > ))} </div > <button onClick ={handleClick} > 點擊我</button > <form onSubmit ={handleFormSubmit} > <input onChange ={handleInputChange} onKeyDown ={handleKeyDown} placeholder ="輸入文字" /> <button type ="submit" > 提交表單</button > </form > </div > ); }
React 不支援 return false
在 HTML 中可以用 return false 阻止預設行為,但在 React 中必須明確呼叫 preventDefault():
<form onsubmit="return false" > <form onSubmit ={(e) => e.preventDefault()}>
實作範例:事件物件與處理 讓我們透過實際的專案範例來學習事件處理的應用。我們將建立一個按鈕元件和表單元件,展示不同的事件處理方式。
步驟 1:建立按鈕元件
src\component\MyButton\MyButton.jsx const handleClick = ( ) => console .log ('is click event!!' );const MyButton = ({ children } ) => <button onClick ={handleClick} > {children}</button > ;export default MyButton ;
這個按鈕元件展示了基本的事件處理:
使用 onClick 屬性綁定點擊事件
事件處理函式 handleClick 在元件內部定義
透過 children prop 接收按鈕文字
步驟 2:在 App 中使用按鈕元件
src\App.jsx import { useState } from 'react' ;import './App.css' ;import MyLogo from './component/MyLogo/MyLogo' ;import MyH1 from './component/MyH1/MyH1' ;import MyButton from './component/MyButton/MyButton' ;export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; return ( <> <MyLogo /> <MyH1 > {h1Title}</MyH1 > <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <MyButton > Click Me!</MyButton > {/*追加此行*/} <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > {/*原本的事件按鈕*/} <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
這裡展示了兩種不同的按鈕:
自定義元件 :<MyButton> 使用元件內部的事件處理
內聯事件 :onClick={() => setCount(...)} 直接在 JSX 中定義
步驟 3:建立表單元件(preventDefault 應用)
src\component\MyForm\MyForm.jsx import MyButton from '../MyButton/MyButton' ;const MyForm = ( ) => { const handleSubmit = (e ) => { e.preventDefault (); console .log ('submit' ); }; const handelChangeText = (e ) => { console .log (e.target .value ); }; return ( <form onSubmit ={handleSubmit} > <input type ="text" placeholder ="請輸入內容" onChange ={handelChangeText} /> <MyButton > 提交</MyButton > <hr /> </form > ); }; export default MyForm ;
這個表單元件展示了:
**preventDefault()**:阻止表單的預設提交行為(頁面重新載入)
onChange 事件 :監聽輸入框內容變化
事件物件使用 :透過 e.target.value 取得輸入值
步驟 4:整合表單到 App
src\App.jsx import MyForm from './component/MyForm/MyForm' ;export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; return ( <> <MyLogo /> <MyH1 > {h1Title}</MyH1 > <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <MyForm /> {/*追加此行*/} <MyButton > Click Me!</MyButton > <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
事件處理最佳實踐:狀態提升 通常建議將事件處理邏輯放在父元件(狀態所在處),子元件只負責觸發事件:
這種模式的優勢:
✅ 關注點分離:UI 和邏輯分開
✅ 可重用性:子元件可以在不同場景使用
✅ 易於測試:邏輯集中在父元件
✅ 狀態管理清晰:所有狀態在一個地方
function SearchInput ({ value, onChange, onSearch } ) { return ( <form onSubmit ={(e) => { e.preventDefault(); onSearch(); }}> <input type ="text" value ={value} onChange ={onChange} placeholder ="搜尋。.." /> <button type ="submit" > 搜尋</button > </form > ); } function App ( ) { const [searchTerm, setSearchTerm] = useState ('' ); const [results, setResults] = useState ([]); const handleInputChange = (e ) => { setSearchTerm (e.target .value ); }; const handleSearch = ( ) => { console .log ('搜尋:' , searchTerm); setResults ([`結果 1 for ${searchTerm} ` , `結果 2 for ${searchTerm} ` ]); }; return ( <div > <SearchInput value ={searchTerm} onChange ={handleInputChange} onSearch ={handleSearch} /> <ul > {results.map((result, index) => ( <li key ={index} > {result}</li > ))} </ul > </div > ); }
實作範例:狀態提升 讓我們透過實際範例來學習狀態提升的概念。我們將修改 MyForm 元件,將事件處理邏輯提升到父元件 App 中。
步驟 1:修改 MyForm 元件(移除內部事件處理)
這個修改展示了:
移除內部事件處理 :註解掉原本的 handleSubmit 和 handelChangeText
接收 props 函式 :透過 onLokiSubmit 和 onLokiChange 接收父元件傳入的事件處理函式
純粹的 UI 元件 :元件只負責渲染,不包含業務邏輯
src\component\MyForm\MyForm.jsx import MyButton from '../MyButton/MyButton' ;const MyForm = ({ onLokiSubmit, onLokiChange } ) => { return ( <form onSubmit ={onLokiSubmit} > <input type ="text" placeholder ="請輸入內容" onChange ={onLokiChange} /> <MyButton > 提交</MyButton > <hr /> </form > ); }; export default MyForm ;
步驟 2:在 App 中實現事件處理邏輯
這個實作展示了:
事件處理提升 :將 MyForm 的事件處理邏輯移到 App 元件
自由命名 props :父元件可以自由命名函式(如 onPasswordSubmit),然後傳給子元件
props 傳遞 :透過 onLokiSubmit={onPasswordSubmit} 將父元件的函式傳給子元件
src\App.jsx export default function App ( ) { const [count, setCount] = useState (0 ); const h1Title = 'Vite + React' ; const onPasswordSubmit = (e ) => { e.preventDefault (); console .log ('submit' ); }; const onPasswordChange = (e ) => { console .log (e.target .value ); }; return ( <> <MyLogo /> <MyH1 > {h1Title}</MyH1 > <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <MyForm onLokiSubmit ={onPasswordSubmit} onLokiChange ={onPasswordChange} /> <MyButton > Click Me!</MyButton > <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
狀態提升的優勢:
關注點分離 :UI 元件專注於渲染,邏輯元件專注於業務邏輯
可重用性 :MyForm 可以在不同場景使用,只需傳入不同的事件處理函式
易於測試 :邏輯集中在父元件,更容易進行單元測試
狀態管理清晰 :所有狀態和邏輯都在父元件中,便於管理
狀態提升的核心概念
子元件 :只負責 UI 渲染,透過 props 接收事件處理函式
父元件 :負責狀態管理和業務邏輯,將函式透過 props 傳遞給子元件
命名自由 :父元件可以自由命名函式,子元件透過 props 名稱接收
State 狀態管理 在傳統的 JavaScript 中,我們可以直接操作 DOM 來更新畫面:
let count = 0 ;const button = document .getElementById ('myButton' );const display = document .getElementById ('display' );button.addEventListener ('click' , () => { count++; display.textContent = `count is ${count} ` ; });
但在 React 中,我們不能直接操作 DOM,因為 React 使用虛擬 DOM(Virtual DOM) 來管理畫面更新。
虛擬 DOM 的概念 React 的虛擬 DOM 是一個 JavaScript 物件,它代表真實 DOM 的結構:
const virtualDOM = { type : 'div' , props : { children : [ { type : 'button' , props : { onClick : handleClick, children : 'Click me' } }, { type : 'span' , props : { children : `count is ${count} ` } } ] } };
虛擬 DOM 的工作流程:
狀態改變 :當 useState 的 setter 被調用時
重新渲染 :React 重新執行元件函式,產生新的虛擬 DOM
差異比較 :React 比較新舊虛擬 DOM 的差異(Diffing)
更新真實 DOM :只更新有變化的部分
graph LR
A["狀態改變<br/>(setState/setter)"]
B["元件重新執行<br/>產生新虛擬 DOM"]
C["差異比較(Diffing)<br/>新舊虛擬 DOM"]
D["只更新有變化部分<br/>同步至真實 DOM"]
A --> B --> C --> D
為什麼需要虛擬 DOM?
傳統的 DOM 操作存在以下問題:
效能瓶頸 :直接操作 DOM 會觸發瀏覽器的重排(reflow)和重繪(repaint),這些都是昂貴的操作
頻繁更新 :每次狀態改變都要重新計算整個 DOM 樹
難以追蹤 :手動管理 DOM 更新容易出錯,難以維護
虛擬 DOM 的優勢:
批量更新 :將多個 DOM 操作合併為一次更新
差異比較 :只更新真正改變的部分,避免不必要的 DOM 操作
預測性 :透過 JavaScript 物件操作,比直接操作 DOM 更快
跨瀏覽器一致性 :統一不同瀏覽器的 DOM 操作差異
效能對比範例:
function updateCounter (count ) { document .getElementById ('counter' ).textContent = count; document .getElementById ('status' ).textContent = count > 10 ? 'High' : 'Low' ; document .getElementById ('progress' ).style .width = count + '%' ; } function Counter ({ count } ) { return ( <div > <span > {count}</span > <span > {count > 10 ? 'High' : 'Low'}</span > <div style ={{ width: count + '%' }} /> </div > ); }
React 的虛擬 DOM 設計讓開發者可以專注於狀態管理,而不用擔心 DOM 操作的效能問題。
useState Hook 基本概念 useState 是 React 提供的一個 Hook,用於在函式元件中管理狀態:
const [state, setState] = useState (initialValue);
useState 的特點:
參數 :initialValue 是狀態的初始值
返回值 :返回一個陣列,包含兩個元素
state:當前的狀態值
setState:更新狀態的函式
重新渲染 :當 setState 被調用時,元件會重新渲染
基本使用範例:
import { useState } from 'react' ;function Counter ( ) { const [count, setCount] = useState (0 ); return ( <div > <p > count is {count}</p > <button onClick ={() => setCount(count + 1)}> Click me </button > </div > ); }
狀態更新的兩種方式 React 提供了兩種更新狀態的方式,理解它們的差異對於避免常見的狀態更新錯誤非常重要。
方式 1:直接傳遞新值
function Counter ( ) { const [count, setCount] = useState (0 ); const handleClick = ( ) => { setCount (count + 1 ); }; return ( <div > <p > Count: {count}</p > <button onClick ={handleClick} > +1</button > </div > ); }
方式 2:使用函式更新(推薦)
function Counter ( ) { const [count, setCount] = useState (0 ); const handleClick = ( ) => { setCount (prevCount => prevCount + 1 ); }; return ( <div > <p > Count: {count}</p > <button onClick ={handleClick} > +1</button > </div > ); }
為什麼推薦使用函式更新?
讓我們透過實際範例來理解兩種方式的差異:
問題場景:快速點擊按鈕
function ProblematicCounter ( ) { const [count, setCount] = useState (0 ); const handleClick = ( ) => { setCount (count + 1 ); setCount (count + 1 ); }; return ( <div > <p > Count: {count}</p > <button onClick ={handleClick} > +2 (可能有問題)</button > </div > ); }
解決方案:使用函式更新
function FixedCounter ( ) { const [count, setCount] = useState (0 ); const handleClick = ( ) => { setCount (prevCount => prevCount + 1 ); setCount (prevCount => prevCount + 1 ); }; return ( <div > <p > Count: {count}</p > <button onClick ={handleClick} > +2 (正確)</button > </div > ); }
關鍵差異總結:
特性
直接傳遞新值
函式更新
基於值
當前渲染的狀態值
最新的狀態值
異步安全
❌ 可能使用舊值
✅ 總是使用最新值
批量更新
❌ 可能計算錯誤
✅ 正確處理
適用場景
簡單的單次更新
複雜邏輯、多次更新
最佳實踐建議:
簡單更新 :如果只是簡單的單次更新,兩種方式都可以
複雜邏輯 :涉及條件判斷或多次更新時,必須使用函式更新
依賴前值 :當新值依賴於前一個值時,使用函式更新
批量操作 :多個狀態更新時,使用函式更新確保正確性
實作範例:圖片幻燈片 讓我們透過建立一個圖片幻燈片元件來學習 useState 的實際應用。這個範例將展示多個狀態管理、條件渲染,以及狀態提升的概念。
步驟 1:準備圖片資料
src\component\MyGallery\data.js const data = [ { title : 'Lorem ipsum dolor. A' , url : 'https://picsum.photos/300/200/?random=10' , }, { title : 'Lorem ipsum dolor. B' , url : 'https://picsum.photos/300/200/?random=11' , }, { title : 'Lorem ipsum dolor. C' , url : 'https://picsum.photos/300/200/?random=12' , }, { title : 'Lorem ipsum dolor. D' , url : 'https://picsum.photos/300/200/?random=13' , }, ]; export default data;
步驟 2:建立基礎幻燈片元件
src\component\MyGallery\MyGallery.jsx import { useState } from 'react' ;import data from './data.js' ;const MyGallery = ( ) => { const [idx, setIdx] = useState (0 ); const handlePrev = ( ) => { const newCount = (idx + data.length - 1 ) % data.length ; setIdx (newCount); }; const handleNext = ( ) => { setIdx ((idx ) => (idx + data.length + 1 ) % data.length ); }; return ( <div style ={{ display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', padding: '20px ', border: '1px solid ', }} > <div style ={{ marginBottom: '10px ' }}> <button onClick ={handlePrev} > prev</button > <button onClick ={handleNext} > next</button > </div > <img src ={data[idx].url} style ={{ display: 'block ' }} /> <label > {data[idx].title}</label > </div > ); }; export default MyGallery ;
這個基礎版本展示了:
單一狀態管理 :使用 idx 來追蹤當前圖片
狀態更新 :handlePrev 使用直接賦值,handleNext 使用函式更新
陣列索引計算 :使用模運算實現循環切換
步驟 3:添加多個狀態管理 一個元件可以多個 state 管理,我們多控制一個 boolean 是否要顯示 title。
src\component\MyGallery\MyGallery.jsx import { useState } from 'react' ;import data from './data.js' ;const MyGallery = ( ) => { const [idx, setIdx] = useState (0 ); const [toShow, setToShow] = useState (true ); const handleControl = (todo ) => { switch (todo) { case 'next' : setIdx ((idx ) => (idx + data.length + 1 ) % data.length ); break ; case 'prev' : setIdx ((idx ) => (idx + data.length - 1 ) % data.length ); break ; } }; return ( <div style ={{ display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', padding: '20px ', border: '1px solid ', }} > <div style ={{ marginBottom: '10px ' }}> <button onClick ={() => handleControl('prev')}>prev</button > <button onClick ={() => setToShow((bool) => !bool)}>{toShow ? 'hide' : 'show'}</button > <button onClick ={() => handleControl('next')}>next</button > </div > <img src ={data[idx].url} style ={{ display: 'block ' }} /> {toShow && <label > {data[idx].title}</label > } </div > ); }; export default MyGallery ;
這個版本展示了:
多個狀態 :idx 和 toShow 兩個獨立的狀態
條件渲染 :使用 {toShow && <label>} 來控制標題顯示
函式更新 :setToShow((bool) => !bool) 切換布林值
步驟 4:狀態提升到父元件
src\component\MyGallery\MyGallery.jsx import { useState } from 'react' ;import data from './data.js' ;const MyGallery = ({ toShow, setToShow } ) => { const [idx, setIdx] = useState (0 ); const handleControl = (todo ) => { switch (todo) { case 'next' : setIdx ((idx ) => (idx + data.length + 1 ) % data.length ); break ; case 'prev' : setIdx ((idx ) => (idx + data.length - 1 ) % data.length ); break ; } }; return ( <div style ={{ display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', padding: '20px ', border: '1px solid ', }} > <div style ={{ marginBottom: '10px ' }}> <button onClick ={() => handleControl('prev')}>prev</button > <button onClick ={() => setToShow((bool) => !bool)}>{toShow ? 'hide' : 'show'}</button > <button onClick ={() => handleControl('next')}>next</button > </div > <img src ={data[idx].url} style ={{ display: 'block ' }} /> {toShow && <label > {data[idx].title}</label > } </div > ); }; export default MyGallery ;
src\App.jsx import { useState } from 'react' ;import MyGallery from './component/MyGallery/MyGallery' ;export default function App ( ) { const [count, setCount] = useState (0 ); const [toShow, setToShow] = useState (true ); return ( <> <MyLogo /> <MyH1 > {h1Title}</MyH1 > <MyGallery toShow ={toShow} setToShow ={setToShow} /> <MyGallery {... { toShow , setToShow }} /> {/*兩種寫法,如果故意同名可以用解構來速寫*/} <div className ="card" style ={{ color: 'red ', background: 'black ' }}> <MyForm onLokiSubmit ={onPasswordSubmit} onLokiChange ={onPasswordChange} /> <MyButton > Click Me!</MyButton > <button onClick ={() => setCount((count) => count + 1)}>count is {count}</button > <p > Edit <code > src/App.jsx</code > and save to test HMR </p > </div > <p className ="read-the-docs" > Click on the Vite and React logos to learn more</p > </> ); }
這個最終版本展示了:
狀態提升 :將 toShow 狀態提升到 App 元件
共享狀態 :兩個 MyGallery 元件共享同一個 toShow 狀態
props 傳遞 :透過 props 將狀態和更新函式傳遞給子元件
解構語法 :{...{ toShow, setToShow }} 簡化 props 傳遞
條件渲染技巧
使用 {condition && <element>} 來實現條件渲染:
true && element 回傳 element
false && element 回傳 false(React 會忽略)
比 condition ? <element> : null 更簡潔
參考文獻