個人網頁開發
EP.04

React 核心概念
Component、JSX、Props、State

看懂我的個人網頁每一行程式碼的關鍵
用你已經看過的實際程式碼來說明

Joseph Chen

2024
5 min read
1.2k views

打開我的個人網頁任何一個 page.tsx 檔案,你會看到滿滿的這種東西:

你看到但還不懂的程式碼
export default function BlogPage() {
  const [expanded, setExpanded] = useState(false);

  return (
    <div className="bg-gray-50 min-h-screen pt-20">
      <h1 className="text-5xl font-black text-gradient">Blog</h1>
      <PostCard post={post} />
    </div>
  );
}

這篇結束後,你會看懂上面每一行在做什麼。我們從最基本的概念開始。

1. Component — 積木式 UI

React 的核心思想是:把整個頁面拆成一個個獨立的「元件(Component)」。 每個 Component 就是一個 JavaScript 函式,回傳 UI

最簡單的 Component
// 定義一個元件
function MyButton() {
  return (
    <button>點我</button>
  );
}

// 在別的地方使用它(像 HTML tag 一樣)
function App() {
  return (
    <div>
      <MyButton />   {/* 用了三次,只需要寫一次程式碼 */}
      <MyButton />
      <MyButton />
    </div>
  );
}

我的個人網頁的 Navbar 就是一個 Component,寫在 components/Navbar.tsx, 然後在 layout.tsx 裡使用一次,但每個頁面都能看到它:

layout.tsx — 使用 Navbar Component
import Navbar from '@/components/Navbar';  // 引入

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Navbar />       {/* 使用,像 HTML tag 一樣 */}
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Component 三個重要規則

• 函式名稱必須大寫開頭(MyButton、Navbar),小寫開頭的是 HTML 原生 tag(div、button)

• 每個 Component 必須回傳 單一根元素(用一個 div 或 <> 包住)

• Component 可以巢狀使用,就像積木可以組合

2. JSX — 在 JS 裡寫 HTML

你可能注意到 React 程式碼裡有奇怪的 HTML-like 語法,這叫 JSX(JavaScript XML)。 它讓你可以在 JavaScript 函式裡直接寫「像 HTML 的東西」。

💡
JSX 不是真正的 HTML。它最終會被編譯成 JavaScript。只是長得像 HTML,讓程式碼更好讀。

JSX 和 HTML 的差異

❌ HTML(不是 JSX)

<!-- HTML -->
<div class="container">
  <label for="name">Name</label>
  <img src="photo.jpg">
</div>

✅ JSX(React 用的)

// JSX
<div className="container">
  <label htmlFor="name">Name</label>
  <img src="photo.jpg" />
</div>

class → className

class 是 JavaScript 的保留字,JSX 改用 className

for → htmlFor

for 也是保留字(for 迴圈),改用 htmlFor

自閉合 tag 必須加 /

<img />、<br />、<input /> 必須有結尾斜線

JSX 裡嵌入 JavaScript — 用 {}

JSX 裡用大括號 {} 就能嵌入任何 JavaScript 表達式:

JSX 嵌入 JavaScript
function ProfileCard() {
  const name = "Joseph Chen";
  const year = 2024;
  const skills = ["Python", "React", "Next.js"];

  return (
    <div>
      <h1>{name}</h1>                          {/* 變數 */}
      <p>{'年份:' + year}</p>                   {/* 字串連接 */}
      <p>{skills.length} 個技能</p>              {/* 計算 */}
      <p>{year > 2020 ? '近期' : '早期'}</p>     {/* 三元運算子 */}
    </div>
  );
}

看一個我的網頁的實際例子,blog/page.tsx 裡的這段:

blog/page.tsx — 實際應用
// series 是一個陣列,.map() 讓每個元素都產生一張卡片
{series.map((s, i) => (
  <a key={s.id} href={`#${s.id}`}>
    <p className="text-lg font-black">{s.posts.length}</p>  {/* 顯示文章數 */}
    <p>{s.label}</p>                                        {/* 顯示系列名稱 */}
  </a>
))}
.map() 是 React 中渲染列表的標準方式。把陣列的每個元素轉換成 JSX,就是一排卡片或列表。記得每個元素要加 key 屬性(唯一值),React 用它來識別哪個元素改變了。

3. Props — 傳資料給 Component

Props(Properties)是從父元件傳給子元件的資料。就像 HTML 的屬性(srchref), Props 讓同一個 Component 能顯示不同的內容。

Props 基本概念
// 定義接收 props 的元件
function PostTitle({ title, date, author }) {   // 解構 props
  return (
    <div>
      <h2>{title}</h2>
      <p>{author} · {date}</p>
    </div>
  );
}

// 使用時傳入 props
function BlogPage() {
  return (
    <div>
      <PostTitle
        title="EP.01 — Two Sum"
        date="2024"
        author="Joseph Chen"
      />
      <PostTitle
        title="EP.02 — Set vs Dict"
        date="2024"
        author="Joseph Chen"
      />
    </div>
  );
}

我的 blog/page.tsx 裡的 EpRow 元件就是這樣運作的, 接收一個 post 物件和 color

blog/page.tsx — 實際的 Props 使用
// 定義:接收 post 和 color 兩個 props
function EpRow({ post, color }: { post: Post; color: string }) {
  return (
    <Link href={post.href}>
      <span className={color}>{post.ep}</span>
      <p>{post.title}</p>
      <p>{post.subtitle}</p>
    </Link>
  );
}

// 使用:傳入 post 和 color
{visiblePosts.map((post, i) => (
  <EpRow key={i} post={post} color={s.color} />
))}

TypeScript 的型別標注

你可能注意到 Props 後面有 : { post: Post; color: string }, 這是 TypeScript 的型別標注,告訴編輯器「post 要是 Post 類型,color 要是字串」。 如果你傳錯型別,VS Code 會立即顯示紅線提醒你。

TypeScript 型別定義
// 先定義 Post 的結構
type Post = {
  title: string;
  subtitle: string;
  date: string;
  author: string;
  href: string;
  isExternal: boolean;
  ep?: string;  // ? 代表可選(optional)
};

// 然後在 props 裡使用這個型別
function EpRow({ post, color }: { post: Post; color: string }) {
  // TypeScript 現在知道 post.title 一定存在
  // 如果你打 post.xxx(不存在的欄位),VS Code 立即報錯
}
💡
TypeScript 是「加了型別系統的 JavaScript」。主要好處是:在寫程式時就能發現 Bug,而不是等到上線才出錯。所有 .tsx 檔案都是 TypeScript + JSX。

4. useState — 管理動態狀態

State(狀態)是 Component 內部的「會改變的資料」。每次 state 改變,React 會自動重新渲染 UI。

useState 是 React 的 Hook,用法是:

useState 基本語法
import { useState } from 'react';

function Counter() {
  // [當前值, 更新函式] = useState(初始值)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>目前計數:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(0)}>重設</button>
    </div>
  );
}

我的 blog/page.tsx 裡用 useState 控制「展開/收起」的功能:

blog/page.tsx — 實際的 useState
function SeriesSection({ s }) {
  // expanded:目前是否展開(初始值 false = 收起)
  const [expanded, setExpanded] = useState(false);

  // 根據 expanded 決定要顯示幾篇
  const visiblePosts = !expanded ? s.posts.slice(0, 5) : s.posts;

  return (
    <div>
      {visiblePosts.map(...)}  {/* 顯示文章 */}

      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? '收起' : '展開全部'}  {/* 文字也會跟著變 */}
      </button>
    </div>
  );
}

State 更新的關鍵:一定要用 setter 函式

❌ 這樣不會觸發重新渲染

// 直接修改 state 變數 → React 不知道改了
expanded = true;
count = count + 1;

✅ 這樣才正確

// 用 setter 函式 → React 知道要更新 UI
setExpanded(true);
setCount(count + 1);

5. 'use client' — 哪裡執行很重要

你一定注意到幾乎每個 page.tsx 開頭都有這行:

page.tsx 頂部
'use client';  // ← 這是什麼?

Next.js 有兩種 Component:

Server Component(預設)

在伺服器執行,HTML 直接輸出給瀏覽器。不能用 useState、onClick 等互動功能。 適合靜態展示。

'use client' Component

在瀏覽器執行,可以用 useState、useEffect、onClick 等互動功能。凡是有互動的頁面都要加這行。

什麼時候要加 'use client'? 只要你的 Component 裡用到:
  • useState、useEffect 等 React Hooks
  • onClick、onChange 等事件處理器
  • framer-motion 的動畫
就要在檔案頂部加 'use client'。我的網頁幾乎每個頁面都有互動,所以都有加。

綜合範例:拆解首頁的程式碼

現在用學到的概念,完整讀懂首頁 Hero 區塊的一小段:

app/page.tsx(首頁片段)
'use client';                         // 有 framer-motion,需要在瀏覽器執行

import { Button, Link, Chip } from '@heroui/react';  // 引入元件庫的元件
import { ArrowRight } from 'lucide-react';            // 引入圖示
import { motion } from 'framer-motion';               // 引入動畫庫

export default function Home() {       // ← Component(大寫開頭的函式)
  return (                             // ← 回傳 JSX
    <div className="bg-white">         // ← className 不是 class(JSX 規則)
      <motion.div                      // ← motion.div 是帶動畫的 div
        initial={{ opacity: 0, y: 30 }}// ← Props:初始狀態(透明 + 往下偏移)
        animate={{ opacity: 1, y: 0 }} // ← Props:目標狀態(不透明 + 回到原位)
        transition={{ duration: 0.8 }} // ← Props:動畫持續 0.8      >
        <h1 className="text-7xl font-black">
          I think, therefore I am       // ← 純文字,不需要 {}
        </h1>
        <Button
          as={Link}                     // ← Props:讓 Button 渲染成 Link
          href="/resume"                // ← Props:連結目標
          color="primary"               // ← Props:HeroUI 的主題色
          endContent={<ArrowRight />}   // ← Props:傳 JSX 作為值(用 {}        >
          View Resume
        </Button>
      </motion.div>
    </div>
  );
}

這篇學到什麼

🧩Component = 大寫開頭的函式,回傳 JSX。積木式組合,寫一次到處用
📝JSX 像 HTML 但不一樣:class→className、for→htmlFor、自閉合 tag 要加 /
JSX 裡用 {} 嵌入 JS 表達式,.map() 渲染列表時記得加 key
📦Props 是從外部傳進 Component 的資料,讓同個元件顯示不同內容
🔄useState 管理會變動的狀態,改變 state 一定要用 setter,React 才會重新渲染
🖥️'use client' 讓 Component 在瀏覽器執行,有互動(Hook、事件)就要加
Web Dev
React
JSX
Props
useState
TypeScript
EP.04