「前端何去何从」一直写 Vue ,为何要在 AI 时代去学 React?

React UI 基础:重新思考学习 React 的意义

在 AI 快速发展的时代,重新思考学习 React 的意义

AI 时代为什么还要学 React

2026 年学 React,很多人会问:AI 都能写代码了,还有必要学框架吗?

我的答案是:比以前更有必要。但学习的方式和目的变了。

现在有了 Copilot、Cursor 这些工具,写组件的速度确实快了很多。

但我发现,如果不理解 React 的核心概念,AI 生成的代码往往会有隐藏的 bug。

工具可以加速,但不能替代理解。

前端开发的现状

前端开发正在经历两个趋势:

  • 框架的成熟:React、Vue、Svelte 等框架已经相对稳定,核心概念不再频繁变化
  • AI 的介入:AI 工具可以生成大量样板代码,但需要人来把控架构和质量

在这个背景下,理解原理比记忆 API 更重要。

React 的组件化思维、单向数据流、纯函数理念,这些不会因为 AI 的出现而过时。

相反,它们是你判断 AI 生成代码质量的标准。

React 在 AI 时代的生态优势

值得一提的是,React 在 AI 时代有一个显著的优势:AI 模型的训练数据主要来自 React 生态

这意味着:

  • AI 工具对 React 代码的理解和生成质量更高
  • 大量优秀的组件库(如 shadcn/ui、Radix UI、Chakra UI)都是为 React 设计的
  • 当你用 AI 生成代码时,React 的代码质量通常比其他框架更好

这不是说其他框架不好,而是一个现实:React 的社区规模和代码量决定了 AI 对它的支持更好。在选择技术栈时,这是一个不容忽视的因素。

本文的定位

这不是一篇从零开始的教程,而是一个有经验的工程师对 React 基础的总结和思考。

本文涉及以下内容:

  • 讲述 React 的设计理念,而不只是语法
  • 分享实战中的经验和常见错误
  • 探讨在 AI 辅助下如何更好地使用 React

如果你有一定的 JavaScript 基础,想快速掌握 React 的核心概念,这篇文章适合你。


Part 1: 组件化思维

组件的本质

React 的组件本质上是函数。

输入 props,输出 UI。

这种函数式的思维方式,让代码更容易测试和维护。

jsx 复制代码
function Profile() {
  return (
    <img
      src="https://i.imgur.com/MK3eW3Am.jpg"
      alt="Katherine Johnson"
    />
  );
}

这个 Profile 组件就是一个函数,返回一段 JSX。你可以在任何地方调用它:

jsx 复制代码
function Gallery() {
  return (
    <section>
      <h1>了不起的科学家</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

为什么选择组件化

React 选择组件化不是偶然的。在传统的 Web 开发中,HTML、CSS、JavaScript 是分离的。

但在现代应用中,UI 逻辑和标记是紧密相关的:按钮的点击事件和按钮本身是一体的,表单的验证逻辑和表单结构是一体的。

组件化让你可以在同一个地方处理这些相关的逻辑,而不是在多个文件之间跳来跳去。这不是技术上的限制,而是对现实问题的务实解决方案。

组件的拆分原则

刚开始写 React 时,我也纠结过什么时候该拆分组件。

后来发现,与其纠结规则,不如问自己:这段代码会不会在其他地方用到?

如果会,就拆;如果不会,先别急着拆。

过度抽象比重复代码更难维护。我见过太多项目,为了"复用"而创建了一堆只用一次的组件,反而增加了理解成本。

实际项目中的经验:

  • 如果一个组件超过 200 行,考虑拆分
  • 如果一个组件做了太多事情(数据获取、状态管理、UI 渲染),考虑拆分
  • 如果一段代码在两个地方用到,可以考虑提取;三个地方用到,一定要提取

组件的命名和组织

React 要求组件名必须以大写字母开头。

这不是为了好看,而是为了区分组件和普通 HTML 标签:

jsx 复制代码
// React 知道 <Profile /> 是组件
<Profile />

// React 知道 <div> 是 HTML 标签
<div></div>

关于文件组织:

  • 一个文件一个主要组件,文件名和组件名保持一致
  • 如果有多个紧密相关的小组件,可以放在同一个文件中
  • 不要在组件内部定义组件,这会导致每次渲染都创建新的组件定义,破坏性能优化

模块化的实践

React 使用标准的 JavaScript 模块系统。

这里需要注意默认导出和具名导出的区别:

jsx 复制代码
// 默认导出 - 一个文件一个主要组件
export default function Button() {
  return <button>点击我</button>;
}

// 导入时可以使用任意名称
import MyButton from './Button.js';
jsx 复制代码
// 具名导出 - 一个文件多个组件
export function Button() {
  return <button>点击我</button>;
}

export function Input() {
  return <input />;
}

// 导入时必须使用相同的名称
import { Button, Input } from './Components.js';

建议:团队内保持一致即可。

个人倾向于默认导出,因为它让导入语句更简洁,也更容易重构。


Part 2: JSX 的设计哲学

JSX 解决了什么问题

在 JSX 出现之前,React 使用 React.createElement() 来创建元素:

javascript 复制代码
const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello, world!'
);

这种方式的问题是:代码结构和最终的 UI 结构差距太大,难以理解和维护。JSX 让代码更接近最终的 UI 结构:

jsx 复制代码
const element = <h1 className="greeting">Hello, world!</h1>;

JSX 不是必需的,但它让代码更直观。这是一个务实的选择。

JSX 的三条规则

JSX 看起来像 HTML,但它更严格。这些规则背后都有技术原因。

规则 1:只能返回一个根元素

jsx 复制代码
// 错误:返回了两个元素
function AboutPage() {
  return (
    <h1>关于我们</h1>
    <p>欢迎来到我们的网站</p>
  );
}

// 正确:用 Fragment 包裹
function AboutPage() {
  return (
    <>
      <h1>关于我们</h1>
      <p>欢迎来到我们的网站</p>
    </>
  );
}

为什么?因为 JSX 会被转换成 JavaScript 函数调用,而函数只能返回一个值。

<>...</> 是 Fragment 的简写,它不会在 DOM 中创建额外的节点。

规则 2:所有标签必须闭合

在 HTML 中,<img><br> 可以不闭合。但在 JSX 中,所有标签都必须闭合:

jsx 复制代码
<img src="avatar.jpg" />
<br />

这是因为 JSX 是 JavaScript,需要明确的语法边界。

规则 3:使用驼峰命名法

JSX 会转换成 JavaScript,所以属性名需要遵循 JavaScript 的命名规则:

jsx 复制代码
// HTML 中
<div class="container" tabindex="0"></div>

// JSX 中
<div className="container" tabIndex={0}></div>

classfor 是 JavaScript 保留字,所以 JSX 使用 classNamehtmlFor。其他属性使用驼峰命名法,如 onClickstrokeWidth

JSX 中的 JavaScript

JSX 的强大之处在于,你可以在标记中嵌入 JavaScript 表达式。使用大括号 {} 就可以"回到" JavaScript:

jsx 复制代码
function Profile() {
  const name = "Katherine Johnson";
  const avatar = "https://i.imgur.com/MK3eW3Am.jpg";

  return (
    <div>
      <h1>{name}的个人资料</h1>
      <img src={avatar} alt={name} />
    </div>
  );
}

你可以在大括号中使用任何 JavaScript 表达式:

jsx 复制代码
function TodoList() {
  const tasks = 3;

  return (
    <div>
      <h1>待办事项</h1>
      <p>你还有 {tasks} 个任务</p>
      <p>完成度:{(tasks / 10) * 100}%</p>
    </div>
  );
}

双大括号的秘密

你可能会看到这样的代码:

jsx 复制代码
<div style={{ backgroundColor: 'black', color: 'pink' }}>
  内容
</div>

这不是特殊语法。外层的 {} 表示"这是 JavaScript 表达式",内层的 {} 表示"这是一个 JavaScript 对象"。

JSX vs 模板语法

有些框架(如 Vue)使用模板语法,提供了 v-ifv-for 这样的指令。React 选择了 JSX,让你直接使用 JavaScript 的 ifmap 等语法。

这两种方式各有优劣:

  • 模板语法更容易学习,但功能受限
  • JSX 更灵活,但需要更好的 JavaScript 基础

React 的选择是:相信开发者的 JavaScript 能力,不创造新的语法。

这在 AI 时代更有意义,因为 AI 更容易理解标准的 JavaScript,而不是框架特定的语法。


Part 3: Props 与数据流

Props 的设计理念

Props 是 React 实现单向数据流的核心机制。

父组件通过 props 向子组件传递数据,子组件不能修改 props。

jsx 复制代码
// 父组件传递 props
function Profile() {
  return (
    <Avatar
      name="Lin Lanying"
      imageUrl="https://i.imgur.com/1bX5QH6.jpg"
      size={100}
    />
  );
}

// 子组件接收 props
function Avatar({ name, imageUrl, size }) {
  return (
    <img
      src={imageUrl}
      alt={name}
      width={size}
      height={size}
    />
  );
}

这里使用了解构语法,让代码更简洁。你也可以使用 props 对象:

jsx 复制代码
function Avatar(props) {
  return (
    <img
      src={props.imageUrl}
      alt={props.name}
      width={props.size}
      height={props.size}
    />
  );
}

我建议使用解构语法,因为它让组件的 API 一目了然。

如何设计 Props API

设计 Props API 是一门艺术。好的 Props API 应该:

  • 命名清晰,见名知意
  • 数量适中,不超过 5-6 个
  • 有合理的默认值
jsx 复制代码
function Avatar({ name, imageUrl, size = 100 }) {
  // size 有默认值,调用时可以省略
  return (
    <img
      src={imageUrl}
      alt={name}
      width={size}
      height={size}
    />
  );
}

实际项目中,如果一个组件需要太多 props,通常意味着它做了太多事情,应该考虑拆分。

children 的使用场景

children 是一个特殊的 prop,用于传递组件标签之间的内容:

jsx 复制代码
function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

// 使用时
function Profile() {
  return (
    <Card>
      <h1>用户资料</h1>
      <p>这是卡片的内容</p>
      <button>编辑</button>
    </Card>
  );
}

children 特别适合创建布局组件。我在项目中经常用它来创建 Modal、Card、Container 这样的组件。

Props 不可变性的意义

Props 是只读的,这是 React 的核心原则之一:

jsx 复制代码
// 错误:不要修改 props
function Clock({ color }) {
  color = 'red';  // 错误!
  return <div style={{ color }}>当前时间</div>;
}

为什么?因为 Props 代表父组件传递的数据。如果子组件可以随意修改,会导致数据流混乱,难以追踪 bug。

Props 就像函数的参数,你不会在函数内部修改参数的值。如果需要修改数据,应该使用 state(这是下一个主题)。

这种单向数据流的设计,让 React 应用更容易理解和调试。在大型项目中,这个优势尤其明显。


Part 4: 条件渲染与列表渲染

四种条件渲染方式

React 没有提供 v-if 这样的指令,而是让你直接使用 JavaScript 的条件语法。这给了你更大的灵活性。

方式 1:if 语句(提前返回)

jsx 复制代码
function PackingItem({ name, isPacked }) {
  if (isPacked) {
    return <li className="item">{name} ✔</li>;
  }
  return <li className="item">{name}</li>;
}

适用场景:两种情况的 UI 差异较大时。

方式 2:三元运算符

jsx 复制代码
function PackingItem({ name, isPacked }) {
  return (
    <li className="item">
      {isPacked ? name + ' ✔' : name}
    </li>
  );
}

适用场景:需要在两个选项之间选择时。

方式 3:逻辑与运算符 &&

jsx 复制代码
function Notification({ message, hasNewMessage }) {
  return (
    <div>
      <h1>通知中心</h1>
      {hasNewMessage && <p>你有新消息:{message}</p>}
    </div>
  );
}

适用场景:条件为假时不需要显示任何内容。

注意 :不要在 && 左侧放数字!

jsx 复制代码
// 错误:当 messageCount 为 0 时,会显示 0
{messageCount && <p>有 {messageCount} 条新消息</p>}

// 正确:确保左侧是布尔值
{messageCount > 0 && <p>有 {messageCount} 条新消息</p>}

为什么?因为在 JavaScript 中,0 && something 会返回 0,而 React 会渲染这个 0

方式 4:条件赋值给变量

jsx 复制代码
function PackingItem({ name, isPacked }) {
  let itemContent = name;

  if (isPacked) {
    itemContent = <del>{name} ✔</del>;
  }

  return (
    <li className="item">
      {itemContent}
    </li>
  );
}

适用场景:条件逻辑复杂,或者需要多次使用条件结果时。

选择哪种方式

这取决于具体场景:

  • 简单的二选一:使用三元运算符
  • 只在条件为真时显示:使用 &&
  • 复杂的条件逻辑:使用 if 语句或变量赋值
  • 完全不同的 UI:使用 if 提前返回

在实际项目中,我倾向于使用 if 提前返回,因为它让代码更容易理解。

个人偏好,团队内保持一致即可。

列表渲染的性能考虑

在 React 中,使用 map() 方法来渲染列表:

jsx 复制代码
const scientists = [
  '凯瑟琳·约翰逊',
  '马里奥·莫利纳',
  '穆罕默德·阿卜杜勒·萨拉姆'
];

function ScientistList() {
  return (
    <ul>
      {scientists.map(scientist =>
        <li>{scientist}</li>
      )}
    </ul>
  );
}

如果需要过滤数据,可以先用 filter(),再用 map()

jsx 复制代码
const people = [
  { id: 0, name: '凯瑟琳·约翰逊', profession: '数学家' },
  { id: 1, name: '马里奥·莫利纳', profession: '化学家' },
  { id: 2, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家' },
  { id: 3, name: '珀西·朱利安', profession: '化学家' },
];

function ChemistList() {
  const chemists = people.filter(person =>
    person.profession === '化学家'
  );

  return (
    <ul>
      {chemists.map(person =>
        <li key={person.id}>
          <b>{person.name}</b>: {person.profession}
        </li>
      )}
    </ul>
  );
}

Key 的作用机制

你可能注意到上面的代码中有 key={person.id}。这个 key 非常重要。

Key 告诉 React 每个组件对应数组中的哪个项。

当列表发生变化时,React 通过 key 来判断哪些项是新增的、删除的或移动的,从而只更新必要的部分。

jsx 复制代码
// 好:使用数据的唯一 ID
<li key={person.id}>{person.name}</li>

// 可以但不推荐:使用索引
<li key={index}>{person.name}</li>

// 错误:没有 key
<li>{person.name}</li>

何时可以使用索引作为 key?

只有当列表满足以下所有条件时:

  • 列表是静态的(不会增删改)
  • 列表不会重新排序
  • 列表项没有 ID

否则,使用索引作为 key 可能导致性能问题或 bug。

实战中的常见错误

我见过很多项目在列表渲染时出现问题,主要是:

  • 忘记添加 key
  • 使用索引作为 key,导致列表更新时出现 bug
  • 使用 Math.random() 生成 key,导致每次渲染都重新创建组件

记住:key 应该来自数据本身,而不是生成的。如果数据没有 ID,考虑在获取数据时生成一个唯一标识。


Part 5: 纯函数与副作用

为什么 React 强调纯函数

React 假设你编写的每个组件都是纯函数。这意味着:

  • 给定相同的 props,总是返回相同的 JSX
  • 不修改渲染前就存在的变量或对象
jsx 复制代码
// 纯组件
function Recipe({ drinkers }) {
  return (
    <ol>
      <li>烧开 {drinkers} 杯水。</li>
      <li>加入 {drinkers} 勺茶和 {0.5 * drinkers} 勺香料。</li>
      <li>加入 {0.5 * drinkers} 杯牛奶和糖调味。</li>
    </ol>
  );
}

这个组件是纯粹的:无论调用多少次,相同的 drinkers 总是返回相同的结果。

不纯组件的问题

jsx 复制代码
// 不纯组件:修改了外部变量
let guest = 0;

function Cup() {
  guest = guest + 1;  // 错误!
  return <h2>茶杯 #{guest}</h2>;
}

function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

这个组件有什么问题?每次渲染 Cup 都会修改 guest 变量。在开发模式下,React 会渲染每个组件两次(用于检测副作用),所以实际输出可能是:茶杯 #2, 茶杯 #4, 茶杯 #6。

正确的做法是通过 props 传递数据:

jsx 复制代码
function Cup({ guest }) {
  return <h2>茶杯 #{guest}</h2>;
}

function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

纯函数对性能优化的意义

纯函数的好处不仅仅是可预测性。它还让 React 可以进行性能优化:

  • 如果 props 没有变化,React 可以跳过渲染
  • React 可以安全地中断和重新开始渲染
  • React 可以并发渲染多个组件

这些优化都依赖于组件的纯粹性。如果组件有副作用,这些优化就无法进行。

副作用的正确处理方式

有些操作必须产生副作用,比如:

  • 发送网络请求
  • 更新 DOM
  • 启动动画

这些副作用应该放在事件处理函数中,而不是渲染逻辑中:

jsx 复制代码
function Button() {
  function handleClick() {
    // 副作用:显示提示
    alert('你点击了我!');
  }

  return <button onClick={handleClick}>点击我</button>;
}

如果需要在渲染时执行副作用(比如数据获取),应该使用 useEffect hook。但这是另一个主题了。

StrictMode 的作用

React 提供了严格模式,在开发环境中会调用每个组件两次,帮助发现不纯的组件:

jsx 复制代码
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

如果你的组件是纯粹的,调用两次不会有任何问题。如果不纯,你会立即发现问题。

我建议始终开启严格模式。它能帮你在开发阶段发现很多潜在的问题。


学习路径与思考

下一步应该学什么

掌握了 UI 基础后,接下来应该学习:

  • 添加交互:事件处理、state 管理
  • 状态管理:如何设计 state 结构,如何在组件间共享 state
  • 副作用处理:useEffect 的使用和常见陷阱
  • 性能优化:memo、useMemo、useCallback 的使用场景

这些主题都建立在 UI 基础之上。

如果你理解了组件、props、纯函数这些概念,后续的学习会容易很多。

如何在实战中提升

理论学习很重要,但实战才能真正掌握。我的建议是:

  • 从小项目开始,逐步挑战更复杂的应用
  • 阅读优秀的 React 项目代码,学习他们的组件设计
  • 遇到问题时,先思考为什么,再查文档
  • 不要过度设计,先让代码工作,再优化

AI 辅助开发的正确姿势

现在有了 AI 工具,开发效率确实提高了。但我发现,AI 最适合做的是:

  • 生成样板代码
  • 实现明确的功能
  • 重构和优化代码

AI 不擅长的是:

  • 架构设计
  • 性能优化
  • 复杂的状态管理

所以,理解原理仍然很重要。AI 可以帮你写代码,但不能帮你做决策。

保持技术敏感度的建议

前端技术变化很快,但核心概念变化不大。我的建议是:

  • 关注核心概念,而不是追逐新工具
  • 理解技术选择背后的权衡
  • 保持好奇心,但不要盲目跟风
  • 在实际项目中验证新技术,而不是为了用而用

React 的组件化思维、单向数据流、纯函数理念,这些概念在其他框架中也有体现。

掌握了这些,学习其他框架会容易很多。


总结

文章总结了 React UI 基础的核心概念:

  • 组件化思维:函数式的 UI 构建方式
  • JSX:声明式的 UI 描述语言
  • Props:单向数据流的实现
  • 条件渲染与列表渲染:动态 UI 的构建方式
  • 纯函数:可预测、可优化的组件设计

这些概念是 React 开发的基石。掌握它们后,你已经可以构建简单但完整的 React 应用了。

在 AI 时代,理解这些原理比记忆 API 更重要。它们是你判断 AI 生成代码质量的标准,也是你做技术决策的依据。


相关资源


本文基于 React 官方文档 "描述 UI" 章节。

相关推荐
aircrushin2 小时前
OpenClaw“养龙虾”现象的社会技术学分析
前端·后端
明君879972 小时前
#Flutter 的官方Skills技能库
前端·flutter
yuki_uix2 小时前
重新认识 React Hooks:从会用到理解设计
前端·react.js
林太白2 小时前
ref和reactive对比终于学会了
前端
Apifox2 小时前
测试数据终于不用到处复制了,Apifox 自动化测试新增「共用测试数据」
前端·后端·测试
小小小小宇3 小时前
Mac龙虾保姆级完整部署指南
前端
睡不着的可乐3 小时前
vue2 和 vue3自定义指令有什么区别,都是怎么实现和使用一个指令
前端·vue.js
闲来没事抠鼻屎3 小时前
Web打印插件实战:轻量化JS打印方案vue-print-designer落地指南
前端
孙凯亮3 小时前
从 SSR 踩坑到 CSR 封神:Nuxt4 全流程终极实战
前端