Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

React 组件基础

方法论:组件 = 可复用的积木。理解 Props 和 State,就理解了 React 的 80%。

Props vs State


📖 本节目标

学完本节,你将能够:

  • ✅ 理解什么是组件
  • ✅ 掌握 Props(传递数据)
  • ✅ 掌握 State(管理状态)
  • ✅ 学会最常用的 Hooks

预计用时:25 分钟


0. 开始前必看:Vibe Coding 心法(重要!)

0.1 不要被语法吓到!

⚠️ Vibe Coder 的救命稻草!

React 的代码看起来很复杂,但你不需要精通所有语法细节。下面告诉你哪些要懂,哪些交给 AI。

你必须理解的逻辑(核心概念)

概念你要懂什么为什么重要
组件什么是组件、为什么要拆组件这是思维方式,AI 写不了
Props数据从外部传入组件理解数据流向
State组件内部有“记忆“,会变化理解交互原理
何时用 State点击、输入等需要变化的用 State判断用不用 State

AI 可以代劳的语法(细节写法)

语法例子不用背!
Props 类型定义{ text }: { text: string }让 AI 写 ✅
解构语法{ title, price, image }让 AI 写 ✅
TypeScript 接口interface ButtonProps {}让 AI 写 ✅
Hooks 具体写法const [x, setX] = useState()让 AI 写 ✅
useEffect 依赖数组useEffect(() => {}, [])让 AI 写 ✅

0.2 学习策略

┌─────────────────────────────────────────────┐
│  第1步:理解概念(你的任务)                  │
│  "组件是什么?Props 和 State 有什么区别?"    │
│                                             │
│  第2步:描述需求(你的任务)                  │
│  "我要一个计数器,点击按钮数字+1"             │
│                                             │
│  第3步:AI 生成代码(AI 的任务)              │
│  AI 会写出所有语法细节                       │
│                                             │
│  第4步:运行调试(你的任务)                  │
│  看效果,不对就告诉 AI "改成xxx"             │
└─────────────────────────────────────────────┘

Vibe Coding 核心:你负责想明白“要做什么“,AI 负责写“怎么做“。看到 { text }: { text: string } 这种复杂语法?跳过!直接问 AI


1. 什么是组件?

一句话解释

组件 = 可复用的 UI 积木

把页面拆成一个个小块,每块就是一个组件:

页面
├── Header(头部组件)
├── ProductList(产品列表组件)
│   ├── ProductCard(产品卡片组件)
│   ├── ProductCard
│   └── ProductCard
└── Footer(底部组件)

为什么要用组件?

问题组件化解决方案
代码重复写一次,到处用
难以维护改一处,全局生效
协作困难分工明确,各写各的

2. 创建组件

2.1 最简单的组件

// 一个简单的按钮组件
function MyButton() {
  return (
    <button className="bg-blue-500 text-white px-4 py-2 rounded">
      点击我
    </button>
  )
}

// 使用组件
export default function Page() {
  return (
    <div>
      <MyButton />
      <MyButton />
      <MyButton />
    </div>
  )
}

2.2 组件文件组织

推荐的文件结构:

src/
├── app/
│   └── page.tsx
└── components/          # 👈 组件都放这里
    ├── Button.tsx
    ├── Card.tsx
    ├── Header.tsx
    └── Footer.tsx

创建组件文件 src/components/Button.tsx

export default function Button() {
  return (
    <button className="bg-blue-500 text-white px-4 py-2 rounded">
      按钮
    </button>
  )
}

在页面使用

import Button from '@/components/Button'

export default function Page() {
  return (
    <div>
      <Button />
    </div>
  )
}

3. Props:传递数据

3.1 什么是 Props?

Props = 组件的参数(从外到内的数据)

类比:Props 就像快递标签📦

你(父组件)→ 快递标签(Props)→ 快递员(子组件)

<Button text="提交" color="blue" />
         ↑         ↑
       Props    Props

核心理解

  • 父组件决定:传什么数据(就像你写快递标签上的收件人、地址)
  • 子组件接收:拿到数据后显示(就像快递员按标签送货)
  • 子组件不能改:Props 是只读的(快递员不能改标签)

3.2 最简单的例子(看懂这个就够了!)

// 定义组件,接收 props
function Button({ text }: { text: string }) {
  //         ↑ 这里接收外部传入的 text
  return (
    <button className="bg-blue-500 text-white px-4 py-2 rounded">
      {text}  {/* 这里显示 text 的值 */}
    </button>
  )
}

// 使用组件,传递不同的 text
export default function Page() {
  return (
    <div className="flex gap-2">
      <Button text="提交" />  {/* 传入 "提交" */}
      <Button text="取消" />  {/* 传入 "取消" */}
      <Button text="删除" />  {/* 传入 "删除" */}
    </div>
  )
}

Vibe Coding 提示:看到 { text }: { text: string } 觉得头晕?

你只需要理解text 是从外面传进来的数据。

至于为什么写两次 text,为什么有冒号和花括号?别管!让 AI 写!

你只要知道:“我要给这个组件传个 text 数据,然后组件里能用 {text} 显示它。”


### 3.3 多个 Props(看懂就行,让 AI 写!)

```tsx
// 定义类型(这部分让 AI 写!)
interface ButtonProps {
  text: string
  color?: 'blue' | 'red' | 'green'  // 可选参数
  onClick?: () => void              // 点击事件
}

function Button({ text, color = 'blue', onClick }: ButtonProps) {
  const colors = {
    blue: 'bg-blue-500 hover:bg-blue-600',
    red: 'bg-red-500 hover:bg-red-600',
    green: 'bg-green-500 hover:bg-green-600',
  }

  return (
    <button
      className={`${colors[color]} text-white px-4 py-2 rounded`}
      onClick={onClick}
    >
      {text}
    </button>
  )
}

// 使用
<Button text="确认" color="green" onClick={() => alert('点击了')} />
<Button text="删除" color="red" />
<Button text="提交" />  {/* 默认蓝色 */}

Vibe Coding 提示

  • interface ButtonProps { ... } 这种类型定义?→ AI 写!
  • color?: 'blue' | 'red' 这种可选参数语法?→ AI 写!
  • 你只需要懂:“color 参数可以传 blue/red/green,不传就默认 blue”

3.4 实战:产品卡片组件

// src/components/ProductCard.tsx
interface ProductCardProps {
  title: string
  price: number
  image: string
  onBuy: () => void
}

export default function ProductCard({
  title,
  price,
  image,
  onBuy
}: ProductCardProps) {
  return (
    <div className="bg-white rounded-lg shadow p-4">
      <img src={image} alt={title} className="w-full h-48 object-cover rounded" />
      <h3 className="text-lg font-bold mt-2">{title}</h3>
      <p className="text-red-500 text-xl">¥{price}</p>
      <button
        onClick={onBuy}
        className="mt-2 w-full bg-blue-500 text-white py-2 rounded"
      >
        立即购买
      </button>
    </div>
  )
}

// 使用
import ProductCard from '@/components/ProductCard'

export default function Page() {
  const products = [
    { id: 1, title: 'iPhone 15', price: 5999, image: '/iphone.jpg' },
    { id: 2, title: 'MacBook Pro', price: 12999, image: '/macbook.jpg' },
  ]

  return (
    <div className="grid grid-cols-2 gap-4 p-4">
      {products.map(product => (
        <ProductCard
          key={product.id}
          title={product.title}
          price={product.price}
          image={product.image}
          onBuy={() => alert(`购买 ${product.title}`)}
        />
      ))}
    </div>
  )
}

4. State:管理状态

4.1 什么是 State?

State = 组件的“记忆“(会变化的数据)

类比:State 就像电子计分板🏀

想象一个篮球比赛的电子计分板:

┌─────────────────────┐
│   主队 vs 客队       │
│    23  :  18        │  ← 这是 State(比分会变)
│   [+1] [+2] [+3]    │  ← 按钮改变比分
└─────────────────────┘

特点:
1. 比分会变(State 会变)
2. 按钮点击后,比分自动更新显示(State 变化,页面自动刷新)
3. 每个计分板独立计数(每个组件有自己的 State)

Props vs State 的区别

PropsState
来源外部传入内部创建
能改吗❌ 不能改(只读)✅ 能改(可变)
类比快递标签(收到就是收到)电子计分板(可以加分)
用途父组件给子组件传数据组件记住自己的状态

4.2 最简单的例子:计数器

'use client'  // 👈 使用 State 必须加这行(为什么?别管,记住就行)

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)  // 初始值为 0
  //     ↑      ↑         ↑
  //   当前值  改值的函数  初始值

  return (
    <div className="p-4">
      <p className="text-2xl">计数:{count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="bg-blue-500 text-white px-4 py-2 rounded mt-2"
      >
        +1
      </button>
    </div>
  )
}

4.3 State 的“魔法“解释(必看!)

学生最常问的问题

Q1: 为什么用方括号 [count, setCount]

A: 这是 JavaScript 的“解构赋值“语法(让 AI 写就行)。

你只需要记住:useState 会给你两样东西

  • 第一个:当前的值(count
  • 第二个:改值的函数(setCount

就像电子计分板给你:当前比分 + 加分按钮

Q2: 为什么不能直接 count = count + 1

A: 这是 React 的规则!

错误写法:

count = count + 1  // ❌ React 不知道值变了,页面不会更新

正确写法:

setCount(count + 1)  // ✅ React 知道了,会自动刷新页面

通俗理解

  • 直接改 count = 偷偷改计分板的数字,观众看不到变化
  • setCount = 正式按下按钮,计分板会闪烁更新,所有人都看到了

Vibe Coding 提示:别纠结为什么!记住:改 State 必须用 setXxx 函数

4.4 useState 语法(AI 帮你写!)

const [状态值, 设置函数] = useState(初始值)

// 例子(这些语法细节让 AI 写!)
const [count, setCount] = useState(0)           // 数字
const [name, setName] = useState('')            // 字符串
const [isOpen, setIsOpen] = useState(false)     // 布尔值
const [items, setItems] = useState([])          // 数组
const [user, setUser] = useState(null)          // 对象

你只需要懂

  1. 第一个参数是当前值,第二个是改值的函数
  2. 改值必须用 setXxx 函数,不能直接赋值
  3. 具体的 TypeScript 类型标注? → AI 写!

4.5 常见场景

场景 1:输入框

'use client'
import { useState } from 'react'

export default function SearchBox() {
  const [keyword, setKeyword] = useState('')

  return (
    <div>
      <input
        type="text"
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        className="border rounded px-3 py-2"
        placeholder="搜索..."
      />
      <p>你输入的是:{keyword}</p>
    </div>
  )
}

你只需要懂:输入框的值存在 keyword 里,输入时用 setKeyword 更新。

onChange={(e) => setKeyword(e.target.value)} 这种写法? → AI 写!

场景 2:显示/隐藏

'use client'
import { useState } from 'react'

export default function Modal() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>
        打开弹窗
      </button>

      {isOpen && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
          <div className="bg-white p-6 rounded-lg">
            <h2>弹窗内容</h2>
            <button onClick={() => setIsOpen(false)}>
              关闭
            </button>
          </div>
        </div>
      )}
    </div>
  )
}

你只需要懂isOpen 控制显示/隐藏,true 显示,false 隐藏。

场景 3:列表操作

'use client'
import { useState } from 'react'

export default function TodoList() {
  const [todos, setTodos] = useState<string[]>([])
  const [input, setInput] = useState('')

  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, input])  // 添加到列表
      setInput('')                   // 清空输入
    }
  }

  return (
    <div className="p-4">
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="border rounded px-3 py-2 flex-1"
        />
        <button onClick={addTodo} className="bg-blue-500 text-white px-4 rounded">
          添加
        </button>
      </div>
      <ul className="mt-4">
        {todos.map((todo, index) => (
          <li key={index} className="py-2 border-b">{todo}</li>
        ))}
      </ul>
    </div>
  )
}

你只需要懂

  1. todos 数组存储列表数据
  2. 点击“添加“时,把 input 的值加到 todos
  3. [...todos, input] 这种展开语法? → AI 写!
  4. <string[]> 这种类型标注? → AI 写!

5. 常用 Hooks

5.1 Hooks 是什么?(必看!)

Hooks = 给组件装备的“工具包“🎒

类比:想象你是一个探险家👨‍🚀

普通组件 = 空手探险(只能走路)

加上 Hooks = 装备齐全的探险家:
├── useState    = 记忆背包(记住状态)
├── useEffect   = 自动触发器(到达某地自动执行任务)
├── useRef      = 定位器(记住位置)
└── useContext  = 对讲机(跨组件通信)

你只需要记住

  • Hooks 是函数,名字都以 use 开头
  • Hooks 给组件增加能力(记忆、副作用、引用等)
  • 常用的就两个useState(记忆)+ useEffect(副作用)

Vibe Coding 提示:Hooks 的具体写法和规则?交给 AI! 你只要知道“我需要在组件加载时获取数据“,AI 会用 useEffect 帮你写。

5.2 useEffect:副作用处理

useEffect = 自动触发器⏰

类比:想象你设置了一个闹钟

┌──────────────────────────────────┐
│  闹钟设置                          │
│                                  │
│  [X] 每天早上 7 点响              │  ← useEffect(() => {}, [])
│  [ ] 每次温度变化时响              │  ← useEffect(() => {}, [temp])
│  [ ] 一直响(不推荐😱)             │  ← useEffect(() => {})
└──────────────────────────────────┘

基本语法

'use client'
import { useState, useEffect } from 'react'

export default function UserProfile() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // 👈 这里的代码会在组件加载时执行
    fetch('/api/user')
      .then(res => res.json())
      .then(data => {
        setUser(data)
        setLoading(false)
      })
  }, [])  // 👈 空数组 = 只在首次加载时执行(闹钟只响一次)

  if (loading) return <div>加载中...</div>

  return <div>欢迎,{user?.name}</div>
}

5.3 useEffect 依赖数组详解(重要!)

学生最常问的问题

Q: 为什么 [] 空数组意味着“只执行一次“?

A: 这是 React 的规则!用闹钟理解:

useEffect(() => {
  console.log('执行了!')
}, [])  // 空数组

等价于:
"设置闹钟,不监听任何变化,所以只在首次加载时响一次"

三种常见模式

写法触发时机闹钟类比
useEffect(() => {}, [])只在组件首次加载时闹钟只响一次
useEffect(() => {}, [userId])首次加载 + 每次 userId 变化时闹钟监听 userId,它变就响
useEffect(() => {})首次加载 + 每次组件更新时闹钟一直响(危险⚠️)

示例对比

// 例子 1:只在首次加载时获取数据
useEffect(() => {
  console.log('组件加载了,获取数据')
  fetchData()
}, [])  // 👈 空数组 = 只执行一次

// 例子 2:userId 变化时重新获取
useEffect(() => {
  console.log('userId 变了:', userId)
  fetchUserData(userId)
}, [userId])  // 👈 监听 userId 变化

// 例子 3:每次渲染都执行(通常不需要)
useEffect(() => {
  console.log('组件更新了')
})  // 👈 没有数组 = 一直执行

Vibe Coding 提示

  • 想在组件加载时做事?→ useEffect(() => { ... }, [])
  • 想在某个值变化时做事?→ useEffect(() => { ... }, [那个值])
  • 具体的依赖数组规则? → 别管!让 AI 帮你写!
  • 闹钟响了要关掉(清理函数)? → AI 会加 return () => { ... },你不用管!

5.4 常见 useEffect 模式(让 AI 写!)

// 1. 只在首次加载时执行
useEffect(() => {
  console.log('组件加载了')
}, [])

// 2. 依赖变化时执行
useEffect(() => {
  console.log('userId 变化了:', userId)
}, [userId])

// 3. 清理函数(组件卸载时执行)
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行中')
  }, 1000)

  return () => clearInterval(timer)  // 👈 清理定时器(AI 会帮你写)
}, [])

6. 客户端 vs 服务端组件

Next.js 的两种组件

类型特点使用场景
服务端组件(默认)在服务器渲染,更快静态内容、数据获取
客户端组件在浏览器渲染交互、useState、事件

什么时候用 ‘use client’?

需要加 'use client' 的情况:

  • 使用 useStateuseEffect 等 Hooks
  • 使用 onClickonChange 等事件
  • 使用浏览器 API(如 windowlocalStorage
'use client'  // 👈 只有需要交互时才加

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

7. Vibe Coding 技巧

让 AI 生成组件

好的 Prompt

帮我创建一个 React 组件:
- 名称:ProductCard
- Props:title, price, image, onAddToCart
- 使用 Tailwind CSS
- 包含:图片、标题、价格、添加购物车按钮
- 点击按钮调用 onAddToCart

常见问题怎么问

# Props 问题
"怎么给组件传递点击事件?"
"怎么设置默认值?"

# State 问题
"怎么在点击后更新页面内容?"
"怎么实现显示/隐藏功能?"

# 渲染问题
"为什么我的列表不显示?"
"怎么遍历数组显示多个组件?"

📝 小结

概念说明例子
组件可复用的 UI 积木<Button />
Props外部传入的数据<Button text="提交" />
State内部管理的状态useState(0)
useEffect副作用处理加载数据

核心理解

  1. 组件让代码可复用、可维护
  2. Props 是“从外到内“传数据
  3. State 是“组件内部“的记忆
  4. 需要交互的组件加 'use client'

📚 本章完成

恭喜你完成了前端开发基础的学习!

回顾

下一步: 👉 后端开发基础

学习如何用 FastAPI 创建后端 API。