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 变成那样。

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。