React 基礎:JSX、元件、狀態與表單

React 是在瀏覽器裡用函式描述畫面的函式庫。資料一變,同一個函式再跑一次,畫面跟著換。這一篇先看它是什麼、為什麼用它,再寫 JSX、元件、狀態、事件、列表與表單。
認識 React
本講義的 React 是 19.3,React Router 是 8,Vite 是 8。元件與 Hooks 對照 react.dev,路由對照 reactrouter.com。查到要從 react-router-dom 引入,或寫成 class 元件,那是舊版,這門課不跟。
這一章先看框架為何存在,以及這門課為什麼選 React。
為何使用框架
沒有框架(framework)時,你要自己驅動 DOM,以及跟畫面連動的瀏覽器 API。用了框架,你改寫元件與 JSX 這種描述畫面的程式;框架當中間的代理人,替你跟瀏覽器溝通,所以更新 DOM 與相關的瀏覽器物件模型(Browser Object Model,BOM),對開發者比較有效率。
graph LR
code["Your code<br/>JSX / components"] --> fw["Framework"]
fw --> browser["Browser"]
browser --> domNode["DOM"]
browser --> bomNode["BOM"]
- 框架站在你的寫法和瀏覽器之間:你描述畫面,函式庫更新 DOM;瀏覽器 API 與 BOM 也透過這套模型來用,不必每一步都自己接上。
- 自己一步步指定要改哪個節點,叫做命令式(imperative);只描述畫面現在該長什麼樣子,叫做宣告式(declarative)。
- 框架簡化的是你寫出來的控制流程;真正的畫面一變,瀏覽器仍會重排(reflow)與重繪(repaint)。
為何選擇 React
Vue、Angular、React 都能把狀態畫成畫面。差別在畫面寫在哪裡、邏輯跟著哪一種語法走,以及專案目錄要不要由工具先定好。這一節先把三套都看過,再說明這門課為什麼停在 React。
Vue
Vue 的定位是漸進式框架(progressive framework)。同一套工具可以只嵌進既有頁面的一小塊,也可以長大成整份應用,範圍由你決定。
常見寫法是單檔元件(Single-File Component,SFC)。一個檔案裡放下這塊畫面的模板、腳本與樣式;使用者看到的結構寫在模板(template)裡,條件與清單用模板自己的指令(template directive),而不是一般的 JavaScript 陳述。官方的路由與狀態也沿同一套文件與套件走,專案往下接時仍在 Vue 的約定裡。
核心以外,往下做完整專案時還常再接這些套件。
- Vue Router 負責網址與頁面的對應。
- Pinia 管理多個元件共用的狀態。舊專案裡還可能看到 Vuex。
- 要做成全端應用時,常改接 Nuxt。
- 介面元件庫(component library)常見 Element Plus 與 Vuetify。
Angular
Angular 把模板、路由、表單和專案結構收得更完整,一開始就用同一套框架約定把這些擺在一起。新專案通常依這套約定長出來,目錄的形狀在產生器裡就定了。
畫面一樣寫在模板裡。路由怎麼對應頁面、表單怎麼綁資料、資料夾怎麼分層,都有框架自己的做法。模板和這些約定讓同一個專案裡的寫法穩定;要另外熟悉的,是那一種模板語法與目錄。
核心以外,就業與實務往下還常再接這些套件。
- 響應式程式庫(RxJS)處理非同步與資料流,寫 Angular 時幾乎都會碰到。
- 大型應用的共用狀態常再接 NgRx。
- 介面元件庫常見 Angular Material。
- 職缺與周邊生態常要求會 TypeScript。
React
React 裡,元件是一支函式。這塊畫面的判斷、事件與要顯示的內容留在 JavaScript,標記用 JSX 寫進同一支函式的回傳。
它不另外準備一套模板語言,也不規定專案目錄該怎麼切。檔案怎麼分、路由與共用狀態日後接哪一套,函式庫不過問。你沿用已經會的 JavaScript,再多學 JSX 這一種寫法。
核心以外,往下常再接這些套件。
- React Router 負責換頁。本課後面會教。
- 全端與伺服器端渲染(server-side rendering,SSR)常見 Next.js。
- 共用狀態常見 Zustand 與 Redux Toolkit。
- 介面元件庫常見 MUI 與 Ant Design。
這門課的取捨
這門課選 React,是因為它留在 JavaScript 旁邊,寫畫面的自由仍是寫 JS 的自由。元件怎麼判斷、資料怎麼變成畫面,都還是一般的 JavaScript 與 JSX,而不是改去學一套模板語言。
準確地說,React 是描述使用者介面的函式庫(library)。路由與共用狀態不在核心裡,要用再接上;這門課後面會用到 React Router。開發與打包交給 Vite,外觀交給 Tailwind,畫面與狀態留給 React,三件分開。把路由、資料與伺服器端渲染綁成一套的全端框架,例如 Next.js,是更後面的題目。官方的元件與 Hooks 說明在 react.dev,和課堂上的函式元件是同一套寫法。
代價也寫在前面。專案骨架要自己定:檔案怎麼分、路由與共用狀態接哪一套,用到的那一天再補。JavaScript 要夠熟,因為畫面以外的流程仍是一般的 JS 在接。
小技巧:函式庫還是框架
React 的核心是函式庫,負責描述畫面。路由、共用狀態與專案目錄要另外接,所以它不像把模板、路由、表單一次配齊的框架。日常對話仍常叫它框架。兩種說法指的是同一套工具,差在有沒有把周邊套件算進去。
什麼是 React
React 由 Meta(前身 Facebook)主導,是用來描述使用者介面(user interface)的 JavaScript 函式庫。你用函式寫出畫面現在的長相,瀏覽器裡的節點由另一個套件掛上去。這一節分開看它站在哪一層、頁面怎麼由元件拼成、JSX 為什麼要先編譯,以及兩個套件各管什麼。
定位
React 專注在 View 層,也就是使用者看得到、點得到的那一層。標題文字、清單有幾列、按鈕現在能不能按,都是這一層要回答的事。
資料庫、帳號驗證與伺服器不在這一套裡。那些程式把資料準備好;React 負責把已經到手的資料畫成畫面。它不取代後端,也不規定資料存在哪裡。
做出來的應用常常是單頁應用(Single Page Application,SPA)。瀏覽器先載入一份 index.html,裡面留一個掛載點 #root。之後換畫面靠 JavaScript 更新這一塊,不必整頁重新載入。掛載點怎麼接、入口檔怎麼把元件放進去,下一章建立專案會對著實際檔案做一次。
元件化
做法是元件化(component-based)。標題、清單、表單各自是一塊元件(component),每塊用函式描述自己現在的長相,頁面再由這些區塊拼起來。
同一種清單可以出現在不同位置,因為它是同一支函式。你改的是這支函式回傳的樣子,用到它的地方跟著那一支走。判斷與事件可以寫在元件裡面;資料庫連線與登入流程仍留在這套畫面函式庫外面。
JSX 與編譯
直接用 JavaScript 建立每個節點,參數會拉得很長,畫面結構也不容易一眼看完。JSX 是語法糖(syntactic sugar):在函式裡寫接近 HTML 的標記,讀的時候對得上畫面上的層次。
瀏覽器不執行 JSX。開發時由 Vite 編成一般的 JavaScript,瀏覽器跑的是編成後的程式。正式站載入的同樣是建置過的結果,不是編輯器裡的 JSX 原文。標記怎麼寫、花括號裡能放什麼,後面的 JSX 章再拆開練。
react 與 react-dom
在瀏覽器裡,React 分成兩個套件。寫元件的那一半,和把畫面放進頁面的那一半,不在同一個套件裡。
react管元件、狀態與 Hooks。你宣告的函式,以及讓函式記住資料的工具,從這個套件來。react-dom管掛載。它把上一項描述好的畫面,放進瀏覽器的 DOM。
少了前者,沒有元件與狀態可寫。少了後者,描述好的畫面不會出現在頁面上。函式怎麼掛上去、第一個元件回傳什麼,下一章建立專案再對著入口檔看。
建立專案
上一階段已經裝好 Node、nvs 與 pnpm。這一章先在練習倉庫根目錄用 Vite 做出一個 React 試做專案,確認畫面跑得起來,再切回課堂準備好的 03-react。後面的元件、檢查工具與資料流,都在 03-react 裡看。
用 Vite 建立 React 專案
工具鏈上一階段已經能跑,這裡不必重裝。上一階段的 Vite 範本是純 HTML、CSS、JavaScript;這次改選 React,產生器會多放 main.jsx、App.jsx 與轉換 JSX 的插件。先在 sandbox 走完建立、安裝、看檔、啟動,再換到 03-react,讓大家的練習環境一致。
認識 React 範本
Vite 同時是開發伺服器與打包器,上一階段已經用它看過 Tailwind。React 專案仍由它啟動:開發時瀏覽器跟它要目前用到的模組,JSX 會先編成一般 JavaScript。上線另跑建置,把用到的模組打包到成品資料夾。
範本用互動選單或一行參數都可以。選單裡 Framework 選 React,Variant 選 JavaScript。等價的參數是 --template react。Variant 選 TypeScript 時,範本名稱是 react-ts,檔案會變成 .tsx。這門課用 JavaScript 那一項。
pnpm dev # 開發伺服器;終端機印出的 Local 多半是 http://localhost:5173/ |
手動建立專案(sandbox)
在 studiesAdvanced 倉庫根目錄自己做出一份能跑的 React 專案。它和課堂的 03-react 分開,做壞了把整個 sandbox 資料夾刪掉重建即可。後面的示範留在 03-react。
# sandbox:試做專案,建置結果在 sandbox/dist/ |
已安裝過可跳過
Node、nvs、pnpm 上一階段已裝好,這裡不要重裝。若 03-react 已經能開開發伺服器,而且你做過一次 Vite 的 React 專案,可以略過 sandbox,從「改用 03-react」的安裝開始。第一次做 React 專案,請把下面四步走完。
步驟 1:建立專案
在 VS Code 開一個新的終端機,它會停在倉庫根目錄(Windows 多半是 PowerShell,macOS 是 zsh,後面的指令兩邊相同)。先確認旁邊看得到 03-react 資料夾,再下建立指令。
這是課堂上建立 React 專案的指令:請 pnpm 下載 Vite 官方的專案產生器,用 React 加 JavaScript 的範本,在目前位置建立 sandbox 資料夾。先看完整指令,再看拆開的註解:
pnpm create vite@latest sandbox -- --template react |
pnpm create # 執行「專案產生器」:暫時下載 create-xxx 套件來跑,用完不留在電腦上 |
就算寫了 --template react,終端機多半還會再問兩題。Which linter to use 選 ESLint。本課一律選 ESLint。選完之後,產生器會拿掉範本裡的 oxlint 套件與 .oxlintrc.json,改加入 eslint、@eslint/js、eslint-plugin-react-hooks、eslint-plugin-react-refresh、globals,寫入 eslint.config.js,並把 scripts.lint 改成 eslint .。sandbox 先認得這份骨架;規則怎麼寫、怎麼跟 Prettier 搭配,留到後面的工具鏈,對照 03-react。Install with pnpm and start now 選 No,安裝與啟動留給下面兩步,你才看得到指令自己跑完。
試試:不指定範本
也可以拿掉範本參數,改用互動選單建立另一個資料夾。畫面會一步步問你框架(Framework)與語言(Variant),用方向鍵選、Enter 確定。Framework 選 React,Variant 選 JavaScript,會得到和上面一樣的結果。Variant 還有 TypeScript、JavaScript + React Compiler、RSC,以及名稱後面帶箭頭、會改跑別的指令的項目,那些先不要選。Which linter to use 同樣選 ESLint,是否立刻安裝仍選 No。試完不需要的資料夾直接刪掉即可。
# 拿掉 --template,改走互動選單;sandbox2 是另一個資料夾 |
步驟 2:安裝依賴
產生器只負責放檔案,還沒有下載任何套件。進入資料夾,依選完 ESLint 之後的 package.json 安裝 React、Vite 與檢查套件。
cd sandbox # 進入剛建立的專案資料夾 |
範本已經寫好 React、React DOM、Vite 與 @vitejs/plugin-react。選 ESLint 之後,同一份清單還會多出 eslint、@eslint/js、eslint-plugin-react-hooks、eslint-plugin-react-refresh 與 globals,lint 指令是 eslint .。這一步只是下載,不必再手動改版本。React 與 React DOM 要一起在;清單裡少一邊時,畫面畫不上去。
步驟 3:認識檔案結構
安裝完先看檔,再啟動。HTML 幾乎是空的,真正的畫面在 src/App.jsx,src/main.jsx 負責把那個元件掛進頁面。
# sandbox/index.html 空頁面,只留 #root,腳本指向 /src/main.jsx |
對一下檢查器改過的片段。react、react-dom、vite 仍在同一份檔案裡,這裡只留你要核對的指令與套件。版本號以產生器當下寫入的為準,名稱要對得上。
{ |
頁面本體只有一個起點。腳本的 type="module" 讓這個檔可以用 import 讀別的檔,路徑以斜線開頭,表示這個專案的根目錄。
<body> |
入口盯著 #root,再把 App 畫進去。外面包的 <StrictMode> 是開發時的檢查。為什麼範本一開始就打開、它在查什麼、怎麼關掉,以及為什麼課堂不關,見後面的「createRoot」。下面依課堂排版補了分號,結構與產生器給的檔相同。
import { StrictMode } from 'react'; |
設定檔裡要有 React 插件,瀏覽器才讀得懂 JSX。defineConfig 是 Vite 提供的設定包裝函式,從 vite 匯入。傳進去的物件就是這份 Vite 設定,下面的例子放的是 plugins。
import react from '@vitejs/plugin-react'; |
包裝函式把設定包起來再匯出
多數情況直接寫 export default { … } 也能跑。包一層主要是寫法統一,並讓編輯器較好給提示。
範本的 App.jsx 比較長,畫面上會是標誌和一顆計數按鈕。整份換成下面這樣,等一下才確定字是從這個檔來的。改完記得存檔。
export default function App() { |
Tailwind 設定留在課堂專案
sandbox 這一步只要跑起來,並對上 eslint.config.js 與 lint 指令。正式練習的完整設定在 03-react,sandbox 先認得產生器給的 ESLint 骨架。想在 sandbox 順便看到工具類別,可以照上一階段把 Tailwind 的 Vite 插件掛上,標題改成 className="text-3xl font-bold text-teal-700",做完不必留著。
步驟 4:啟動開發伺服器
在 sandbox 目錄啟動開發伺服器,終端機會印出 Local 網址,多半是 http://localhost:5173/。用 Chrome 打開,應看到「React 有跑起來」。
pnpm dev # 日常開發;終端機印出的 Local 多半是 http://localhost:5173/ |
改 App.jsx 再存檔,畫面會跟著換。這叫熱更新(Hot Module Replacement,HMR):開發伺服器換上你改過的那一塊。看完在終端機按 Ctrl+C 停掉。要產出上線用的檔案再執行:
pnpm build # 輸出到 sandbox/dist/,是打包後的檔,不是正在編輯的 src/ |
建置結果放在輸出資料夾。之後日常開發仍用開發伺服器,要交靜態檔時才再跑建置。
改用 03-react 練習環境
試做專案確認三件事即可:選的是 React、看懂 main.jsx 與 App.jsx、pnpm dev 看得到自己寫的標題。後面每一則示範都在課堂專案。Tailwind 插件、入口 CSS、ESLint 與 Prettier 已經接好。正式練習一律進入 03-react。
在 VS Code 新開一個終端機(它會開在倉庫根目錄),再進入專案、安裝、啟動。若目前終端機還停在試做資料夾,先回到根目錄。sandbox 的開發伺服器若還開著,先在那個終端機按 Ctrl+C。
cd .. # 若還停在 sandbox,先回到倉庫根目錄 |
專案已經在手上03-react 是倉庫裡準備好的資料夾,用產生器重建會蓋掉課堂檔。Node、pnpm 與這個資料夾都已經能用時,終端機進入 03-react,執行 pnpm install 與 pnpm dev 即可。
同樣用終端機印出的 Local 網址開 Chrome。應看到一排示範按鈕,主區是編號 01。建置會把這個專案輸出到它自己的成品資料夾。套件清單要求的 Node 至少是 24;若安裝時抱怨版本,回到上一階段確認版本號。
# pnpm build 輸出到 03-react/dist/ |
依賴已經寫在 package.json,pnpm install 會裝上。React 與 React DOM 是 19,兩邊都在清單裡。Vite 是 8,@vitejs/plugin-react 是 6,JSX 由這個插件轉換。Tailwind 是 4,插件與入口 CSS 都接好了。
# dependencies:react、react-dom(19) |
Vite 設定同時留下 React 與 Tailwind 兩個插件。樣式從入口 CSS 引入。
import tailwindcss from '@tailwindcss/vite'; |
@import 'tailwindcss'; |
入口仍是 index.html 的 #root 與 src/main.jsx,和 sandbox 同一條路。課堂的 App.jsx 用一份清單切換後面的示範,按按鈕就換內容,網址不會變。掛載在下一節拆開看,檢查設定與那份清單在工具鏈。
元件與掛載
上一章把 React 放在用函式描述畫面的位置。進到 03-react 之後,你交給它的就是這樣一個函式:回傳這一塊現在的長相。狀態變了,React 再呼叫這個函式,用新的回傳換掉畫面上的那一塊。
- 元件是回傳畫面的函式。一塊標題、一列清單、一張表單,都可以各是一個元件。
- 渲染(render)是 React 呼叫元件、算出這一次的元素。提交(commit)才把差異寫進頁面上的節點。
函式元件
元件就是一個函式。名稱用大寫開頭,回傳你要的元素。React 呼叫它,把回傳的那棵元素畫進頁面。
觀念
函式名稱要大寫開頭,React 才把它當元件。回傳的標籤寫在函式裡,看起來像 HTML,實際上是 JavaScript 裡的 JSX。最外層只有一個父元素,裡面可以再放標題與段落。
直接寫 class 會跟 JavaScript 的類別撞名,所以外觀類別寫成 className。這一則只描述畫面,還沒有按鈕,也還沒有會變的資料。
export default function Hello() { |
畫面:
你好 |
解析
export default把這個函式交給別的檔使用。一個檔案通常交出一個主要元件。- 回傳外面的括號,是為了讓標籤可以分成多行寫。
- 父層是
section。裡面兩個標籤是它的子元素,所以這一次回傳仍是一棵樹。 - 標籤必須成對,或像輸入框那樣自己結束。少了結尾,轉換會失敗。
以前的寫法
class 元件用 this.state 記資料。現在的文件與這門課都只寫函式元件。
換你試試:
標題改成「你好,React」。下面那句留著,父層仍是同一個 section。
參考答案
回傳的文字改了,函式再跑時畫面就會換成新的標題。這一則還沒有按鈕,所以存檔後看畫面即可。
export default function Hello() { |
createRoot
瀏覽器先載入一份幾乎是空的 HTML。React 從指定的那個節點開始畫。開發時外面包一層嚴格模式,讓有問題的寫法更早出現。
觀念
HTML 只留一個空的 div 當起點。createRoot 盯著這個節點,render 把元件畫進去。畫面上的內容仍是元件的回傳。
Vite 產生 React 專案時,會在 main.jsx 用 <StrictMode> 包住 <App />。sandbox 與 03-react 都是這份寫法,所以專案一開始嚴格模式就是開的。<StrictMode> 是 React 的嚴格模式(React Strict Mode),不是多出來的 HTML 標籤。
它只在開發時多做檢查,用來提早抓到不純的渲染:同樣的狀態應算出同樣的畫面,元件函式在這一步不該改到外面的資料。effect 的收尾檢查會在下一篇 React 進階:Effect 與共用狀態 看到。<StrictMode> 不會在畫面上多包一層看得到的外框。正式建置不會對使用者再跑那些額外檢查。
開發時,元件第一次掛上畫面,React 可能把元件函式再呼叫一次。兩次都應算出同一份畫面。元件函式裡若有起始的 console.log,同一行會印兩次。那是檢查在跑,使用者並沒有點兩下。後面的「批次更新」會看到這個現象:第一次載入時起始那一行可能連續出現兩次,按下按鈕之後新的那一行只有一次。
要關掉,就拿掉 main.jsx 裡的 <StrictMode>,留下 createRoot(document.getElementById('root')).render(<App />)。課堂與 03-react 保持打開。關掉之後 Console 比較安靜,嚴格模式本來要露出的問題也一起被藏起來。
正式建置不會多一層畫面pnpm build 做出來的成品不會因為 Strict Mode 多給使用者一層畫面。開發時多跑是刻意的檢查。
|
import { StrictMode } from 'react'; |
畫面的最外層是 App。打開開發者工具,#root 裡面會出現元件產生的標籤。
解析
index.html沒有正文內容。正文由 React 畫進#root。- 腳本的
type="module"讓這個檔可以用import讀別的檔。 createRoot的引數必須是頁面上真的有的那個節點。找不到就無法畫。- Vite 的 React 範本在
main.jsx用<StrictMode>包住<App />。sandbox 與03-react都這樣寫,所以嚴格模式一開始就是開的。 <StrictMode>只在開發時檢查不純的渲染。它不會變成畫面上多出來的一層,正式建置也不會對使用者重跑那些檢查。- 開發時第一次掛上,元件函式可能再跑一次,兩次都應算出同一份畫面。起始的
console.log印兩次是這項檢查,使用者並沒有點兩下。 - 要關掉就拿掉
<StrictMode>,改成createRoot(...).render(<App />)。課堂與03-react保持打開。關掉只會讓 Console 變安靜,問題仍留在程式裡。
換你試試:
把 HTML 的標題改成「元件練習」。#root 與腳本那行留著。
參考答案
分頁標題來自 HTML 的 title,跟元件裡的標題是兩件事。
// ... |
工具鏈
03-react 裡 Vite 負責開發伺服器與打包,Tailwind 負責外觀的工具類別,ESLint 看 React 的規則,Prettier 只排版。四件事都寫在專案裡,不要寫進個人的使用者設定。這一節打開檢查、排版,以及最外層怎麼切換示範。
ESLint
ESLint 看程式有沒有違反 React 的規則。Prettier 只負責空格、引號與分號。兩件事分開,避免同一行被兩套工具各改一次。你在 03-react 執行 pnpm lint,跑的就是這份設定。
檢查器選 ESLintpnpm create vite 問 Which linter to use 時,本課一律選 ESLint。sandbox 認的是產生器留下的骨架,03-react 的 eslint.config.js 再往下接 Prettier。同一個專案不要同時留兩套檢查器改同一支程式。
觀念
這份設定用 ESLint 的 flat config。檢查範圍是 JavaScript 與 JSX。dist 不檢查。React Hooks 的建議規則,以及 Vite 重新整理元件時要守的規則,都掛在這裡。
Prettier 的設定放在陣列最後,讓排版規則不要跟檢查規則打架。終端機用 pnpm lint 看結果。
這裡的 defineConfig 從 eslint/config 匯入。它和 Vite 的 defineConfig 同名,套件不同,是另一個函式。用途類似:把設定包起來再匯出。前面 vite.config.js 見過同名寫法,來源不同;這裡包的是 flat config,內容是陣列。
import js from '@eslint/js'; |
沒有錯誤時,指令結束、終端機不印問題清單。警告仍會印出來,但不會讓指令失敗。
解析
eslint-plugin-react-hooks會抓 Hooks 的呼叫順序。effect 的依賴若少寫,它會提出警告。- 在 effect 裡同步呼叫設定狀態,這套規則會當成錯誤。那是下一篇才會碰到的寫法。
eslint-plugin-react-refresh避免一個檔案匯出的內容讓熱更新失效。globals.browser讓 document 與 fetch 不會被當成未宣告變數。eslint-config-prettier放最後,空格與引號才不會被 ESLint 再判一次錯。
換你試試:
在專案資料夾執行 pnpm lint。起始程式應能通過。
參考答案
通過時不會列出錯誤。若某個示範少了依賴或改壞了 Hooks 的規則,同一支指令會指出檔名與行號。
pnpm lint |
Prettier
全班用同一套排版:兩個空格、要分號、JavaScript 用單引號。JSX 的屬性仍用雙引號,這是 Prettier 的預設。
觀念
設定只放專案裡的 .prettierrc。存檔時要排版,並讓 ESLint 能一併修可自動修的問題。這份編輯器設定放在專案的 .vscode,不要寫進個人的使用者設定。
規則跟專案走。不要在使用者的 settings.json 另外寫 prettier.semi 這一類鍵。
{ |
{ |
存檔後,JavaScript 字串會是單引號,語句結尾有分號。JSX 屬性仍是雙引號。
解析
singleQuote只管 JavaScript 字串。標籤屬性維持雙引號,讀起來才跟 HTML 一致。tabWidth是縮排幾個空格。這門課用 2。- 存檔修 ESLint 用的是專案設定。別人 clone 同一份專案,會套到同一套。
- 排版問題不要寫進 ESLint 規則。空格與引號只交給 Prettier。
換你試試:
在任一隻示範檔故意把字串改成雙引號,存檔。存完應回到單引號,分號還在。
參考答案
Prettier 會依 .prettierrc 把 JavaScript 字串改回單引號。不要為了這一點去改 ESLint。
export default function ExprDemo() { |
示範清單
切換清單記著現在要看哪一個示範。狀態存的是編號,按鈕把編號換成另一個。網址不會變,換頁留到 React Router。
觀念
每一筆有編號、標題與元件。找到對上的那一筆之後,把元件放到變數 View,再用標籤把它畫出來。變數名稱要大寫開頭,React 才會把它當元件。
這一頁的狀態只負責切換。各示範自己的狀態在切走之後會卸掉,再點回來會重新開始。
import { useState } from 'react'; |
打開頁面時,主區是編號 01 的示範。按下別的編號,主區換成那一則,網址仍停在原處。
解析
- 起始編號是 01,所以一打開是第一則。
find找不到時用第一筆,避免編號對不上就整頁空白。- 按鈕的
key用編號。標題可以改,身份仍是那個編號。 - 大寫的
View才會被當成元件。小寫開頭會被當成普通標籤。
換你試試:
把起始編號改成 06。重新整理後應先看到表達式那個示範。
參考答案
只改起始值。清單與按鈕不用動。
// ... |
重新整理後,主區是花括號表達式那一則。
單向資料流
資料由父層持有,用屬性傳給子層。子層不要改父層交下來的那一份,要改就請父層換一份新的。這樣你永遠追得回來:畫面不對,就從擁有那份資料的元件看起。
- 單向資料流是父層把現在的值傳下去。子層用得到,但不能把那份物件直接改掉。
- 要往上通知時,父層再傳一個函式下去。子層呼叫它,真正的更新仍發生在父層。
- 狀態放在「用得到它的那些元件」的共同父層。只服務一顆元件的值,就留在那一顆。
Thinking in React 是官方建議的拆法:先照畫面把元件界線畫出來,用固定資料把靜態畫面組好,再決定哪幾個值會變、該放在哪一層。這一節先做前半。會變的值用到的 useState,在狀態那一章才寫。
由父層傳入
子層的參數就是父層這一次傳下來的值。同一份標題傳給兩張卡片,兩張看到的是同一個字,子層裡沒有自己的副本可以偷偷改。
觀念
Card 只負責把收到的標題畫出來。DownDemo 擁有那個標題,用屬性交下去。卡片裡沒有設定函式,所以它改不了父層的常數。
這就是單向。之後狀態放進父層時,方向仍然一樣,只是值會在父層被換成新的。
function Card({ title }) { |
畫面是上下兩張卡片,標題都是「台北城市漫遊」。
解析
- 兩次
<Card title={title} />各是一次渲染。它們讀到的是父層同一個常數。 - 子層的參數
title是這一次的值。在子層寫title = '花蓮山海三日'不會改到父層,也不會讓另一張卡片跟著變。 - 卡片沒有自己的資料來源。標題不對時,回到
DownDemo看那個常數。 - 屬性名稱是你自己取的。子層的參數名稱要跟父層寫的屬性對上。
換你試試:
父層的常數改成「花蓮山海三日」。兩張卡片的標題都要跟著變,卡片元件裡不要寫死文字。
參考答案
文字只改父層。子層仍只讀 title,所以兩張一起換。
// ... |
拆分元件
先不要管資料會不會變。把畫面裡重複的區塊收成元件,固定的字先寫死或用常數傳下去。界線清楚之後,再決定狀態放哪。
觀念
這一頁還是同一張畫面:標題「洛奇旅行」、一句說明、兩張卡片。先把整頁寫在同一個函式裡,再抽出標題、說明與卡片,父層只負責把它們組合回去。
這一步對應 Thinking in React 的前兩步:畫出元件階層,以及用固定資料組出靜態版本。按鈕與刪除留到狀態那一章。
// TODO: 講義「拆分元件」。先看拆分前;講義有拆分後。換你試試:在拆分後的檔案再排一張卡片,名稱是京都紅葉散步、價格是 42800。標題與說明那兩個元件留著。 |
畫面與拆分後相同:上方是標題「洛奇旅行」與一句說明,下面兩張卡片分別是台北城市漫遊 12800、花蓮山海三日 15900。
小技巧:先評估再拆
這一則是為了示範怎麼把畫面拆成元件。實務上不要為了拆而拆。先看哪些區塊值得獨立出來:重複出現、可以單獨命名、之後會單獨改動的,再收成元件。
拆完之後,標題、說明、卡片各自是一個函式,父層的回傳只負責把它們排回同一個 section。
// TODO: 講義「拆分元件」。換你試試:再排一張卡片,名稱是京都紅葉散步、價格是 42800。標題與說明那兩個元件留著。 |
解析
- 拆分前,整頁都在同一個
return裡。卡片外框寫了兩次,要改其中一塊就得在這一段標籤裡找,也不容易拿到別處再用。 - 拆分後,
PageTitle與PageLead沒有參數,因為標題與說明在這一頁是固定的。Card接收title與price,因為外框相同、兩筆資料不同。 - 父層的回傳可以看出頁面結構。要改卡片去
Card,要改標題去PageTitle,各改各的地方。 - 小元件寫在同一個檔、而且沒有
export,代表只有這一頁用得到。下一章才把它們移到別的檔。 - 這一則是為了把拆分步驟看清楚。上面那則提醒仍然適用:先判斷哪些區塊值得拆,再收成元件。
換你試試:
再排一張卡片,名稱是京都紅葉散步、價格是 42800。標題與說明那兩個元件留著。
參考答案
新卡片仍由父層決定位置與內容。卡片元件本身不用改。
// ... |
狀態的位置
會變的值放在「所有用得到它的元件」上面那一層。只有一顆元件看得到的值,不要抬到更外面。誰都不看的值,不要放進狀態。
觀念
價格是否顯示,兩張卡片都要跟著走,所以這個布林值屬於父層。父層再把現在的布林傳下去。卡片只負責決定這一次要不要畫出價格,不自己保管開關。
能從現有資料算出來的字,也不要另存一份。名稱已經在,就不要再存一個「大寫名稱」。
function Card({ title, price, showPrice }) { |
showPrice 目前是常數 true,所以兩張卡片都看得到價格。
把它改成 false 時,兩張的價格一起消失。真正的按鈕留到 useState。
解析
- 開關若寫在
Card裡面,每一張卡片會各持有一份,無法一次關掉全部。 - 放在父層時,兩張讀到的是同一次傳下去的值。
- 卡片用三元決定要畫價格還是什麼都不畫。
null不會在畫面上留下節點。 - 價格的數字已經是參數。不要再為了顯示而複製一份到別的變數裡當狀態。
換你試試:
把 showPrice 改成 false。兩張卡片都只留名稱,卡片元件的參數不要刪。
參考答案
父層的值一改,兩張卡片收到的屬性一起改。這就是「狀態該放在共同父層」的原因,即使這一則還沒有寫 useState。
// ... |
React Developer Tools
React Developer Tools 是瀏覽器擴充功能。它讓你點畫面上的元件,直接看這一輪的 props 與 state,而不用在程式裡臨時加 console.log。
觀念
到 Chrome 或 Firefox 的擴充功能商店安裝 React Developer Tools。裝好後重新整理開發中的頁面,開發者工具會多出 Components 分頁。
在 Components 裡點選一顆元件,右側會列出它收到的 props,以及它自己的 state 與 hooks。這一則還沒有 state,所以你會先看到 props。下一章的計數元件,則可以在同一個分頁看 count 怎麼變。
function Card({ title }) { |
打開示範「由父層傳入」。Components 樹裡會看到 DownDemo,下面兩顆 Card。
點任一顆 Card,props 的 title 是台北城市漫遊。
解析
- Components 分頁看的是 React 的元件樹,不是 HTML 的標籤樹。標籤樹仍在 Elements。
- 點子層時,右側的 props 就是父層這一次傳下來的值。
- 畫面上有兩顆
Card,樹上也有兩顆。它們的 props 可以相同,仍是兩個元件。 - 正式建置若沒有開發用的除錯資訊,這個分頁可能對不到你的函式名稱。開發伺服器底下看即可。
換你試試:
在 Components 點 DownDemo。
確認它沒有 props,再點一顆 Card,讀出 title。
參考答案
DownDemo 是這一頁的根,沒有人傳 props 給它。
Card 的 title 來自父層的常數,所以右側會看到台北城市漫遊。
元件樹由上到下是 DownDemo,然後兩顆 Card。
不要在 Elements 裡找這個名稱,HTML 上只會有 section 與 article。
JSX
JSX 讓你在 JavaScript 裡寫元素。它不是字串,也不是另開一個 HTML 檔。這一章看一棵樹怎麼回傳、花括號能放什麼、樣式物件怎麼寫,以及條件怎麼決定畫哪一塊。
JSX 語法
標籤要有結尾,或寫成自閉。花括號裡放的是值,不是一整段敘述。樣式若要寫在元素上,交給一個物件,屬性名稱用駝峰。
- 回傳只能有一個父層。多個兄弟用 Fragment,或包進
section。 - 花括號裡放表達式。變數、運算、函式呼叫都可以。
if與for要寫在回傳外面。 - 類別寫
className。style的值是物件,鍵用駝峰,例如backgroundColor。
Fragment
兩個兄弟要一起回傳、又不想多一個父標籤時,用 Fragment。簡寫是空標籤。需要 key 時才寫出 Fragment 這個名稱。
觀念
空標籤 <>...</> 是 Fragment 的簡寫。它會消失在 HTML 裡,裡面的標題與段落直接變成上一層的子節點。
列表裡的每一項如果要回傳兩個元素又要有 key,簡寫沒有地方放 key。那時改用 Fragment。
這一則先用簡寫。
export default function FragmentDemo() { |
畫面是一行標題、一行說明。用開發者工具看 #root 裡這一層,標題與段落的外面不會多一個空的包裹元素。
解析
- 簡寫包住的兩個標籤算一次回傳。拿掉簡寫、讓它們並列,轉換會失敗。
- Fragment 本身不會出現在 Elements。你看不到一個叫做 Fragment 的標籤。
- 要在列表裡給這一組兄弟一個 key 時,寫
<Fragment key={id}>,並從 react 引入 Fragment。 - 大多數版面仍然用
section或article當父層。只有「多這一層沒有意義」時才用 Fragment。
換你試試:
在段落下面再加一個段落,文字是「仍然沒有外框」。簡寫留著。
參考答案
三個兄弟都放在同一個 Fragment 裡,回傳還是只有一個根。
// ... |
表達式
花括號裡放會算出一個值的式子。if 是敘述,不能寫在花括號裡;要在 return 之前先算好,再把結果插進畫面。依條件整塊換畫面的寫法,留到後面的條件渲染。
觀念
元件函式裡先準備名稱、單價與人數。花括號只接受表達式。人數要換成畫面上的說法時,在 return 之前用 if 決定 peopleLabel,回傳時只把這個字串放進花括號。總價寫 {price * people},乘法會先算出數字,再變成文字節點。
export default function ExprDemo() { |
畫面:
台北城市漫遊 |
不要這樣寫
花括號裡只能放表達式。{ if (people === 2) { ... } } 把整段 if 敘述塞進標籤中間,不是合法的 JSX,不要照這樣抄。這一則是在 return 之前用 if 決定 peopleLabel,畫面上只寫 {peopleLabel}。
解析
{name}的結果是字串,所以標題就是台北城市漫遊。{price * people}先做乘法。人數是 2 時,花括號裡看到的是結果 25600,不是算式本身。{peopleLabel}裡只有字串。這個字串在return之前用if決定,人數是 2 時是「兩人」。- 兩個花括號中間的空白是固定文字。固定文字與表達式可以接在同一段。
- 物件直接放進花括號當子節點,畫面不會變成你期望的文字。物件要先取出欄位。
換你試試:
人數改成 3。標題仍是名稱。應顯示多人 38400。
參考答案
只改 people。同一個 if 會把標籤改成「多人」,總價仍用 {price * people} 算出來。標題仍是 {name}。
// ... |
台北城市漫遊 |
style 物件
JSX 的屬性名稱沿用 DOM 的屬性,不是每一個 HTML 屬性都照抄。要直接寫行內樣式時,值是一個物件。
觀念
htmlFor 對應 HTML 的 for,因為 for 在 JavaScript 裡另有用途。
readOnly 中間的 O 要大寫。
style 接受物件。CSS 的 background-color 寫成 backgroundColor。
物件可以先放在常數,再交給 style。Tailwind 的 className 仍是字串。兩者可以同時在,這一則為了看物件,才把顏色寫進行內樣式。
export default function StyleDemo() { |
畫面是一個白底、有內距的區塊。標籤寫著名稱,輸入框裡是台北城市漫遊,而且不能打字。
解析
style={panel}的花括號代表「這裡是 JavaScript 的值」。裡面那個物件不是字串。- 樣式的鍵是駝峰。寫成
'background-color'不會套到你要的顏色。 htmlFor必須跟輸入框的id相同,點標籤才會聚焦到那一格。readOnly是屬性,不是子元素。它讓這一格只顯示、不接受輸入。
換你試試:
把內距改成 20px,輸入框的值改成花蓮山海三日。
htmlFor 與 id 仍要相同。
參考答案
樣式物件換一個欄位,輸入框換 value。標籤與輸入框的對應不要拆開。
export default function StyleDemo() { |
className
外觀類別是一個字串。條件只決定這個字串的內容,標籤本身不必拆成兩個 return。
觀念
active 為真時用深色按鈕,為假時用淺色按鈕。三元算出的是字串,放進 className。Tailwind 的工具類別用空白接在同一個字串裡。
不要把兩種按鈕寫成兩個元件,除非它們的結構真的不同。這裡只差外觀。
export default function ClassDemo() { |
畫面第一行是「使用中」,按鈕是白字深底。
解析
className的值是字串。三元的兩個分支都要是字串,不能一邊是字串、一邊是元件。- 工具類別之間留一個空白。少了空白,Tailwind 會把它看成一個不存在的類別。
- 段落的三元決定文字,按鈕的三元決定外觀。兩件事可以各算各的。
- 類別很多時,先放進常數再綁到屬性上,回傳的標籤比較短。
換你試試:
把 active 改成 false。第一行應變成「已停用」,按鈕改成淺底。
參考答案
同一個布林同時餵給文字與類別。改一處,兩處一起換。
export default function ClassDemo() { |
條件渲染
同一顆元件可以依資料回傳不同的元素。提早 return 與三元決定整塊或二選一。短路運算子則在樹裡多畫一塊、補預設字,或把數字 0 留在畫面上。
- 提早 return 在函式前半就結束。適合整塊畫面完全不同的情況,例如還沒有資料。
- 三元運算子在兩個元素之間挑一個。兩個分支都寫在附近,讀的人看得到另一種長相。
&&在條件為真時才多畫一塊。左邊要是布林;數字 0 直接放在左邊會被畫出來。||在左邊缺內容時補預設字,0 也會被換成右邊。要留下 0、只補null與undefined時用??。
graph LR
checkNode["先看條件"] --> earlyNode["整顆提早 return"]
checkNode --> ternNode["三元挑一個元素"]
checkNode --> logicNode["短路:多畫、補字、留 0"]
style checkNode fill:#fff3e0
style earlyNode fill:#e3f2fd
style ternNode fill:#e3f2fd
style logicNode fill:#e8f5e9
提早 return
條件成立時先回傳另一棵樹,後面的主要畫面就不會再執行。讀程式的人可以先把例外看完,再看正常的那一塊。
觀念
沒有選到行程時,函式立刻回傳一句說明。有行程時才回傳卡片。兩個 return 不會同時發生。
這種寫法適合「整頁都不一樣」。只是少一個小標記、或只補一句預設字時,用後面的三元與邏輯運算子就夠了,不必把整顆元件分成兩大段。
export default function EarlyDemo() { |
畫面只有一行「尚未選擇行程。」。卡片那一段這一次不會跑到。
解析
if寫在return之前,不寫在 JSX 的花括號裡。- 第一個
return執行後,函式結束。下面的卡片不會被算出來。 - product 是
null時去讀product.title會丟出錯誤。
提早 return 就是在那一行之前擋下來。
- 主要畫面仍是一個父元素。例外畫面這一則只有一個段落,也符合一次回傳一棵樹。
換你試試:
把 product 改成一筆真的資料,名稱台北城市漫遊、價格 12800。畫面應變成卡片,說明那一行消失。
參考答案
條件不再成立,函式會走到第二個 return。卡片用的是物件的欄位。
export default function EarlyDemo() { |
三元運算子
兩個互斥的元素要留在同一段 JSX 裡時,用三元。條件為真取問號後面那個,為假取冒號後面那個。
觀念
庫存大於 0 就顯示「有庫存」,否則顯示「補貨中」。兩個段落都寫在附近,之後改文案不用翻到函式的另一端。
三元的兩個分支都應該是可畫的值。一邊是元素、另一邊卻是一段沒有回傳的敘述,語法不會過。只在成立時多畫一塊,或左邊缺內容要補預設字,用下一節的短路寫法。
export default function TernaryDemo() { |
畫面標題是台北城市漫遊,下面一行是「有庫存」。
解析
- 條件是
stock > 0。3 通過,所以畫出第一個段落。 - 冒號後面的段落這一次不會出現在 HTML 裡,但它留在程式裡當另一種結果。
- 三元本身放在花括號裡,因為它是表達式,會算出一個元素。
- 分支一多就會很難讀。超過兩個結果時,改在
return前用if先決定要回傳哪一個變數。
換你試試:
把庫存改成 0。標題留著,下面應變成「補貨中」。
參考答案
條件變成假,三元改走冒號後面的元素。
export default function TernaryDemo() { |
邏輯運算子與短路求值
第 06 天 JavaScript 基礎:值、函式、陣列與模組 已經看過短路與空值合併:&& 左邊為假就交出左邊,|| 左邊為假才改看右邊,?? 只在 null 或 undefined 時才改用右邊。這裡看同一套規則出現在 JSX 裡:條件成立才多畫一塊、缺內容時補預設字,以及怎樣避開把 0 畫出來或把 0 吃掉。
數字 0 同時是 falsy,也是 React 願意畫上畫面的值。左邊放什麼,決定你看到的是元素、預設字,還是一個不該出現的 0。
&& 運算子
條件成立時才多一塊元素,不成立時那一格什麼都不留,用 &&。左邊要是布林或比較,結果為真才畫出右邊。
觀念
有折扣時才顯示那一行。另外用布林 hasTag 決定要不要多一個標籤。兩個條件各自短路,標題與價格始終都在。
不要用 && 在兩個很長的畫面之間二選一。那種情況用上面的三元或提早 return。左邊也不要直接放件數或金額,0 的問題在下一節。
export default function AndDemo() { |
畫面有四行:台北城市漫遊、價格 12800、折扣 10、本週推薦。
解析
discount > 0為真,所以折扣那一段會被畫出來。hasTag本身是布林。為真時,標籤段落出現;布林不會變成畫面上的文字。- 比較或布林為假時,這一格沒有元素。不要再寫一個
null在後面,那是三元的工作。 - 左邊若改成數字,短路交出的就是那個數字。0 會被畫出來,下一節專門看這個特例。
左邊用布林
好的寫法是比較或布林。discount > 0 與 hasTag 都屬於這一類。
換你試試:
把 hasTag 改成 false。標籤那一行消失,折扣、標題與價格留著。
參考答案
布林是 false,&& 短路,標籤段落不會出現。折扣那一行的左邊仍是比較,而且結果為真,所以留著。
export default function AndDemo() { |
左邊是 0 的陷阱
上一節的 && 在左邊為假時會交出左邊。左邊如果是數字 0,React 會把那個 0 畫出來。這一節就是剛才那個短路的特例。
觀念
件數是 0 時,count && <p>...</p> 的結果就是 0,不是「什麼都不畫」。畫面上會多一個你沒打算顯示的 0。
要表達「大於 0 才顯示」,就把左邊寫成比較。或改用三元,假的那一邊給 null。
export default function ZeroDemo() { |
畫面標題下面會出現一個 0,不會出現「人數 0」那個段落。
台北城市漫遊 |
解析
&&在左邊為假時,整個表達式的值就是左邊。0 為假,所以值是 0。- React 允許數字當子節點,因此 0 被畫出來。
false與null則不會。 - 這不是數學錯,是假值與可渲染的值重疊了。
- 寫成
count > 0 && ...之後,左邊變成布林,0 就不會再漏到畫面上。
件數、長度、金額
這三種常常是 0。拿它們直接當 && 的左邊之前,先改成比較。
換你試試:
左邊改成 count > 0。畫面應只剩標題,那個 0 消失。
參考答案
比較的結果是 false。false 不會被畫成文字,右邊的段落也不會出現。
// ... |
|| 運算子
字串是空的、或值是別的 falsy 時,用 || 補上一句預設字。左邊已經有內容,就留下左邊,右邊不執行。
觀念
地點是空字串時,畫面上改顯示「未定地點」。備註已經寫了「含早餐」,就繼續用這句,不會換成「尚無備註」。兩種結果都是文字,段落本身都還在。
這和 && 的方向相反。&& 是左邊成立才採用右邊;|| 是左邊缺了才改看右邊。
export default function OrDemo() { |
畫面有三行。地點補上預設字,備註留下已有的內容。
台北城市漫遊 |
解析
place是空字串。空字串是 falsy,||交出右邊,所以地點那一行是「未定地點」。note已有「含早餐」。||不再看「尚無備註」,備註那一行就是含早餐。- 畫出來的是字串,不是條件本身。兩行都會有段落,差在花括號裡的文字。
- 數字 0 也是 falsy。用
||補「未定」會把 0 吃掉。要留下 0 時用下一節的??。
和 06 同一條規則
空字串、數字 0、false、null、undefined、NaN 都會讓 || 改走右邊。這裡只是把結果放進花括號。
0 會被吃掉
金額或件數是 0 時,|| 也會換成預設字。要留下 0,用下一節的 ??。
換你試試:
把 place 改成「花蓮」。地點那一行應變成花蓮,備註仍是含早餐。
參考答案
「花蓮」有內容,|| 交出左邊,不再用「未定地點」。備註本來就有字,所以那一行不變。
export default function OrDemo() { |
?? 運算子
0 是有效數字。空值合併只在左邊是 null 或 undefined 時才改用右邊,所以 0 會留在畫面上。
觀念
同一筆價格是 0。用 || 的那一行會變成「未定」,用 ?? 的那一行仍是 0。兩行放在一起,就是為了對照哪一種運算子把 0 當成沒有值。
空字串也一樣:|| 會換成預設字,?? 會留下空字串。這一則先把對照看在數字 0 上。
export default function NullishDemo() { |
畫面標題下面兩行不一樣。邏輯或把 0 換成「未定」,空值合併留下 0。
台北城市漫遊 |
解析
- 0 是 falsy,
||改用右邊,第一行是「未定」。金額 0 被吃掉了。 ??只在null或undefined時才改用右邊。0 兩種都不是,第二行留下 0。- 兩行的段落都在。差在花括號算出的文字,不是條件要不要畫出整段。
- 左邊改成
null或undefined時,兩種寫法都會落到「未定」。
金額與件數
這兩種常常是 0。補「未定」這類預設字時用 ??,不要用 ||。
換你試試:
把 price 改成 null。兩行都應變成「未定」。
參考答案
null 對兩種運算子都算空值,所以兩行都是「未定」。這和上面的 0 不同:0 只會被 || 換掉。
export default function NullishDemo() { |
元件
元件是可重複使用的函式。這一章把卡片拆到另一個檔,再用 props 把資料與函式傳進去。父層若要決定子層中間畫什麼,就把那一塊當成 children 夾在標籤裡。
模組
一個檔可以同時交出預設元件與具名元件。引入的一邊要對上交法:預設的不加花括號,具名的要加,而且名稱要一樣。
- default export 是這個檔的主角。引入時名稱可以自己取,一個檔只有一個預設匯出。
- named export 可以有好幾個。引入時用花括號,名稱必須跟交出時相同。
default export
預設匯出給切換清單這樣的呼叫端使用。呼叫端寫 <CardDemo />,不必自己準備名稱與價格。
觀念
這個檔的主角是 CardDemo。它在內部使用 Card,並傳入固定的標題與價格。
Card 另外用具名匯出,下一節的頁面才接得走。
只看這一則時,畫面就是一張卡片。預設匯出負責把元件變成「打開就能看」的示範。
export function Card({ title, price }) { |
畫面是一張白底卡片,標題台北城市漫遊,下面一行 12800。
解析
export default這一行決定別的檔在不加快括號時會拿到誰。- 同一個檔還有
export function Card。那是具名匯出,不是第二個預設匯出。 - 一個檔不能有兩個
export default。第二個會讓轉換失敗。 CardDemo把資料寫死,是為了單獨打開這個檔也能看到卡片。真正的列表會改由父層傳不同的資料。
換你試試:
預設匯出裡的價格改成 15900,名稱改成花蓮山海三日。Card 的參數不要改成固定文字。
參考答案
改的是呼叫 Card 時傳下去的值。卡片元件仍舊只負責顯示參數。
// ... |
named export
具名匯出要在引入時把名字放進花括號。名字寫錯,或忘了花括號,這個檔就接不到 Card。
觀念
BoardDemo 從卡片檔引入 Card,排兩張不同的卡片。它自己用預設匯出,因為切換清單要的是這一頁,不是卡片。
路徑寫成 ./15-card.jsx。這個點代表同一個資料夾。不要用一整條從磁碟根目錄開始的路徑。
import { Card } from './15-card.jsx'; |
畫面是上下兩張卡片:台北城市漫遊 12800、花蓮山海三日 15900。外框與標題樣式來自 Card,這一頁只決定有幾張、各是什麼。
解析
- 花括號裡的
Card必須跟15-card.jsx的具名匯出相同。 - 寫成
import Card from './15-card.jsx'會拿到預設的CardDemo,不是卡片本身。 - 兩次使用
Card各傳各的 props。元件函式會各跑一次。 - 頁面元件用
export default,跟卡片的具名匯出是兩種交法,可以同時存在於不同檔。
換你試試:
再加一張卡片,名稱京都紅葉散步、價格 42800。引入那一行留著。
參考答案
新的資料仍由這一頁決定。卡片檔不用為了第三張而修改。
// ... |
props
props 是父層傳給子層的參數。子層用解構把要的欄位取出來,沒傳的欄位可以給預設值。父層也可以把函式當參數傳下去,或把標籤中間的內容當成 children。
- 解構寫在參數的位置,欄位名稱要跟父層的屬性一樣。
- 預設值在沒有傳那個屬性時補上。父層有傳,就算傳的是空字串,也不會改用預設值。
- 函式型 props 讓子層在某個時機通知父層。子層不擁有那份狀態。
- children 是標籤中間的內容。子層決定放在版面的哪裡,不決定內容是什麼。
解構
參數寫成 { title, price } 時,函式裡面可以直接用這兩個名字。不解構的話,它們會待在同一個物件上。
觀念
父層寫 <Product title="台北城市漫遊" price={12800} />。子層的參數就是 { title: '台北城市漫遊', price: 12800 }。
解構之後,title 與 price 是兩個區域變數。
屬性是字串時用雙引號。屬性是數字時用花括號。花括號裡的 12800 是數字,不是文字「12800」。
function Product({ title, price }) { |
畫面是一張卡片,標題台北城市漫遊,下面一行 12800。
解析
- 解構的名字跟屬性名一一對上。父層寫
price,子層才能取出price。 price={12800}的花括號表示這是 JavaScript 的值。寫成price="12800"會變成字串。- 子層不要改
title再指望父層跟著變。這一次的參數只活在這一次渲染。 - 多個屬性可以只解構用得到的那些。用不到的不必全部列出。
換你試試:
再傳一個屬性 unit,值是「天」。卡片在價格後面顯示這個單位。
參考答案
新欄位要在父層傳、在子層的解構裡接。只寫其中一邊,另一邊會是 undefined。
function Product({ name, price, unit }) { |
預設值
解構時可以在欄位後面給預設值。父層省略那個屬性時,子層仍有東西可畫。
觀念
單位常常是「天」,所以預設寫在參數上。這一則父層沒有傳 unit,畫面仍會出現天。價格沒有預設,因為每條行程都不該偷偷變成 0。
預設值只在值是 undefined 時介入。父層若明確傳了空字串,畫面上就是空的,不會被預設值蓋掉。
function Product({ title, price, unit = '天' }) { |
畫面是「台北城市漫遊」,下一行是「12800 天」。
解析
unit = '天'寫在解構裡。這是參數的預設值,不是狀態。- 父層沒有寫
unit這個屬性,所以子層用天。 - 預設值每次渲染會重新看一次。它不會被記到下一次。
- 不要為了預設值去改父層傳下來的物件。預設發生在參數這一層。
換你試試:
呼叫時加上 unit="人"。畫面應變成 12800 人,預設的天這一次不會出現。
參考答案
父層傳了值,預設值就不會介入。參數上的 '天' 留著,給下一次沒傳的時候用。
// ... |
函式型 props
父層把函式傳下來,子層在點擊時呼叫它。被點到的是哪一筆,由子層把名字交回去,狀態仍留在父層。
觀念
Shelf 擁有「目前選到誰」。
Choice 只知道自己的名稱,以及一個叫做 onPick 的函式。
按下時它呼叫 onPick(title),不自己保存選取結果。
屬性名稱用 on 開頭,表示「發生這件事時」。子層裡對應的參數也沿用這個名字。真正更新狀態的那一行只出現在父層。
import { useState } from 'react'; |
一開始是「選到 尚未選」。按下花蓮山海三日之後,變成「選到 花蓮山海三日」。台北城市漫遊那一顆會把文字改成台北城市漫遊。
解析
onPick={setPicked}把設定函式交下去。子層拿到的是函式本身,不是呼叫結果。- 箭頭函式
() => onPick(name)等到點擊才跑。寫成onClick={onPick(name)}會在渲染時就呼叫。 - 兩顆按鈕共用同一個
onPick。差在各自閉包裡的title。 - 父層是唯一呼叫
setPicked的地方。子層沒有自己的useState。
換你試試:
再加一顆選項,名稱是京都紅葉散步。按下之後,上面那一行應變成「選到 京都紅葉散步」。
參考答案
新按鈕仍把同一個設定函式傳下去,只是名稱不同。
// ... |
children
標籤中間的內容會變成子層的 children。版面元件負責外框,不負責中間的句子是什麼。
觀念
Panel 畫一個有邊框的區塊,中間放 {children}。
父層把標題與段落寫在 <Panel> 與 </Panel> 之間。
下次要放一張表單,仍用同一個外框,不必改 Panel。
children 這個名字是 React 定的。不要另外取一個 content,除非你真的想用屬性而不是標籤中間。
function Panel({ children }) { |
畫面是一個白底邊框,裡面有標題「洛奇旅行」與一句說明。
解析
- 開始標籤與結束標籤中間的元素,就是這一次的
children。 Panel不知道中間是標題還是表單。它只決定外框與內距。- 沒有寫中間內容時,
children是undefined,那一塊就是空的。 - 要限制只能放一種子元件時,再另外用明確的 props。一般的外框用
children就夠。
換你試試:
在段落下面再放一行「第二段仍在外框裡」。Panel 的實作不要改。
參考答案
多出來的段落仍是 children 的一部分。外框元件會照單全收。
// ... |
狀態
會變、而且變了就要重畫的資料,放進狀態。React 在渲染時讀到的是這一輪的快照。設定函式把下一輪要的值排進去,提交之後畫面才換成新的。
useState
useState 讓元件記住會變的資料。呼叫寫成這樣:
const [value, setValue] = useState(initialValue); |
回傳值是一個有兩個元素的陣列:現在的值,以及設定函式。我們用解構把這兩個名字拆開來用。
名字成對。管次數就寫 count 與 setCount。讀的人看得出來第二個是用來改第一個的。
initialValue 只在這顆元件實例第一次掛上時使用。同一個實例之後的渲染,不會再用起始值蓋掉現在的數字。
要讓下一輪渲染看到新的值,呼叫設定函式。指派給 value 只會碰到這一次渲染的常數,畫面不會因此重畫。
更深的規則先記方向,完整的原因在後面各節:
- 渲染讀到的狀態是這一輪的快照。同一個事件裡呼叫設定函式之後,手上的
count仍是這一輪的值。見「狀態快照」。 - 下一輪要從前一個狀態算出來時,用更新函式。見「更新函式」。
- 同一個事件裡的多次設定會批次處理,通常只再渲染一次。見「批次更新」。
useState寫在元件最上面,每次渲染的呼叫順序要一樣。見「Hooks 規則」。
graph LR
clickNode["事件呼叫設定函式"] --> queueNode["更新排進佇列"]
queueNode --> renderNode["渲染:用快照算出元素"]
renderNode --> commitNode["提交:寫進畫面"]
style clickNode fill:#e3f2fd
style queueNode fill:#fff3e0
style renderNode fill:#fff3e0
style commitNode fill:#e8f5e9
React Compiler
同一輪裡重複的計算,以及子元件要不要重畫,這門課交給 React Compiler。不另外寫 memo、useMemo 或 useCallback。
宣告狀態
useState 寫在元件函式的最上面。這一則用 count 與 setCount 管次數。
觀念
計數從 0 開始。按下按鈕時呼叫 setCount,下一輪的次數才會加一。畫面上的「次數」讀的是 count。你沒有自己去找那個段落再改文字。
設定函式的名字用 set 接在狀態名後面。這是慣例,React 不會因為你換了名字就壞掉,但全班這樣寫最好認。
不要這樣寫
指派給 count 更新不到 React 記住的狀態。const 解出來的 count 也不能被指派,那一行會丟出錯誤。改成 let 之後,改到的仍只是這個函式裡的變數,畫面不會重畫。
const [count, setCount] = useState(0); |
要換下一輪的數字,呼叫 setCount。下面這一則的按鈕就是這樣寫的。
import { useState } from 'react'; |
一開始畫面是「次數 0」。按三次之後是「次數 3」。
解析
- 起始值
0只在這顆元件被放上畫面的第一次用到。之後按按鈕不會再把次數清回 0。 (value) => value + 1用的是排到這一次的狀態。這一則每次點擊只排一個更新,寫setCount(count + 1)也會加一。下一輪要從前一個值算出來時,優先用更新函式。同一個事件裡排兩次的差別,在「狀態快照」與「更新函式」。- 換成已知常數時直接交那個值。歸零寫
setCount(0)。 - 設定成同一個數字時,React 可以略過這次渲染。畫面上的次數不會無故閃動。
換你試試:
再加一顆按鈕,按下去把次數設回 0。加一那顆留著。
參考答案
歸零的下一輪是已知常數 0,直接交給 setCount。兩顆按鈕各管自己的設定。
// ... |
render 與 commit
一次更新分成渲染與提交。渲染只呼叫你的元件、算出元素。提交才把跟上一輪不同的地方寫進頁面。這一節沿用同一支 03-react/src/demos/21-state.jsx,程式仍是上一則的計數按鈕。
觀念
按下「加一」之後,順序是:
- 事件呼叫
setCount,更新排進佇列。 - 渲染:React 重新呼叫
CountDemo。這一次讀到的count已是新數字,回傳的段落也是新的描述。這一步還在 JavaScript 裡,頁面上的文字仍是舊的。 - 提交:比對這一輪與上一輪的元素,只把有差的節點寫進頁面。段落從「次數 0」變成「次數 1」。按鈕文字沒改,那一顆按鈕可以留在原處。
官方文件用 render 與 commit 稱呼這兩步。渲染是純的計算:同樣的狀態應算出同樣的元素。渲染過程中不要改狀態,也不要在元件函式裡直接改畫面上的 DOM。
解析
- 渲染發生在 JavaScript 裡。這時候頁面上的文字還是舊的。
- 提交比對這一次與上一次的元素,只更新有差的節點。
- 元件函式裡不要寫「順便改某個 DOM」。那會跟提交打架。
- 開發時的嚴格模式可能讓渲染多跑一次。多跑的那次仍應算出同一棵元素。
換你試試:
按加一三次。確認每一次都是渲染讀到新的 count 之後,畫面才變成 1、2、3。
參考答案
每一次點擊都排進一個「加一」。React 渲染時讀到的是加上去之後的快照,提交才把段落換成那個數字。三次點擊就是三次這樣的循環,所以停在 3。
程式維持原樣即可。若數字一次跳 3,代表一個事件裡呼叫了三次設定,而不是按了三下。
多個狀態
互不相關的值分成多個 useState。名稱與件數會各自改,拆開各管各的。呼叫 Hooks 的順序每次渲染都要固定。
觀念
先寫名稱,再寫件數。改名稱時只呼叫 setName,件數留在原來的數字。每一次渲染都維持這個順序。不要把其中一個 useState 放進 if 或迴圈;呼叫次數一變,狀態就會對錯格。那種寫法在「Hooks 規則」看完整的原因。
不要這樣寫
名稱與件數會分開改。塞進同一個物件只是少寫一個 useState。改名稱時仍要展開整包,把沒改的欄位再帶回去。
const [trip, setTrip] = useState({ |
會分開改的,拆成兩個 useState。兩個值永遠一起換,才合成一個物件。
import { useState } from 'react'; |
一開始是「台北城市漫遊 × 1」。按加一人變成「台北城市漫遊 × 2」。再按改成花蓮山海三日,變成「花蓮山海三日 × 2」。件數不會因為改名而回到 1。
解析
- 兩個
useState是兩份狀態。更新其中一份,另一份用上一輪的值留到這一輪。 - 改名那顆按鈕沒有碰到
setCount,所以件數保持不變。 - 起始值各自只算一次。件數的
1不會在改名時重跑。 - 先
name、後count,每次渲染都要這個順序。「Hooks 規則」會看把useState放進if或迴圈時,順序為什麼對不上。 - 兩個值永遠一起換時,可以合成一個物件。會分開改的,維持上面兩個
useState。
換你試試:
再加一個狀態 city,起始值台北,以及一顆按鈕把它改成台南。名稱與件數那兩行更新留著。
參考答案
第三個 Hook 仍寫在元件最上面。按鈕只呼叫它自己的設定函式。
// ... |
狀態快照
這一則是錯法示範,跟下一則「更新函式」對著看。按鈕寫「以為加二」,事件裡卻連叫兩次 setCount(count + 1)。兩次用的是同一個快照,畫面只加一。
觀念
setCount(count + 1) 用的是這一次渲染關進事件函式裡的 count。同一個事件裡呼叫兩次,兩次讀到的都是按下當下的那個數字。畫面是 0 時,兩次都在排「設成 1」,後一次把前一次蓋掉。
console.log(count) 印的也是這個快照。設定函式不會把同一個函式裡的 count 當場改掉。
一個事件裡只更新一次、而且下一輪就是「這一輪的 count 加一」時,setCount(count + 1) 可以用。下一輪是已知常數時,直接交那個值,例如 setCount(0)。
同一個事件裡要疊加多次,或下一個值必須接在前一個更新之後,改用 setCount((value) => value + 1)。更新函式拿到的是佇列裡排到它為止的值。下一輪只要依賴前一個狀態,就優先用更新函式;直接交值,用在你已經知道要換成哪個常數的時候。下一則就是兩個更新函式,按一次才會加二。
import { useState } from 'react'; |
從 0 按一次「以為加二」,畫面是「快照次數 1」。Console 印出按下當下的快照:
0 |
解析
count是這一次渲染的常數。事件函式把它關在閉包裡,兩次setCount(count + 1)都讀這個數字。- 兩次設定排的是同一個下一輪的值,後一次蓋掉前一次。這是同一次點擊裡的兩次設定,畫面因此只加 1。
- 只更新一次時,
setCount(count + 1)與setCount((value) => value + 1)都會變成這一輪再加一。要在同一個事件裡疊加,才看得出更新函式的差別。 console.log(count)發生在設定之後,印出的仍是按下之前的數字。下一輪渲染才會有新的count,新的事件函式也跟著那一輪一起建立。
換你試試:
在 console.log 後面再印一次 count + 1。畫面仍是「快照次數 1」,Console 第二行應是 1。
參考答案
count + 1 只是把快照拿來做算術,不會多排一次更新。畫面仍由那兩次相同的設定決定,停在「快照次數 1」。
// ... |
0 |
更新函式
這一則是正確累加,對照上一則的快照。兩個 (value) => value + 1 會接在佇列上:第一個把 0 變成 1,第二個接到 1 再變成 2。按鈕寫「正確加二」。
觀念
setCount((value) => value + 1) 的 value 是排到這一次為止的狀態,不是事件函式閉包裡的 count。所以同一個事件裡的兩次加一會疊上去。事件函式裡的 console.log(count) 仍印按下當下的 0,畫面要等下一輪渲染才是 2。
規則與上一則同一套。下一輪要從前一個狀態算出來,用更新函式。下一個值是已知常數,直接交那個值,例如 setCount(0)。一個事件裡只做一次「目前畫面上的數字加一」時,setCount(count + 1) 也可以;會連續排、或下一個值依賴前一個值時,用更新函式。切換布林、依據上一筆陣列產生新陣列,也是依賴前一個狀態,同樣用更新函式。
import { useState } from 'react'; |
從 0 按一次「正確加二」,畫面是「累加次數 2」。再按一次,從 2 加到 4。Console 仍印出 0,那一行讀的是按下當下的快照,不是更新函式的參數。
0 |
解析
- 更新函式的參數是佇列裡最新的那一個。第一個拿到 0 交出 1,第二個拿到 1 交出 2。
- 上一則兩次
setCount(count + 1)都排成 1,後一次蓋掉前一次。這一則兩次加一會疊加。 - 一個事件裡只有一次、而且就是目前畫面上的數字加一時,
setCount(count + 1)也可以。會連續排、或下一個值依賴前一個值時,用更新函式。已知常數直接交值。 console.log(count)沒有讀更新函式的參數,所以仍是按下之前的 0。下一輪渲染的count才是 2。
換你試試:
再排第三個更新函式,同樣加一。按一次,畫面應從「累加次數 0」變成「累加次數 3」。Console 仍是 0。
參考答案
三個更新函式依序接到 0、1、2,最後排定的值是 3。畫面是「累加次數 3」。
// ... |
0 |
批次更新
同一個事件裡的多次設定,React 會收成一次渲染。「多個狀態」是兩顆按鈕各改一份。這一則是同一次點擊裡設定名稱與件數,下一輪一起出現。
觀念
名稱與件數在同一個點擊裡各設定一次。元件函式裡的 console.log 會在渲染時印出兩個新值。按一次,只多一輪渲染。你不會先看到名稱變了、件數還是舊的那一輪。
兩份狀態仍然拆開。批次是 React 在做,不必為了少畫一次就把不相關的欄位塞進同一個物件。
開發時,嚴格模式可能在第一次載入印出兩次相同的起始渲染。那是檢查,不是點了兩下。按下按鈕之後,應只再增加一次。
import { useState } from 'react'; |
按下之後畫面是「花蓮山海三日 × 3」。
渲染 台北城市漫遊 1 |
開發模式第一次載入時,第一行可能連續出現兩次。按下按鈕後,新的那一行只有一次。
解析
- 兩次設定發生在同一個事件裡,所以下一次渲染同時看得到新名稱與新件數。
- 渲染時的
console.log印的是這一輪的快照,不是按鈕函式裡的區域變數。 - 不要為了批次而把兩個不相關的值硬併成一個狀態。批次是 React 做的。
- 嚴格模式多跑的那次渲染應算出同樣的畫面。若印出的兩次起始值不同,表示渲染過程改到了外部資料。
換你試試:
按鈕裡把件數改成 5,名稱改成京都紅葉散步。按下後 Console 應多一行「渲染 京都紅葉散步 5」,而且只有這一次新增。
參考答案
同一個事件裡的兩個新值會一起出現在下一輪渲染。
// ... |
渲染 京都紅葉散步 5 |
Hooks 規則
Hooks 只在元件的最上層呼叫,自訂 Hook 也一樣。每一次渲染,useState 的呼叫次數與順序都要一樣,React 才對得回上一輪的狀態。條件寫在 JSX,決定畫出哪一句。
觀念
少呼叫一次、或多呼叫一次,後面的狀態就會對到前面的格子。ESLint 的 react-hooks/rules-of-hooks 會把下面兩種寫法報成錯誤。練習檔 26-rules.jsx 只放正確寫法,這兩段不要接進 App.jsx。
不要這樣寫if 成立時才呼叫 useState。下一次渲染若跳過這個 if,少了一次呼叫,後面的 useState 會往前對到上一輪的第一個狀態。
function StockDemo({ soldOut }) { |
for 或 map 裡呼叫 useState 也一樣。筆數一變,Hook 的次數就變,每一筆對不回上一次的同一個狀態。
function ListDemo({ rows }) { |
rows.map((row) => { const [on, setOn] = useState(false); }) 同樣是在迴圈裡產生 Hook。要幾個狀態,就在元件最上面寫幾個。
下面這份把兩個 Hook 放在任何條件之前。有沒有庫存,只影響回傳的那一行字。
import { useState } from 'react'; |
一開始是「台北城市漫遊 有庫存」。按下設為缺貨,變成「台北城市漫遊 補貨中」。名稱那顆按鈕仍改得到文字。
解析
- 兩個 Hook 都在
return之前,也不在條件或迴圈裡。每次渲染都會各呼叫一次,順序固定,狀態才對得回同一個格子。 - 庫存判斷寫在 JSX 裡。
stock > 0只選句子,不決定useState有沒有被呼叫。 - 上面警告裡的
if與for/map會讓呼叫次數隨資料變。ESLint 的react-hooks/rules-of-hooks會擋下這些寫法。 - 普通函式不是元件。名稱不是大寫開頭時,不要在裡面呼叫 Hooks。
換你試試:
把 setStock(0) 改成更新函式,讓庫存減 1。按兩次應變成補貨中。Hook 仍留在最上面。
參考答案
減一要用上一輪的庫存,所以用更新函式。條件仍只負責選句子。
// ... |
從 2 按一次還是有庫存,再按一次庫存是 0,句子變成補貨中。
不可變更新
狀態裡的陣列與物件不能直接改內容。push、改某個欄位,都不會穩定地觸發下一輪渲染。要複製一份,在新的那份上留下變更,再交出去。
- 新增時把舊元素展開到新陣列,再接上新的一筆。
- 刪除時用
filter留下不是那一筆的元素。 - 修改時用
map找到那一筆,展開後蓋掉要改的欄位,其他筆原樣放回。 - 巢狀物件要每一層都換新的。只展開最外層時,裡面的物件仍是原來的那一份。
陣列新增
備忘從兩則開始。按按鈕時產生新陣列,長度比原來多一。原來的陣列沒有被 push。
觀念
新陣列先把現有項目攤開,再接上新字串。新字串帶上長度,避免跟舊的文字重複。清單用文字當 key 只是因為這一則的文字不會重複。會刪、會改的清單要改用 id,列表那一節會專門看。
import { useState } from 'react'; |
一開始是台北城市漫遊、花蓮山海三日。按一次多出「備忘3」。
解析
- 展開會複製元素到新陣列。舊陣列的內容不會被這一行改掉。
- 更新函式的
items是排到這一次的陣列。連續點擊也不會用到過期的長度。 notes.push(...)改的是原陣列。狀態的參考沒換,畫面常常不會更新。sort也會改到呼叫它的那一份。要排序時先複製再排。
不要改原陣列
新增、刪除、排序都交一份新陣列。原陣列留著,這一輪的其他程式才不會看到做到一半的資料。
換你試試:
再加一顆按鈕,按下去把備忘變成空陣列。加一則的按鈕留著。
參考答案
空清單是新的陣列 [],不是把原陣列的長度改成 0。清空後再加,序號會從 1 重新數。
// ... |
陣列刪除
刪除是留下其他筆,不是把那一格挖空。filter 回傳的是新陣列,對不上 id 的那一筆不會在裡面。
觀念
每一列有自己的刪除按鈕。按下時把那一筆的 id 交給更新函式。filter 的條件是「id 不相同才留下」。台北城市漫遊被刪掉之後,花蓮山海三日與京都紅葉散步還在,而且順序不變。
import { useState } from 'react'; |
一開始三列。按下花蓮山海三日的刪除之後,只剩台北城市漫遊與京都紅葉散步。
解析
filter不會改原陣列。它交出一份較短的新陣列。- 條件用 id,不用名稱。以後兩筆都叫台北城市漫遊時,才不會一次刪掉兩列。
- 箭頭函式把這一列的 id 傳進
onRemove。不要寫成onClick={onRemove(item.id)}。 - 刪到最後一筆時,
map得到空陣列,清單就是空的,不需要另外把<ul>拿掉。
換你試試:
刪除改成只留被點到的那一筆,其他筆都拿掉。按花蓮山海三日之後,清單只剩花蓮山海三日。
參考答案
條件改成 id 相同才留下。這仍是新陣列,不是把其他元素設成空的。
// ... |
陣列修改
修改某一筆時,那一筆要換成新物件,其他筆可以沿用原來的參考。map 負責走訪,展開負責複製欄位。
觀念
按下「花蓮改價」時,只有 id 是 hualien 的那一筆換成價格 16800 的新物件。台北城市漫遊與京都紅葉散步仍是原來的物件。畫面三列都還在,花蓮山海三日的數字變了。
不要寫 item.price = 16800。那是在改狀態裡的原物件,下一輪渲染可能看不出有什麼需要更新。
import { useState } from 'react'; |
按下之後,中間一列從「花蓮山海三日 15900」變成「花蓮山海三日 16800」。另外兩列不變。
解析
map的長度與原陣列相同。這一則不是在新增或刪除。- 對到的那一筆用展開複製,再把
price蓋掉。標題仍是花蓮山海三日。 - 沒對到的筆直接放回原來的物件。不必每一筆都複製。
- 三元的兩個分支都要是物件。一邊回傳物件、一邊什麼都不回傳,那一格會變成
undefined。
換你試試:
同一顆按鈕把花蓮山海三日的標題也改成「花蓮山海五日」。價格仍是 16800。
參考答案
兩個新欄位都寫在展開後面。舊的標題與價格都會被覆蓋。
// ... |
按下之後是「花蓮山海五日 16800」。
物件欄位
改物件的一個欄位時,展開舊物件,再寫上新欄位。只交一個新欄位的物件,其他欄位會消失。
觀念
人物有名稱與城市。改城市時,名稱要留在新物件裡。展開放前面、新欄位放後面,同名時才會留下新的城市。
這是巢狀更新的前一步。只有一層時,展開一次就夠。
import { useState } from 'react'; |
一開始是「阿光 在 台北」。按下之後變成「阿光 在 台中」。
解析
profile.city = '台中'改的是原物件。不要用這種寫法更新狀態。- 展開在前、新欄位在後。順序對調就會被舊的城市蓋回去。
- 名稱沒有寫在後面,所以仍是阿光。沒被覆蓋的欄位會保留。
- 更新函式的
current是上一輪的人物。不要改用渲染時的profile再展開,連續更新才不會蓋掉彼此。
換你試試:
同一顆按鈕除了改城市,也把名稱改成「阿明」。
參考答案
兩個新欄位都寫在展開後面。舊的名稱與城市都會被覆蓋。
// ... |
按下之後是「阿明 在 台中」。
巢狀物件
物件裡還有物件時,要改的那一層與它的每一層父物件都得換成新的。只展開最外層,裡面的地址仍是原來的參考。
觀念
人物有名稱,也有一個地址物件。改城市時,先展開人物,再展開地址,最後蓋掉城市。名稱與路名都留在新物件裡。
只寫 { ...profile, city: '台南' } 會把城市放在人物這一層,地址裡的城市仍是舊的,畫面若讀的是 profile.address.city 就不會變。
import { useState } from 'react'; |
一開始是「阿光 在 台北 中山路」。按下之後,城市換成台南,路名仍是中山路。
解析
- 外層展開留下
name。沒有這層展開,名稱會消失。 address被一整個換成新物件。新物件再展開舊地址,所以路名還在。current.address.city = '台南'改的是原物件。不要用這種寫法更新狀態。- 更深的巢狀就繼續一層一個展開。不要為了省事把整包狀態變成可直接改的外部變數。
換你試試:
同一顆按鈕把路名也改成「民生路」。城市仍是台南,名稱仍是阿光。
參考答案
路名與城市都寫在地址那一層的展開後面。人物的名稱沒有被覆蓋。
// ... |
按下之後是「阿光 在 台南 民生路」。
事件
使用者的點擊、輸入與送出,都經由事件處理函式進入元件。屬性用 on 接事件名,處理函式用 handle 接動作。需要額外參數時,用箭頭函式包一層,不要在渲染時就把函式呼叫掉。
事件處理函式
處理函式在事件發生時才跑。寫進 onClick 的是函式本身。React 會把事件物件交給它。命名分成屬性與函式兩邊,讀的人看得出誰在等事件、誰在做事。
- 屬性叫
onClick、onChange、onSubmit。
on 後面的事件名用大寫開頭。
- 函式叫
handleClick、handleChange、handleSubmit。
同一個動作在子層的參數也可以沿用 onClick 這種名字。
命名慣例
onClick 是 JSX 的屬性。handleClick 是你寫的函式。兩邊不要對調,也不要在屬性上立刻呼叫函式。
觀念
按下按鈕時,handleClick 把文字改成「已記錄」。屬性寫 onClick={handleClick}。
若寫成 onClick={handleClick()},渲染到這一行就會執行,而且交給 React 的是回傳值,不是函式。
按鈕要寫 type="button"。放在表單裡時,預設的 submit 會順便送出表單。
import { useState } from 'react'; |
還沒點時,按鈕下面是「尚未點」。點一次之後變成「已記錄」。
解析
handleClick沒有在渲染時被呼叫。畫面先畫完,等使用者按下才跑。- 屬性名稱是
onClick,不是 HTML 字串'onclick'。JSX 裡這是一個函式屬性。 - 設定狀態發生在處理函式裡。下一輪渲染才讀得到新的
label。 - 一個屬性綁一個函式。要先做兩件事,把兩件事寫進同一個
handleClick。
換你試試:
再加一顆按鈕,屬性是 onClick,處理函式叫 handleReset,按下把文字設回「尚未點」。
參考答案
新的處理函式仍是「把函式交給屬性」。不要在 onClick 後面加上呼叫括號。
// ... |
事件物件
React 呼叫處理函式時會傳入事件物件。要讀「被點到的那一個元素」時用 currentTarget。它是你綁定監聽的那個節點。
觀念
handleClick 把按鈕上的文字放進狀態,並在 Console 印出事件種類。currentTarget 不會因為你點到按鈕裡的子元素就換成別的節點。這一則的按鈕沒有子標籤,文字就是按鈕自己的內容。
import { useState } from 'react'; |
點一次之後,那段文字變成「記錄這顆按鈕」。
click |
解析
- 參數
event由 React 傳入。你不用在onClick後面自己寫括號把事件塞進去。 event.type在點擊時是click。這可以用來確認處理函式真的有跑。currentTarget是綁定onClick的按鈕。
target 可能是按鈕裡面更小的節點。
- 渲染的當下不會印出 click。一定要等使用者按下。
換你試試:
Console 除了事件種類,再印出按鈕文字。畫面的那段文字維持不變。
參考答案
文字已經在設定狀態時讀過一次。再印一次同一個來源,Console 就會多一行按鈕文字。
// ... |
click |
參數與預設動作
有時處理函式需要按鈕以外的資料,例如這一列的 id。瀏覽器對表單還有預設動作:按下送出會整頁重新載入。兩件事都在處理函式裡處理。
- 要額外參數時,用箭頭函式包住呼叫,讓渲染時只建立函式、不執行它。
preventDefault取消這一次事件的預設動作。表單就不會整頁重載,網址也不會被瀏覽器改掉。
帶參數
每一顆按鈕對應一個名稱。處理函式收名稱,屬性上用箭頭函式把那個名稱傳進去。
觀念
handlePick 只負責把收到的名稱放進狀態。兩顆按鈕各寫 onClick={() => handlePick('台北城市漫遊')}。
渲染時箭頭函式被建立,但 handlePick 還沒跑。點下去才把字串交出去。
不要寫 onClick={handlePick('台北城市漫遊')}。
那一行在渲染時就會挑好台北城市漫遊,而且 onClick 拿到的是 undefined。
import { useState } from 'react'; |
按下花蓮山海三日之後,上面一行是「選到 花蓮山海三日」。
解析
- 箭頭函式的參數是事件物件,這一則用不到,所以留空。真正要的名稱寫在箭頭函式裡面。
handlePick可以重用。差在每一顆按鈕交出去的字串。- 渲染時 Console 不該出現選取結果。出現的話,就是在屬性上呼叫了函式。
- 名稱若來自陣列,把那一筆的欄位放進箭頭函式,不要把整個事件當成名稱。
換你試試:
再加一顆按鈕,按下把選取設成京都紅葉散步。handlePick 不要複製第二份。
參考答案
第三顆按鈕呼叫同一個處理函式,只換傳進去的字串。
// ... |
preventDefault
表單的送出有預設動作:瀏覽器會重新載入頁面。處理函式先取消這件事,再讀你放在狀態裡的值。
觀念
handleSubmit 收到事件物件後第一行就呼叫 preventDefault。接著把名稱寫進畫面下方的訊息。網址列不會多出問號,示範頁也不會整頁刷新回第一個按鈕。
輸入框這一則先用受控的寫法:value 與 onChange 都綁到 name。
表單那一章會把每一種欄位拆開看。
import { useState } from 'react'; |
按下送出,訊息變成「送出 台北城市漫遊」。網址沒有變,頁面沒有整個重載。
解析
onSubmit綁在<form>上,不綁在按鈕上。按 Enter 時同樣會跑到這個函式。- 沒有
preventDefault時,瀏覽器會用預設方式送出,React 這一頁的狀態會跟著重載消失。 - 按鈕的
type="submit"是為了觸發表單。它不是第二個送出邏輯。 - 訊息來自狀態。不要在送出時直接改某個段落的
textContent。
換你試試:
送出時改把訊息設成「已收到」加上名稱。仍要先取消預設動作。
參考答案
preventDefault 留在第一行。後面只換要放進狀態的字串。
// ... |
名稱是台北城市漫遊時,訊息是「已收到台北城市漫遊」。
列表與表單
清單用 map 把陣列轉成一組元素,再用 filter 先拿掉不要的筆。
每一個兄弟要有穩定的 key。表單則把欄位的值放進狀態,讓輸入與畫面讀的是同一份資料。
列表
陣列在 JavaScript 裡轉換,JSX 只負責把轉換結果放進樹裡。filter 與 map 可以串在一起:先留下符合條件的,再變成元素。
map的回傳要是元素,長度與你交出去的陣列相同。filter不改原陣列。它先決定哪些筆有資格出現。key幫助 React 認出「這一個還是原來的那一筆」。不要用會跟著插入、刪除而改變的 index。
map
三條行程各有 id。map 回傳一列項目。這一則先把每一筆都畫出來,篩選放在下一則。
觀念
tours 寫在元件外面,因為這三筆不會變。元件每次渲染都讀得到同一份陣列。
map 的回呼回傳 <li>,所以清單的子節點就是這三個元素。
const tours = [ |
畫面是三列:台北城市漫遊 12800、花蓮山海三日 15900、京都紅葉散步 42800。每一列都是白底邊框。
解析
map寫在花括號裡,因為它的結果是一組元素。- 回呼的參數
item是陣列裡的一個物件。標題與價格從欄位讀。 key寫在map直接回傳的那個元素上。寫在更裡面的標籤,React 對不到這一層兄弟。- 陣列是空的時候,
map得到空陣列,ul 裡就沒有 li。
換你試試:
陣列再加一筆,id 是 osaka,名稱大阪一日,價格 9800。前三筆留著。
參考答案
新的 id 沒有跟舊的重複,所以四列都能各自對上。
// ... |
filter
先留下價格低於 20000 的行程,再交給 map。畫面上就不會出現京都紅葉散步。條件寫在 JavaScript,不寫成「先全部畫出來再用 CSS 藏起來」。
觀念
filter 回傳新陣列,原 tours 仍是三筆。
後面的 map 只走訪留下的那些。兩個動作串在同一個花括號裡,讀的人看得到先篩再畫。
價格的條件用數字比較。不要把價格先變成字串再比大小。
const tours = [ |
畫面兩列:台北城市漫遊 12800、花蓮山海三日 15900。京都紅葉散步被留在原陣列裡,沒有進 visible。
解析
filter的條件為真才留下。京都紅葉散步的價格是 42800,所以不在新陣列裡。- 原陣列沒有被改掉。下一輪若條件放寬,京都紅葉散步還在。
visible在渲染時算出來。它不必放進狀態,因為它完全由tours與條件決定。key仍用行程 id。篩選之後花蓮山海三日還是hualien,不會因為它變成第二列就換身份。
換你試試:
條件改成價格大於 15000。畫面應變成花蓮山海三日與京都紅葉散步,台北城市漫遊消失。
參考答案
台北城市漫遊的價格是 12800,過不了新的條件。花蓮山海三日與京都紅葉散步留下。
// ... |
空清單
篩選之後可能一筆都不剩。這種情況要另外回傳一句說明,不要留下一個沒有項目、也沒有解釋的空 <ul>。
觀念
價格小於 0 的行程在這份資料裡不存在,所以 visible 是空陣列。長度是 0 時提早 return 一句話。有資料時才走 map。
空陣列是正常結果,不是錯誤。下一篇的讀取失敗會另外用狀態區分。
const tours = [ |
畫面只有一行「沒有符合的行程。」
解析
- 長度是 0 才提早 return。不要用
visible &&判斷陣列,空陣列也是真值。 - 說明與清單是兩次不同的回傳。有資料時不會同時看到那句話。
filter沒有改到原陣列。條件放寬之後,行程還在。- 這一句是給人看的空狀態。它不是讀取失敗。
換你試試:
條件改成價格小於 20000。說明應消失,改出現台北城市漫遊與花蓮山海三日。
參考答案
條件放寬後長度不再是 0,函式走到清單那個回傳。
// ... |
key
key 讓 React 把狀態對回「同一筆資料」,而不是「現在排在第幾列」。子元件自己的輸入狀態會跟著 key 走。用 index 當 key 時,刪掉前面一列,後面的文字會黏在錯誤的那一列上。
觀念
Row 自己記得輸入框的文字。父層的清單用 item.id 當 key。你在花蓮山海三日那一列打了字,再刪掉台北城市漫遊,花蓮山海三日的文字還在花蓮山海三日旁邊。
若改成 key={index},刪掉第一列之後,原本第二列的元件會被當成第一列重用,文字就黏到剩下的那一列上。身份應該跟著 id,不跟著位置。
import { useState } from 'react'; |
在花蓮山海三日的輸入框打「甲」。按下刪掉台北城市漫遊之後,畫面上只剩花蓮山海三日,輸入框裡仍是甲。
解析
key寫在 Row 上,因為 Row 才是 map 直接回傳的那個元素。- id 用資料自己的代號。重新排序後,花蓮山海三日仍是
hualien。 key不會變成 DOM 上的屬性。要顯示編號時,另外把編號寫進文字。- 用 index 當 key,刪掉第一筆時,後面每一筆的 key 都會往前移,輸入框裡的字可能跟著錯位。
換你試試:
把 key 暫時改成 index,在花蓮山海三日那一列打字再刪掉台北城市漫遊。
看完文字黏到哪一列之後,改回 item.id。
參考答案
index 會讓 React 以為「現在的第 0 列」仍是剛才的第 0 列。台北城市漫遊刪掉之後,花蓮山海三日變成第 0 列,於是沿用了台北城市漫遊那個輸入框的狀態,花蓮山海三日打的字反而留不下來。
改回 id 之後,花蓮山海三日的 key 一直是 hualien,輸入框的文字跟著花蓮山海三日走。
import { useState } from 'react'; |
表單欄位
受控欄位的值放在狀態裡。畫面讀狀態,使用者一輸入就用事件把狀態換成新的字串或布林。不同的欄位讀值的方式不一樣。
input與 textarea 用 value 配 onChange,從event.target.value讀文字。select同樣用 value 對到選項的 value,不是對到畫面上的那一行字。checkbox讀checked。
radio 用同一份狀態比對每一顆的值。
input
單行文字的值就是狀態裡的字串。每一次按鍵都換成新字串,輸入框顯示的永遠是狀態,不是瀏覽器自己記的那一份。
觀念
value={name} 讓輸入框變成受控。沒有 onChange 時,使用者打字不會更新狀態,畫面會像卡住。
event.target.value 是這一刻輸入框裡的整段文字,不是只有新按下的那一個鍵。
import { useState } from 'react'; |
還沒打字時,下面是「目前:尚未填」。打上台北城市漫遊之後,變成「目前:台北城市漫遊」。
解析
- 空字串是合法的受控值。不要用
undefined當value,那會讓輸入框在受控與非受控之間切換。 onChange在每一個輸入時發生,不只有失焦的時候。- 下面那一行讀的是同一個
name。輸入框與文字不會各記各的。 name || '尚未填'在空字串時顯示提示。0 不會出現在這個欄位,所以這裡可以用或運算。
換你試試:
起始值改成台北城市漫遊。打開頁面時輸入框裡就有台北城市漫遊,下面一行也是台北城市漫遊。
參考答案
起始值只影響第一次。之後仍由 onChange 更新。
import { useState } from 'react'; |
textarea
多行文字的寫法與單行輸入框相同,都是 value 配 onChange。不要把要顯示的文字放在開始標籤與結束標籤中間,那是 HTML 的寫法,在受控元件裡不會跟著狀態走。
觀念
備註放在 note。輸入框用 <textarea value={note} />。換行也是字串的一部分,狀態裡會保留使用者按下的換行。
import { useState } from 'react'; |
打開時文字區裡是「第一行」,下面是「字數 3」。再打一個字,字數變成 4。
解析
value放在屬性上。標籤中間不要再塞一份預設文字。rows只影響高度。它不是狀態,這一則沒有讓使用者改列數。note.length在渲染時算出來。字數不必另存一個狀態。- 清空時把狀態設成空字串。不要去呼叫文字區的 DOM 方法。
換你試試:
加一顆按鈕,按下把備註設成空字串。字數應變成 0。
參考答案
按鈕只更新 note。字數跟著下一輪渲染重算。
// ... |
select
下拉選單的 value 對到某個 <option value="...">。
畫面上的「台北」是給人看的,狀態裡存的是 taipei。
觀念
city 起始是 taipei,所以打開時選在台北。
onChange 仍從 event.target.value 讀,讀到的是選項的 value,不是標籤中間的文字。
沒有任何選項的 value 與狀態相同時,React 會在開發時提出警告。起始值要對到一個真的選項。
import { useState } from 'react'; |
打開時下面是「代碼 taipei」。改選台南之後變成 tainan。
解析
value在 select 上,不在每一個 option 上用selected。- 選項的
value用你程式裡的代碼。顯示文字可以是中文。 - 狀態換了,選單會自己跳到對應的選項。不必再找 DOM 去設
selectedIndex。 - 這一則只有兩個城市。要加第三個,就再加一個
<option>,並確定 value 不重複。
換你試試:
加一個選項,value 是 taichung,文字是台中。選了之後代碼那一行應是 taichung。
參考答案
新選項的 value 就是會進狀態的字串。顯示文字只出現在選單上。
// ... |
checkbox
核取方塊的狀態是布林。
讀 event.target.checked,綁定用 checked,不要用 value 記住有沒有勾。
觀念
gift 為真時顯示「要早餐」。
勾選時 checked 是 true,取消時是 false。
value 這一則沒有用來保存勾選,因為勾選不是一段文字。
import { useState } from 'react'; |
一開始是「不含早餐」。勾起來之後變成「要早餐」。再點一次回到不包。
解析
checked必須是布林。不要把字串'on'放進去。onChange讀checked。
讀 value 會拿到輸入框的 value 屬性,不是勾選狀態。
- 標籤包住輸入框時,點文字也會切換。
htmlFor那一則是另一種綁法。 - 畫面下方的句子由同一個布林決定。不要再存一份「要早餐」的文字狀態。
換你試試:
起始值改成 true。打開頁面時應已勾選,文字是要早餐。
參考答案
起始布林改成真。事件仍把使用者的下一次勾選寫回狀態。
import { useState } from 'react'; |
radio
同一組選項共用一個狀態。每一顆的 checked 是「狀態是否等於我的值」。點任何一顆,都把狀態設成那一顆的值。
觀念
unit 起始是 person。
第一顆的 checked 為真,第二顆為假。兩顆都叫同一個 name,瀏覽器才把它們當同一組。真正決定畫面的仍是 React 的狀態,不是瀏覽器記住的那一顆。
import { useState } from 'react'; |
打開時單位那一行是 person。點天之後變成 day,人那一顆不再勾選。
解析
- 兩顆的
checked來自同一份unit。不會兩顆同時為真。 onChange寫成設定固定的值。這一則不必讀event.target.value,因為值已經寫在那一顆上。name相同讓鍵盤與瀏覽器把它們當一組。React 的狀態才是你之後送出時要讀的那一份。- 要加第三顆時,再比對一個新的字串,並在點擊時設定那個字串。
換你試試:
加一顆「團」,值是 group。點了之後,下面一行應是 group。
參考答案
第三顆用同一個狀態。
checked 只有在 unit 等於 group 時為真。
// ... |
送出表單
多個欄位可以收在同一個物件裡,欄位的 name 對到物件的鍵。送出時讀這份物件。標籤與輸入框的 id 用 useId 產生,避免同一頁有兩份表單時 id 撞在一起。
- 物件欄位用一個
onChange依event.target.name更新對應的鍵。 handleSubmit先preventDefault,再使用這一份物件。useId每呼叫一次給一組穩定的 id。
把它接到 htmlFor 與 id。
多欄位物件
名稱與價格分屬物件的兩個鍵。一個 handleChange 服務兩個輸入框,靠輸入框的 name 決定改哪一鍵。
觀念
更新時展開舊物件,再用計算屬性蓋掉被改的那一鍵。另一鍵留著。這跟巢狀物件是同一件事,只是欄位都在第一層。
兩個輸入框都受控。價格這一則先當字串,送出前若要做算術再轉成數字。不要在每打一個字時就轉,否則空字串會變成 0,使用者會刪不掉。
import { useState } from 'react'; |
在名稱打台北城市漫遊、價格打 12800,下面一行變成「台北城市漫遊 12800」。
解析
- 輸入框的
name必須是狀態物件裡的鍵。寫錯就會多出一個你沒在畫面上讀的鍵。 [name]是計算屬性。它用變數的值當鍵,不是用字面的"name"。- 展開放在前面,新鍵放在後面,另一個欄位才不會被清掉。
- 兩個輸入框共用
handleChange。不必每個欄位各寫一個幾乎一樣的函式。
換你試試:
物件再加一個鍵 unit,起始是空字串,並加一個對應的輸入框。打「天」時,下面一行也要看得到。
參考答案
新鍵靠同一個 handleChange 更新。
輸入框的 name 寫成 unit。
import { useState } from 'react'; |
submit
送出時不要再去問 DOM 裡的輸入框。狀態物件就是要送出的那一份。先取消預設動作,再把結果寫到畫面上。
觀念
handleSubmit 用名稱與價格組成一句話。兩個輸入框的更新仍走 handleChange。
送出按鈕是 type="submit",表單綁 onSubmit。
這一則只把結果留在畫面。下一篇才把同一份物件送到網路上。
import { useState } from 'react'; |
名稱填台北城市漫遊、價格填 12800,按下送出,訊息是「台北城市漫遊 12800」。網址不變。
解析
preventDefault放在讀狀態之前。頁面沒重載,訊息才留得住。- 訊息用的是送出當下的
form。這是這一次事件的快照,夠用,因為送出只讀一次。 - 輸入過程中
message不會跟著變。它只在送出時更新。 - 不要在送出時用
document.querySelector再讀一次輸入框。狀態已經有了。
換你試試:
名稱是空的時候不要更新訊息,改把訊息設成「請填名稱」。有名稱時仍送出原本那句。
參考答案
空字串在送出時擋下來。兩個輸入框的受控寫法不用改。
// ... |
useId
useId 產生這一輪元件專用的 id。
把它接到標籤的 htmlFor 與輸入框的 id,點標籤就會聚焦到那一格。同一頁有兩份元件時,兩組 id 不會撞在一起。
觀念
每呼叫一次 useId 得到一個字串。名稱與價格各要一組,所以呼叫兩次,而且兩次都在元件最上面。不要把 useId 放進迴圈或條件。
id 的值不要自己寫死成 name。另一個元件若也寫 name,點標籤會聚焦到頁面上第一個同 id 的輸入框。
import { useId, useState } from 'react'; |
點「名稱」兩個字,焦點進到第一個輸入框。點「價格」則進到第二個。用開發者工具看,兩個 id 不會都叫 name。
解析
htmlFor必須與輸入框的id相同。只寫其中一個,點文字不會聚焦。- 兩次
useId的結果不同。不要把同一個 id 接到兩個輸入框。 useId的結果在這顆元件的生命裡是穩定的。不要把它放進狀態再改。- 價格這一格先示範標籤綁定。要讓它受控時,再加一份狀態與
value。
換你試試:
讓價格也受控,狀態放在 price,起始是空字串。
標籤的 htmlFor 仍用 priceId。
參考答案
第二個欄位補上 value 與 onChange。
id 繼續用 useId 的結果,不要改回寫死的字串。
// ... |
本篇重點回顧
- 元件是回傳元素的函式。渲染算出這一次的元素,提交才寫進畫面。
- 資料從父層用 props 往下流。子層要改資料時,呼叫父層傳下來的函式。狀態放在用得到它的共同父層。
- JSX 的花括號放表達式。整塊換畫面用提早 return 或三元。多畫一塊用
&&,左邊要是布林。缺內容用||補預設字;要留下 0 時改用空值合併??。 className寫類別,style寫駝峰物件。多個兄弟又不想多包一層時用 Fragment。- 狀態是快照。下一輪依賴前一個狀態時用更新函式;換成已知常數時直接交那個值。同一個事件裡的多次設定會收成一次渲染。
useState每次渲染的呼叫順序都要一樣。陣列與物件都交一份新的值。 - 事件屬性用
onClick這種名字。要參數就包一層箭頭函式,表單送出要preventDefault。 - 列表用
map畫出,key 用穩定的 id。表單欄位受控,多欄位可以收成一個物件,標籤 id 用useId。
專有名詞中英對照
認識 React
- 框架(framework)
- 重排(reflow)
- 重繪(repaint)
- 宣告式(declarative)
- 命令式(imperative)
- 漸進式框架(progressive framework)
- 單檔元件(Single-File Component、SFC)
- 模板(template)
- 模板指令(template directive)
- Vue 狀態管理(Pinia)
- Vue 全端框架(Nuxt)
- Vue 介面元件庫(Element Plus)
- Vue 介面元件庫(Vuetify)
- 響應式程式庫(RxJS)
- Angular 狀態管理(NgRx)
- Angular 介面元件庫(Angular Material)
- React 狀態管理(Zustand)
- React 狀態管理(Redux Toolkit)
- React 介面元件庫(MUI)
- React 介面元件庫(Ant Design)
- 函式庫(library)
- JSX
- 使用者介面(user interface)
- View
- 元件(component)
- 元件化(component-based)
- 單頁應用(Single Page Application、SPA)
- 語法糖(syntactic sugar)
建立專案
- 元件(Component)
- JSX
- 渲染(render)
- 提交(commit)
- 嚴格模式(React Strict Mode、
<StrictMode>) - 單向資料流(one-way data flow)
- Thinking in React
- props
- React 開發者工具(React Developer Tools)
- 專案範本(project template)
- 專案產生器(project generator)
- 熱更新(Hot Module Replacement、HMR)
- Vite 的 defineConfig(defineConfig,from vite)
- ESLint
- ESLint 的 defineConfig(defineConfig,from eslint/config)
JSX
- Fragment
- 條件渲染(conditional rendering)
- 表達式(expression)
- 短路求值(short-circuit evaluation)
- 邏輯且(logical AND、
&&) - 邏輯或(logical OR、
||) - 空值合併(nullish coalescing、
??)
元件
- 預設匯出(default export)
- 具名匯出(named export)
- 解構(destructuring)
- 子元素(children)
狀態
- 狀態(state)
- 快照(snapshot)
- Hook
- 更新函式(updater function)
- 批次更新(batching)
- Hooks 規則(Rules of Hooks)
事件
- 事件處理函式(event handler)
- 預設動作(default action)
列表與表單
- 鍵(key)
- 受控元件(controlled component)
- 表單(form)