跳转到内容

4.6 为什么需要前端框架:React 在替你做什么

一、本节你会做出什么

你会看懂 page.tsx 里那些“长得像 HTML 又不是 HTML”的东西,并亲手把一段重复三遍的代码变成一个组件。

📷 效果位:改造前(重复三段)vs 改造后(一个组件 + 三行调用)

这一节解释一个你可能一直在疑惑的事:4.2 学的 HTML 明明挺好,为什么真实项目里不这么写?


二、开始前你需要

  • 已完成 4.2(标签与嵌套)、4.4(数组、对象、函数、map)
  • 已完成 4.5,知道 app/page.tsx 在哪
  • hello-node 项目能跑起来(pnpm dev)

⚠️ 4.4 的 map 如果没亲手跑过,建议回去跑一遍。 本节第 6 步直接建立在它上面。


三、跟着做

第 1 步:先体会没有框架的痛

假设「遇见成都」要显示 6 个景点卡片。用 4.2 的纯 HTML 写,是这样:

<div class="card">
<img src="/panda.jpg" alt="大熊猫基地">
<h3>大熊猫繁育研究基地</h3>
<p>¥55 · 3 小时</p>
</div>
<div class="card">
<img src="/kuanzhai.jpg" alt="宽窄巷子">
<h3>宽窄巷子</h3>
<p>免费 · 2 小时</p>
</div>
<!-- ... 再复制粘贴 4 遍 ... -->

现在来三个真实需求,感受一下疼在哪:

需求 纯 HTML 的代价
「卡片加个圆角」 改 6 个地方(漏一个就不一致)
「每张卡片加一个“收藏”按钮」 改 6 个地方
「景点改成 20 个」 复制粘贴 20 遍,而且数据和样子混在一起

还有更难的。如果数据是从服务器来的呢?

页面加载时你并不知道有几个景点。你得用 4.4 的 JS 手动做这些事:

// 没有框架时,大致要这么干
const container = document.querySelector(".cards");
container.innerHTML = ""; // 先清空
attractions.forEach(item => {
const div = document.createElement("div"); // 一个个造元素
div.className = "card";
div.innerHTML = `<h3>${item.name}</h3>...`; // 手动拼字符串
container.appendChild(div); // 一个个塞进去
});

数据一变,这一整套重新来一遍。页面上有十个这样的区域,你就要维护十套这种代码,而且**很容易出现“数据改了但页面没更新”**的 bug。

🔑 这就是框架要解决的问题。 不是为了炫技,是为了让上面这些事不用你操心。

第 2 步:React 替你做的三件事

React 做什么 换成大白话
① 数据驱动界面 你只管改数据,页面自己跟着变。 不用再手动找元素、改元素
② 组件化 一段界面可以打包成一个“自定义标签”,到处复用,改一处全都变
③ 高效更新 数据变了,它自己算出“页面上哪一小块需要变”,只改那一块

第 ① 条是最核心的思想转变:

没有框架: 数据变了 → 你去找到元素 → 你手动改元素
React: 数据变了 → 页面自动变(你什么都不用做)

💡 一个类比: 没有框架像手写账本——改一个数字,你得把受影响的所有行都手动重算一遍。 React 像 Excel——你改一个单元格,所有依赖它的公式自动重算。

你只负责改数据,刷新界面是 React 的活儿。

第 3 步:JSX —— 为什么 HTML 长在 JS 里

打开 app/page.tsx,你看到的大概是这样:

export default function Home() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">遇见成都</h1>
</main>
);
}

第一反应会是:这是 HTML 还是 JS?

答案:这是 JSX —— 在 JS 里直接写标签。 它会被编译成真正的 HTML。

你要知道的 说明
它就是 4.2 学的那些标签 <div> <h1> <img> 全都一样
class 要写成 className 因为 class 在 JS 里是保留字。这是 JSX 最常见的一个差异
{} 里面是 JS <h1>{city}</h1> —— 大括号里的东西会被当代码执行,结果显示出来
标签必须闭合 <img> 要写成 <img />,4.2 里 HTML 容忍的写法这里不行
只能返回一个最外层元素 要并列多个就用 <>...</> 包起来(叫 Fragment)

{} 这个能力是关键,它让“数据”和“界面”连起来了:

const city = "成都";
const count = 6;
return (
<div>
<h1>遇见{city}</h1> {/* 显示:遇见成都 */}
<p>共 {count} 个景点</p> {/* 显示:共 6 个景点 */}
<p>{count > 5 ? "很多" : "不多"}</p> {/* 还能写判断 */}
</div>
);

🔑 看到 {} 就知道:这里的内容是算出来的,不是写死的。 这是你读 JSX 时最该注意的信号——页面上哪些部分是动态的,一眼就看出来了。

第 4 步:组件 —— 自己造标签

组件就是一个返回 JSX 的函数。 就这么简单。

function AttractionCard() {
return (
<div className="rounded-xl shadow p-4">
<h3>大熊猫繁育研究基地</h3>
<p>¥55 · 3 小时</p>
</div>
);
}

定义好之后,它就能像标签一样用:

<AttractionCard />
规则 说明
首字母必须大写 <AttractionCard /> ✅ / <attractionCard /> ❌ —— 小写会被当成普通 HTML 标签
它是个函数 所以你会看到 function Xxx() 或 const Xxx = () =>
必须 return JSX 返回的东西就是它渲染出来的界面

💡 怎么在代码里认出组件:找首字母大写的标签。<div> 是 HTML,<AttractionCard> 是别人写的组件——想改它的样子,就去找同名的那个函数。

第 5 步:props —— 给组件传参数

上面那个卡片有个问题:内容写死了,六个景点就得写六个组件。

props 解决这个——像给标签加属性一样给组件传数据:

// 定义时:接收 props
function AttractionCard(props) {
return (
<div className="rounded-xl shadow p-4">
<h3>{props.name}</h3>
<p>¥{props.price} · {props.duration}</p>
</div>
);
}
// 使用时:像 HTML 属性一样传
<AttractionCard name="大熊猫基地" price={55} duration="3 小时" />
<AttractionCard name="宽窄巷子" price={0} duration="2 小时" />

对照 4.2 学的 HTML 属性,你会发现是同一个思路:

HTML: <img src="/panda.jpg" alt="熊猫" /> ← 给标签传信息
React: <AttractionCard name="大熊猫基地" /> ← 给组件传信息

你还会常看到这种解构写法,意思完全一样:

function AttractionCard({ name, price, duration }) {
return <div><h3>{name}</h3>...</div>;
}

看到 ({ name, price }) 这个形状,就知道“这个组件需要别人传 name 和 price 给它”。这是你读组件代码时第一个要看的地方——它需要什么数据。

第 6 步:组件 + 数组 + map = 第 5 章的核心套路

现在把 4.4 的 map 接上来。这是本章最重要的一个组合。

const attractions = [
{ id: 1, name: "大熊猫基地", price: 55, duration: "3 小时" },
{ id: 2, name: "宽窄巷子", price: 0, duration: "2 小时" },
{ id: 3, name: "武侯祠", price: 50, duration: "1.5 小时" }
];
export default function Home() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{attractions.map(item => (
<AttractionCard
key={item.id}
name={item.name}
price={item.price}
duration={item.duration}
/>
))}
</div>
);
}

读一遍这段代码,你会发现本章学的东西全在里面:

这一部分 来自哪一节
[{...}, {...}] 数据形状 4.4(“一张表格”)
.map(item => ...) 4.4(把一个数组变成另一个数组)
grid grid-cols-1 md:grid-cols-3 gap-6 4.3(Grid 布局 + 响应式断点)
<AttractionCard /> 自定义标签 4.6(组件)
name={item.name} 4.6(props)

现在景点改成 20 个,你只要改数据数组。 卡片样式要改,只改 AttractionCard 一处。这就是第 1 步那三个痛点的答案。

🔑 key={item.id} 是干什么的:React 需要一个身份证来区分列表里的每一项,否则更新时会错乱。

你会遇到这个警告:控制台黄字 Each child in a list should have a unique "key" prop。看到它就知道:某个 map 渲染的列表少了 key。跟 AI 说一句就能修。

第 7 步:state —— 会变的数据

props 是“别人传进来的数据”,state 是“组件自己的、会变的数据”。

import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
// ↑读 ↑改 ↑初始值
return (
<button onClick={() => setCount(count + 1)}>
已有 {count} 人想去
</button>
);
}

useState 返回两个东西:

作用
count 当前的值(读它)
setCount 改这个值的函数(必须用它改)

点一下按钮 → setCount 改了数据 → React 自动重新渲染,按钮上的数字变了。

⚠️ 一条铁律:改 state 必须用 setXxx。直接写 count = count + 1 不会有任何反应——React 不知道数据变了,界面不会更新。

这是新手最常见的 React bug,而且它不报错,就是“点了没反应”(4.1 定位表里那一条)。

什么东西该做成 state:

场景 要 state
输入框里用户打的字 ✅
弹窗是开着还是关着 ✅
当前选中的是哪个标签页 ✅
从服务器拿回来的数据 ✅
景点的固定介绍文字 ❌ 不变的东西不需要 state

第 8 步:你会在 AI 代码里看到的五样东西

这是本节的核心交付能力。 打开任意一个 React 文件,找这五样:

# 找什么 长什么样 意味着什么
1 组件定义 export default function Home() 这个文件对应的界面从这儿开始
2 return 里的 JSX return ( <div>... ) 页面长什么样,看这里
3 props ({ name, price }) 这个组件需要别人给它什么数据
4 useState const [x, setX] = useState() 哪些东西是会变的
5 map {list.map(item => ...)} 数据在这里变成了界面

💡 配合 4.4 第 6 步的四步法,你现在有了一套完整的读码方法: 先找数据(数组 / useState)→ 再找 map(数据怎么变成界面)→ 再找事件(onClick)→ 剩下的是样式,看 Tailwind class。

不用逐行读。 找到这几个点,整个文件的骨架就清楚了。

第 9 步:动手——把重复代码变成组件

这是本节交付物。 在 hello-node 里做。

动手前先存档(2.3 的规矩一):

终端窗口
git add -A && git commit -m "4.6 组件化练习前的存档点"

第一步,先造出重复。 让 AI 做:

在 app/page.tsx 里,把现有内容替换成三个景点卡片,
每张卡片包含景点名、价格、游玩时长,用 Tailwind 做出圆角和浅阴影。
**故意不要抽组件,三张卡片的代码重复写三遍**,我要用它做组件化练习。

跑起来看一眼(pnpm dev),然后打开 page.tsx——你会看到三段几乎一样的代码。

第二步,让它改造:

现在把这三段重复的卡片代码抽成一个组件:
1. 组件名叫 AttractionCard,放在 components/AttractionCard.tsx;
2. 通过 props 接收景点名、价格、时长;
3. 在 page.tsx 里把景点数据写成一个数组,用 map 渲染出三张卡片,记得加 key;
4. 页面最终效果要和现在**完全一样**。
改完告诉我:你新建了哪些文件、page.tsx 里的代码行数从多少变成了多少。

第三步,验收(3.5 的四种验收):

终端窗口
git diff --stat # 看改动范围

浏览器刷新 —— 页面看起来应该一模一样,但代码已经完全不同了。

第四步,检验组件化到底值不值。 再提一个需求:

给每张卡片加一个「加入行程」按钮,样式和卡片风格一致。

注意它只改了一个文件(AttractionCard.tsx),三张卡片全都有了按钮。

📷 截图位 1:改造前后的 page.tsx 对比,以及加按钮时只改一个文件的 git diff

满意就存档:

终端窗口
git add -A && git commit -m "抽出 AttractionCard 组件,用 map 渲染列表"

🔑 你刚才做的,就是第 5.4 节的核心内容。 到那时只是换成真实的六个成都景点和真实的图片——动作一模一样。

第 10 步:什么时候不需要框架

为了不让你以为“框架永远是对的”:

场景 建议
一个静态落地页,没有交互 纯 HTML + CSS 足够,上框架是负担
一个简单的工具页(算个数、转个格式) 单个 HTML 文件就行
内容型网站、多页面、有重复区块 用框架(第 5 章)
有数据、有登录、有交互状态 必须用框架(第 9 章)

1.4 你用在线工具做的那个个人主页,背后很可能就是纯 HTML——那个场景确实不需要 React。


四、完成的标志是

  • 我说得出纯 HTML 做重复区块的三个痛点
  • 我知道 React 的核心是「改数据,界面自动变」
  • 我知道 JSX 就是“写在 JS 里的标签”,且 class 要写成 className
  • 我知道 JSX 里 {} 包的是会被执行的代码(看到它就知道这块是动态的)
  • 我知道组件就是个返回 JSX 的函数,首字母必须大写
  • 我知道 props 是“给组件传数据”,和 HTML 属性是同一个思路
  • 我看得懂 {list.map(item => <Card key={item.id} name={item.name} />)} 在干什么
  • 我知道 key 是列表项的身份证,少了会有控制台警告
  • 我知道 state 是“会变的数据”,改它必须用 setXxx,直接赋值不会更新界面
  • 我记住了读 React 代码要找的五样东西
  • 我完成了「重复三段 → 一个组件 + map」的改造,且页面效果不变
  • 我体会到了加按钮时只需改一个文件

五、卡住了看这里

Q:控制台警告 Each child in a list should have a unique "key" prop。 map 渲染的列表少了 key。跟 AI 说:「给这个 map 渲染的列表加上 key,用数据里的 id」。这是警告不是错误,页面还能跑,但该修。

Q:报错 Objects are not valid as a React child。 你把一个对象直接放进 JSX 显示了。比如写了 {item} 而不是 {item.name}。把整段报错给 AI 即可。

Q:我改了数据,页面没变。 两种情况:① 改的是普通变量而不是 state → 要用 useState + setXxx;② 用了 state 但直接赋值(count = 1)而不是 setCount(1)。这两个都是“点了没反应”的典型原因。

Q:className 和 class 我老是写错。 不用担心,你不写代码。但 review 时见到 JSX 里写了 class= 要留意——样式不生效的一个隐蔽原因就是这个。

Q:组件抽出来之后,页面变样了 / 报错了。 用 2.3 的后悔药:git restore . 回到改造前,然后换个说法重来(3.5 讲的“三轮不对就回滚重开”)。常见原因是 props 名字对不上——AI 在组件里写了 title,在 page 里传的是 name。

Q:React 和 Next.js 什么关系? React 管“怎么把数据变成界面”,Next.js 在 React 外面加了一层:路由(网址怎么对应页面)、服务端渲染、项目结构约定。

类比:React 是发动机,Next.js 是整台车。 你的 hello-node 就是 Next.js 项目,里面写的是 React。4.7、4.8 讲 Next.js 那一层。

Q:我需要专门去学 React 吗? 不需要。本节这些就是你做完第 5 章项目所需的全部 React 知识。 等你做完三个项目,回头再看 React 文档会轻松很多——那时你已经有实感了。


六、本节提示词

① 让 AI 解释一个 React 文件(按第 8 步的五样东西问):

我是零基础,正在学看懂 React。请讲解这个文件:
【@文件名 或粘贴代码】
请按这个顺序讲,用大白话:
1. **组件**:这个文件定义了哪些组件,分别对应页面上哪一块?
2. **数据**:数据从哪来?是写死的、props 传进来的、还是 state?
3. **动态部分**:JSX 里哪些 `{}` 是动态的,分别显示什么?
4. **列表**:有没有 map?它把什么数据变成了什么界面?
5. **交互**:有没有 onClick 之类的事件?点了会发生什么?
6. 最后用三句话总结这个文件在干什么。
不要逐行翻译语法。

② 抽组件(本节第 9 步的完整版):

我的页面里有重复的代码,想抽成组件。
文件:【@文件名】
重复的部分:【描述,比如"三段几乎一样的卡片代码"】
请你:
1. **先告诉我方案**:组件叫什么、放哪个文件、通过 props 接收哪些数据,等我确认再动手;
2. 改造后页面效果必须**完全不变**;
3. 数据抽成数组,用 map 渲染,记得加 key;
4. 不要新增任何依赖包;
5. 改完告诉我:新建了哪些文件、原文件代码行数的变化。

③ 判断该不该用 state:

我想在页面上实现这个效果:【具体描述,比如"点击按钮展开/收起详情"】
请告诉我:
1. 这里面哪些数据是"会变的",需要做成 state?
2. 哪些是固定的,不需要 state?
3. 给我实现思路(**先不要写代码**),说明用户操作之后数据怎么变、界面怎么跟着变;
4. 这个功能有没有更简单的做法(比如纯 CSS 就能实现)?

上一节 ← 4.5 一个前端项目由哪些文件组成 下一节 → 4.7 Next.js 快速上手:创建项目与目录结构