Chapter 03

React 基础:组件、JSX 与 Props

一切皆组件——React 用声明式的方式描述 UI,让界面随数据变化自动更新

1. 什么是 React

React 是 Meta(原 Facebook)开源的 UI 库——注意是"库"不是"框架"。它只负责视图层(View),不内置路由、状态管理等能力,需要搭配生态中的其他库(React Router、Zustand 等)来构建完整应用。

组件化思想

React 的核心思想是组件化:把 UI 拆分为独立、可复用的小块,每个组件管理自己的状态和渲染逻辑,组件之间通过 Props 通信。就像乐高积木,复杂界面由简单组件组合而成。

ℹ️

声明式 vs 命令式:传统 DOM 操作是命令式的——"找到这个元素,改变它的文本"。React 是声明式的——"当 count 是 5 时,UI 应该长这样"。你描述应该是什么,React 负责让 DOM 变成那样。

声明式编程(Declarative)
描述"应该是什么"(What),而不是"怎么做"(How)。React 中你声明"当 state=X 时 UI 应该长这样",React 负责让 DOM 变成那样。对比命令式:手动 document.getElementById('x').textContent = val
Virtual DOM
在内存中维护的轻量 JavaScript 对象树,是真实 DOM 的镜像。状态变化时,React 先更新 Virtual DOM,通过 Diff 算法找出最小变更集,再批量更新真实 DOM,减少昂贵的浏览器重排/重绘操作。
React Fiber
React 16+ 的协调引擎重写。将渲染工作拆分为可中断的小单元(fiber),支持任务优先级调度和时间切片(Time Slicing)。高优先级的用户交互可以打断低优先级的渲染,保持应用响应性。
协调(Reconciliation)
React 将新旧 Virtual DOM 树进行 Diff 比较,决定哪些真实 DOM 节点需要更新、添加或删除的过程。使用 key 帮助 React 在列表中追踪元素身份,实现精准的最小化 DOM 操作。

2. Virtual DOM 原理

直接操作真实 DOM 是昂贵的——浏览器需要重新计算布局(reflow)和重绘(repaint)。React 引入 Virtual DOM 来优化这个过程。

工作原理

  1. React 在内存中维护一棵轻量的 JS 对象树,映射真实 DOM 结构
  2. 当状态变化时,React 生成一棵新的 Virtual DOM 树
  3. Diff 算法对比新旧两棵树,找出差异(最小变更集)
  4. 只把差异部分更新到真实 DOM(批量更新)

key 的重要性

React 的 Diff 算法在处理列表时,用 key 来识别每个列表项的"身份"。有了 key,React 能精准判断哪个元素被增删改,避免不必要的重渲染。

// ❌ 没有 key —— React 无法判断哪个元素是哪个
{items.map(item => <li>{item.name}</li>)}

// ✅ 有稳定唯一的 key
{items.map(item => <li key={item.id}>{item.name}</li>)}

3. 创建第一个 React 项目

推荐使用 Vite 创建 React + TypeScript 项目。Vite 是新一代前端构建工具,开发服务器启动极快(利用浏览器原生 ESM),热更新几乎即时。

创建项目

# 创建项目(react-ts 模板自带 TypeScript 配置)
npm create vite@latest my-app -- --template react-ts

cd my-app
npm install
npm run dev    # 启动开发服务器,默认 http://localhost:5173

项目结构说明

my-app/
├── public/              # 静态资源(不经 Vite 处理,直接复制到 dist)
├── src/
│   ├── main.tsx         # 入口文件 —— 将 App 挂载到 #root
│   ├── App.tsx          # 根组件
│   ├── App.css          # 根组件样式
│   ├── index.css        # 全局样式
│   └── vite-env.d.ts    # Vite 的类型声明
├── index.html           # HTML 模板,Vite 从这里开始构建
├── vite.config.ts       # Vite 配置
├── tsconfig.json        # TypeScript 配置
└── package.json

main.tsx — 应用入口

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

// 找到 index.html 中的 <div id="root">,将 React 应用挂载到此
createRoot(document.getElementById('root')!).render(
  <StrictMode>  // 开发模式下额外检查(双调用 render,只在 dev 环境)
    <App />
  </StrictMode>,
)

4. JSX 规则

JSX(JavaScript XML)是 React 的模板语法,让你在 JS/TS 中写类似 HTML 的代码。但 JSX 不是 HTML,有很多不同之处需要注意。

JSX vs HTML 的主要差异

HTMLJSX原因
class="btn"className="btn"class 是 JS 保留字
for="input1"htmlFor="input1"for 是 JS 保留字
onclick="fn()"onClick={fn}驼峰命名,传函数引用
style="color:red"style={{ color: 'red' }}style 是 JS 对象
<br><br />JSX 要求自闭合
可以多个根元素必须有一个根元素JSX 转 JS 函数调用

Fragment 避免多余 DOM 节点

// ❌ 多个根元素 —— 报错
return (
  <h1>Title</h1>
  <p>Content</p>
);

// ✅ 用 Fragment 包裹(不生成额外 DOM 节点)
return (
  <>
    <h1>Title</h1>
    <p>Content</p>
  </>
);

JSX 表达式 {}

const name = 'Alice';
const isAdmin = true;
const items = ['Apple', 'Banana'];

return (
  <div>
    {/* {} 中放 JS 表达式(不能放语句)*/}
    <p>Hello, {name}!</p>
    <p>Score: {90 + 10}</p>

    {/* 条件渲染 */}
    {isAdmin && <span>Admin</span>}
    {isAdmin ? <span>Admin</span> : <span>User</span>}

    {/* 列表渲染 */}
    <ul>
      {items.map((item, i) => <li key={i}>{item}</li>)}
    </ul>
  </div>
);

5. 函数组件

React 16.8 之前,有状态的组件必须用类(Class Component)来写。Hooks 的引入让函数组件也能有状态,类组件几乎已被废弃。现代 React 全用函数组件。

// 最简单的函数组件 —— 接收 props,返回 JSX
function Hello({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>;
}

// 箭头函数写法(两种都常见)
const Hello = ({ name }: { name: string }) => (
  <h1>Hello, {name}!</h1>
);

// 使用(大写开头,与 HTML 标签区分)
<Hello name="World" />
ℹ️

React.FC 的争议:早期常见 const Comp: React.FC<Props> = (props) => ...,但 React.FC 在 React 18 中已移除了隐式的 children: ReactNode,且类型推断有些奇怪。现代推荐直接标注参数类型:function Comp(props: Props)

6. Props 详解

Props(Properties)是组件的输入参数。父组件通过 Props 向子组件传递数据,就像函数的参数一样。

Props 是只读的

这是 React 的核心约束:子组件不能修改 Props。Props 是单向数据流的体现——数据只从父流向子,子组件通过回调函数(也是 Props)通知父组件更改状态。

定义 Props 接口

interface UserCardProps {
  name: string;
  age: number;
  role: 'admin' | 'editor' | 'viewer';
  avatar?: string;        // 可选,没有则显示默认头像
  onFollow?: () => void; // 可选回调
  children?: React.ReactNode; // 接收子内容
}

function UserCard({
  name,
  age,
  role,
  avatar = '/default-avatar.png', // 参数默认值(代替 defaultProps)
  onFollow,
  children
}: UserCardProps) {
  return (
    <div className="card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{age} · {role}</p>
      {onFollow && <button onClick={onFollow}>Follow</button>}
      {children}   {/* 渲染子内容 */}
    </div>
  );
}

// 使用
<UserCard
  name="Alice"
  age={25}
  role="admin"
  onFollow={() => console.log('followed!')}
>
  <p>Additional content here</p>
</UserCard>

children 类型

interface ContainerProps {
  children: React.ReactNode;  // 最宽泛:JSX、string、number、null...
}

interface SingleChildProps {
  children: React.ReactElement;  // 必须是单个 JSX 元素
}

interface RenderPropProps {
  children: (data: User) => React.ReactNode; // 函数 children
}

7. 组件组合

React 推崇组合(Composition)而非继承。通过 children 和 Props 回调,父组件可以灵活地定制子组件的行为。

条件渲染

function UserPanel({ user, isLoading, error }: PanelProps) {
  // 提前返回(Early Return)—— 处理不同状态
  if (isLoading) return <div>加载中...</div>;
  if (error)     return <div>错误: {error}</div>;
  if (!user)     return null; // 不渲染任何东西

  return (
    <div>
      {/* && 短路:只有 user.isAdmin 为 true 才渲染 */}
      {user.isAdmin && <span className="badge">Admin</span>}

      {/* 三元运算符:两个分支 */}
      <p>{user.isActive ? '在线' : '离线'}</p>
    </div>
  );
}
⚠️

陷阱{count && <Comp />} —— 当 count0 时,会在页面上渲染数字 0!因为 0 是假值但不是 null/undefined,React 会渲染它。
正确写法:{count > 0 && <Comp />}{!!count && <Comp />}

8. 列表渲染

React 使用 JavaScript 的 .map() 来渲染列表,每个列表项必须有唯一的 key prop。

key 为什么不能用 index

用数组下标作为 key 会导致问题:当列表被重排序或中间插入/删除元素时,index 会变化,React 认为"第 2 个元素变了",从而重新渲染了本来没有变化的组件,还可能导致组件状态混乱(比如表单输入内容跑到了错误的行)。

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map(todo => (
        {/* ✅ 用稳定的业务 id,而不是数组 index */}
        <li key={todo.id}>
          <span
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.text}
          </span>
        </li>
      ))}
    </ul>
  );
}

9. 事件处理

React 使用合成事件(SyntheticEvent)对浏览器原生事件进行跨浏览器封装,提供统一的接口。合成事件遵循 W3C 规范,让你不用担心浏览器兼容性。

常用事件处理示例

import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // input 的 onChange 事件
  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  // form 的 onSubmit 事件
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // 阻止默认行为(页面刷新)
    console.log({ email, password });
  };

  // 按键事件
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') handleSubmit(e as any);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={handleEmailChange}
        onKeyDown={handleKeyDown}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)} {/* 内联写法 */}
      />
      <button type="submit">登录</button>
    </form>
  );
}

事件处理的 TypeScript 类型参考

事件TS 类型
input / textarea onChangeReact.ChangeEvent<HTMLInputElement>
select onChangeReact.ChangeEvent<HTMLSelectElement>
button onClickReact.MouseEvent<HTMLButtonElement>
form onSubmitReact.FormEvent<HTMLFormElement>
键盘事件 onKeyDownReact.KeyboardEvent<HTMLInputElement>
鼠标悬浮 onMouseEnterReact.MouseEvent<HTMLDivElement>

本章小结:React 的核心是"声明式 UI + 组件化"。掌握 JSX 规则(className、驼峰事件、必须有根元素),理解 Props 是只读的单向数据流,学会列表渲染和事件处理,就具备了 React 开发的基础。下一章深入 Hooks。