轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • 前端框架
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • 前端框架
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • 前端框架

    • Vue.js快速入门
    • Vue.js历代版本新特性
    • Nuxt.js极简入门
    • React极简入门
      • 一、React 简介
        • 什么是 React
        • 核心特性
        • React vs Vue.js
      • 二、环境准备
        • 开发环境要求
        • 创建第一个 React 应用
        • 项目结构
      • 三、核心概念
        • 1. 第一个组件:Hello World
        • 2. JSX:在 JavaScript 中写 HTML
        • 基础示例
        • 在 JSX 中使用 JavaScript
        • JSX 的重要规则
        • 3. Props:组件间传递数据
        • 完整示例:用户卡片
        • Props 解构(更简洁的写法)
        • Props 默认值
        • 特殊的 Props:children
        • 4. State:组件的记忆
        • 第一个有状态的组件:计数器
        • 完整的计数器示例
        • 不同类型的 State
        • State 更新的重要规则
        • 5. 事件处理:响应用户操作
        • 基础点击事件
        • 表单输入处理
        • 常见事件示例
        • 6. 条件渲染:根据条件显示不同内容
        • 使用 if/else
        • 使用三元运算符
        • 使用 && 运算符
        • 完整示例:用户权限
        • 7. 列表渲染:显示多个相似的元素
        • 基础列表渲染
        • 对象数组渲染
        • 可交互的列表
        • 为什么需要 key?
      • 四、Hooks(钩子)
        • 什么是 Hooks?
        • useState - 状态管理
        • 基础用法回顾
        • 多个状态
        • 对象状态的正确更新
        • 数组状态的更新
        • 函数式更新
        • useEffect - 副作用处理
        • 什么是副作用?
        • 基础用法
        • 依赖数组
        • 清理函数
        • 数据获取示例
        • 监听窗口大小
        • useRef - 引用 DOM 或保持值
        • 访问 DOM 元素
        • 视频播放器示例
        • 保持值(不触发重新渲染)
        • 对比 useState 和 useRef
        • useContext - 跨组件传递数据
        • 问题:层层传递 Props
        • 解决方案:使用 Context
        • 完整示例:用户信息共享
        • useMemo - 缓存计算结果
        • 基础示例
        • 实用示例:过滤列表
        • useCallback - 缓存函数
        • 基础示例
        • 实用示例:搜索功能
        • 自定义 Hooks
        • 自定义 Hook:useLocalStorage
        • 自定义 Hook:useToggle
        • 自定义 Hook:useFetch
        • Hooks 使用规则
      • 五、实战示例:Todo 应用
      • 六、最佳实践
        • 1. 组件设计原则
        • 2. 状态管理
        • 3. 性能优化
        • 4. 代码组织
      • 七、常用工具和库
        • 路由
        • 状态管理
        • UI 组件库
        • 开发工具
      • 八、学习资源
        • 官方文档
        • 进阶主题
        • 实践项目
      • 九、总结
    • UmiJS快速入门
  • 工程化

  • 浏览器与Web API

  • 前端
  • 前端框架
轩辕李
2024-12-24
目录

React极简入门

本文面向具有 JavaScript 基础的开发者,快速介绍 React 的核心概念和开发方式。如果你已经熟悉 JavaScript 和基本的前端开发,这篇文章将帮助你快速上手 React。

# 一、React 简介

# 什么是 React

React 是由 Facebook(现 Meta)开发的用于构建用户界面的 JavaScript 库。它于 2013 年开源,现已成为最流行的前端框架之一。

React 与 JavaScript 的关系:

  • JavaScript 是一门编程语言,提供基础语法和 API
  • React 是基于 JavaScript 构建的库,专注于视图层的构建
  • 如果你还不熟悉 JavaScript,建议先阅读 JavaScript极简入门

# 核心特性

  1. 组件化:UI 被拆分为独立、可复用的组件
  2. 声明式:描述 UI 应该是什么样子,React 负责更新
  3. 虚拟 DOM:高效的 DOM 更新机制
  4. 单向数据流:数据从父组件流向子组件
  5. JSX 语法:在 JavaScript 中编写类似 HTML 的标记
  6. 生态丰富:拥有完善的工具链和第三方库

# React vs Vue.js

两者都是优秀的前端框架,主要区别:

特性 React Vue.js
学习曲线 需要学习 JSX、Hooks 等概念 模板语法更接近传统 HTML
灵活性 更自由,选择更多 提供更多官方方案
数据绑定 单向数据流 双向数据绑定(v-model)
生态 社区方案为主 官方方案为主
类型支持 TypeScript 支持良好 TypeScript 支持良好

如需了解 Vue.js,请参考 Vue.js快速入门。

# 二、环境准备

# 开发环境要求

  • Node.js 16.0 或更高版本
  • 现代浏览器(Chrome、Firefox、Edge 等)
  • 代码编辑器(推荐 VS Code)

# 创建第一个 React 应用

使用 Vite 创建 React 项目(推荐方式):

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

或使用 Create React App:

npx create-react-app my-react-app
cd my-react-app
npm start

# 项目结构

my-react-app/
├── node_modules/
├── public/
│   └── index.html
├── src/
│   ├── App.jsx          # 根组件
│   ├── App.css          # 样式文件
│   ├── main.jsx         # 入口文件
│   └── index.css
├── package.json
└── vite.config.js       # Vite 配置

# 三、核心概念

# 1. 第一个组件:Hello World

在 React 中,一切都是组件。让我们从最简单的组件开始:

// 在 src/App.jsx 中
function App() {
  return <h1>Hello World</h1>;
}

export default App;

这个组件做了什么?

  1. function App() - 定义了一个名为 App 的函数组件
  2. return <h1>Hello World</h1> - 返回一个看起来像 HTML 的标记(这就是 JSX)
  3. export default App - 导出组件,让其他文件可以使用它

运行项目后,你会在浏览器中看到 "Hello World"。

# 2. JSX:在 JavaScript 中写 HTML

JSX 让你可以在 JavaScript 中编写类似 HTML 的代码。

# 基础示例

function Welcome() {
  return (
    <div>
      <h1>欢迎来到 React</h1>
      <p>这是一个简单的示例</p>
    </div>
  );
}

export default Welcome;

# 在 JSX 中使用 JavaScript

使用 {} 可以在 JSX 中嵌入 JavaScript 表达式:

function Greeting() {
  const name = '张三';
  const age = 25;
  const hobbies = ['阅读', '编程', '运动'];

  return (
    <div>
      {/* 使用变量 */}
      <h1>你好,{name}!</h1>
      
      {/* 使用表达式 */}
      <p>明年你将 {age + 1} 岁</p>
      
      {/* 使用方法 */}
      <p>你的名字有 {name.length} 个字</p>
      
      {/* 使用数组 */}
      <p>爱好:{hobbies.join('、')}</p>
    </div>
  );
}

export default Greeting;

保存文件后,浏览器会显示:

你好,张三!
明年你将 26 岁
你的名字有 2 个字
爱好:阅读、编程、运动

# JSX 的重要规则

// ❌ 错误:必须有一个根元素
function Wrong() {
  return (
    <h1>标题</h1>
    <p>段落</p>  // 错误!不能并列两个元素
  );
}

// ✅ 正确:用一个 div 包裹
function Correct1() {
  return (
    <div>
      <h1>标题</h1>
      <p>段落</p>
    </div>
  );
}

// ✅ 正确:使用 Fragment(不会产生额外的 DOM 节点)
function Correct2() {
  return (
    <>
      <h1>标题</h1>
      <p>段落</p>
    </>
  );
}

// ✅ 正确:标签必须闭合
function Tags() {
  return (
    <div>
      <img src="avatar.jpg" />  {/* 自闭合 */}
      <input type="text" />     {/* 自闭合 */}
      <br />                    {/* 自闭合 */}
    </div>
  );
}

// ⚠️ 注意:使用 className 而不是 class
function Styling() {
  return (
    <div className="container">  {/* ✅ className */}
      <h1 className="title">标题</h1>
    </div>
  );
}

# 3. Props:组件间传递数据

Props(properties 的缩写)是父组件传递给子组件的数据,就像函数的参数。

# 完整示例:用户卡片

// UserCard.jsx - 子组件
function UserCard(props) {
  return (
    <div className="card">
      <h2>{props.name}</h2>
      <p>年龄:{props.age}</p>
      <p>职业:{props.job}</p>
    </div>
  );
}

// App.jsx - 父组件
function App() {
  return (
    <div>
      <h1>员工列表</h1>
      <UserCard name="张三" age={28} job="前端工程师" />
      <UserCard name="李四" age={32} job="后端工程师" />
      <UserCard name="王五" age={25} job="设计师" />
    </div>
  );
}

export default App;

这个例子中:

  • UserCard 是一个可复用的组件
  • App 组件使用了 3 次 UserCard,每次传入不同的数据
  • Props 就像函数参数,让组件可以根据不同的数据显示不同的内容

# Props 解构(更简洁的写法)

// 不使用解构
function UserCard(props) {
  return <h2>{props.name}</h2>;
}

// ✅ 使用解构(推荐)
function UserCard({ name, age, job }) {
  return (
    <div className="card">
      <h2>{name}</h2>
      <p>年龄:{age}</p>
      <p>职业:{job}</p>
    </div>
  );
}

# Props 默认值

function Button({ text = '点击', color = 'blue' }) {
  return (
    <button style={{ backgroundColor: color }}>
      {text}
    </button>
  );
}

// 使用
function App() {
  return (
    <div>
      <Button />  {/* 显示:点击(蓝色) */}
      <Button text="提交" />  {/* 显示:提交(蓝色) */}
      <Button text="删除" color="red" />  {/* 显示:删除(红色) */}
    </div>
  );
}

# 特殊的 Props:children

children 是一个特殊的 prop,表示组件标签之间的内容:

// Card.jsx
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}

// App.jsx
function App() {
  return (
    <Card title="个人信息">
      <p>姓名:张三</p>
      <p>年龄:28</p>
      <button>编辑</button>
    </Card>
  );
}

export default App;

浏览器显示:

个人信息
姓名:张三
年龄:28
[编辑按钮]

重点理解:

  • Props 是只读的,子组件不能修改 props
  • Props 可以传递任何类型的数据:字符串、数字、对象、数组、函数等
  • Props 让组件变得可复用

# 4. State:组件的记忆

如果说 Props 是从外部传入的数据,那么 State 就是组件内部的数据,而且可以改变。

# 第一个有状态的组件:计数器

import { useState } from 'react';

function Counter() {
  // useState 返回两个值:
  // 1. count - 当前的状态值
  // 2. setCount - 更新状态的函数
  const [count, setCount] = useState(0);  // 0 是初始值

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
    </div>
  );
}

export default Counter;

运行流程:

  1. 组件首次渲染,count 的值是 0
  2. 点击按钮,执行 setCount(count + 1),即 setCount(1)
  3. React 重新渲染组件,这次 count 的值是 1
  4. 页面更新,显示"当前计数:1"

# 完整的计数器示例

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>计数器</h2>
      <p>当前计数:{count}</p>
      
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
      
      <button onClick={() => setCount(count - 1)}>
        减少
      </button>
      
      <button onClick={() => setCount(0)}>
        重置
      </button>
      
      <button onClick={() => setCount(count * 2)}>
        翻倍
      </button>
    </div>
  );
}

export default Counter;

# 不同类型的 State

import { useState } from 'react';

function StateExamples() {
  // 字符串状态
  const [name, setName] = useState('张三');
  
  // 布尔状态
  const [isVisible, setIsVisible] = useState(true);
  
  // 对象状态
  const [user, setUser] = useState({
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com'
  });
  
  // 数组状态
  const [items, setItems] = useState(['苹果', '香蕉', '橙子']);

  return (
    <div>
      {/* 字符串状态 */}
      <div>
        <p>姓名:{name}</p>
        <button onClick={() => setName('李四')}>
          改名为李四
        </button>
      </div>

      {/* 布尔状态 */}
      <div>
        <button onClick={() => setIsVisible(!isVisible)}>
          {isVisible ? '隐藏' : '显示'}
        </button>
        {isVisible && <p>我是可见的内容</p>}
      </div>

      {/* 对象状态 */}
      <div>
        <p>{user.name} - {user.age}岁</p>
        <button onClick={() => setUser({ ...user, age: user.age + 1 })}>
          年龄+1
        </button>
      </div>

      {/* 数组状态 */}
      <div>
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
        <button onClick={() => setItems([...items, '西瓜'])}>
          添加西瓜
        </button>
      </div>
    </div>
  );
}

export default StateExamples;

# State 更新的重要规则

import { useState } from 'react';

function StateRules() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: '张三', age: 25 });

  // ❌ 错误:直接修改 state
  const wrongWay = () => {
    count = count + 1;  // 不会触发重新渲染!
  };

  // ✅ 正确:使用 setter 函数
  const rightWay = () => {
    setCount(count + 1);
  };

  // ❌ 错误:直接修改对象
  const wrongObjectUpdate = () => {
    user.age = 26;  // 不会触发重新渲染!
    setUser(user);
  };

  // ✅ 正确:创建新对象
  const rightObjectUpdate = () => {
    setUser({ ...user, age: 26 });
  };

  // ✅ 函数式更新(推荐)
  const functionalUpdate = () => {
    setCount(prevCount => prevCount + 1);
  };

  return <div>{/* ... */}</div>;
}

# 5. 事件处理:响应用户操作

React 中的事件处理与 HTML 类似,但有一些差异。

# 基础点击事件

import { useState } from 'react';

function ClickExample() {
  const [message, setMessage] = useState('等待点击...');

  const handleClick = () => {
    setMessage('按钮被点击了!');
  };

  return (
    <div>
      <p>{message}</p>
      <button onClick={handleClick}>点击我</button>
    </div>
  );
}

export default ClickExample;

# 表单输入处理

import { useState } from 'react';

function FormExample() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();  // 阻止表单默认提交行为
    alert(`用户名:${username}\n密码:${password}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="请输入用户名"
        />
      </div>

      <div>
        <label>密码:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="请输入密码"
        />
      </div>

      <button type="submit">登录</button>

      <div>
        <p>当前输入:{username}</p>
        <p>密码长度:{password.length}</p>
      </div>
    </form>
  );
}

export default FormExample;

运行流程:

  1. 用户在输入框输入 "张"
  2. 触发 onChange 事件
  3. 执行 setUsername(e.target.value),即 setUsername('张')
  4. 组件重新渲染,输入框显示 "张"
  5. 下方实时显示"当前输入:张"

# 常见事件示例

import { useState } from 'react';

function EventExamples() {
  const [logs, setLogs] = useState([]);

  const addLog = (message) => {
    setLogs([...logs, `${new Date().toLocaleTimeString()} - ${message}`]);
  };

  return (
    <div>
      <h2>事件演示</h2>

      {/* 点击事件 */}
      <button onClick={() => addLog('按钮被点击')}>
        点击测试
      </button>

      {/* 双击事件 */}
      <button onDoubleClick={() => addLog('双击了按钮')}>
        双击测试
      </button>

      {/* 鼠标进入/离开 */}
      <div
        onMouseEnter={() => addLog('鼠标进入')}
        onMouseLeave={() => addLog('鼠标离开')}
        style={{ padding: '20px', background: '#f0f0f0', margin: '10px 0' }}
      >
        移动鼠标到这里
      </div>

      {/* 键盘事件 */}
      <input
        type="text"
        onKeyDown={(e) => addLog(`按下了 ${e.key} 键`)}
        placeholder="输入任意内容"
      />

      {/* 焦点事件 */}
      <input
        type="text"
        onFocus={() => addLog('输入框获得焦点')}
        onBlur={() => addLog('输入框失去焦点')}
        placeholder="点击获得焦点"
      />

      {/* 事件日志 */}
      <div style={{ marginTop: '20px', maxHeight: '200px', overflow: 'auto' }}>
        <h3>事件日志:</h3>
        <ul>
          {logs.map((log, index) => (
            <li key={index}>{log}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default EventExamples;

# 6. 条件渲染:根据条件显示不同内容

# 使用 if/else

import { useState } from 'react';

function LoginStatus() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // 方式1:提前 return
  if (isLoggedIn) {
    return (
      <div>
        <h2>欢迎回来!</h2>
        <button onClick={() => setIsLoggedIn(false)}>退出登录</button>
      </div>
    );
  }

  return (
    <div>
      <h2>请先登录</h2>
      <button onClick={() => setIsLoggedIn(true)}>登录</button>
    </div>
  );
}

export default LoginStatus;

# 使用三元运算符

import { useState } from 'react';

function LoginButton() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div>
      <h2>
        {isLoggedIn ? '欢迎回来!' : '请先登录'}
      </h2>
      
      <button onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? '退出登录' : '登录'}
      </button>
    </div>
  );
}

export default LoginButton;

# 使用 && 运算符

import { useState } from 'react';

function Notifications() {
  const [unreadCount, setUnreadCount] = useState(5);

  return (
    <div>
      <h2>通知中心</h2>
      
      {/* 只有当 unreadCount > 0 时才显示 */}
      {unreadCount > 0 && (
        <div style={{ color: 'red' }}>
          您有 {unreadCount} 条未读消息
        </div>
      )}

      {unreadCount === 0 && (
        <div style={{ color: 'green' }}>
          没有新消息
        </div>
      )}

      <button onClick={() => setUnreadCount(unreadCount + 1)}>
        新消息 +1
      </button>
      
      <button onClick={() => setUnreadCount(0)}>
        全部已读
      </button>
    </div>
  );
}

export default Notifications;

# 完整示例:用户权限

import { useState } from 'react';

function UserDashboard() {
  const [user, setUser] = useState(null);

  const loginAsUser = () => {
    setUser({ name: '张三', role: 'user' });
  };

  const loginAsAdmin = () => {
    setUser({ name: '管理员', role: 'admin' });
  };

  const logout = () => {
    setUser(null);
  };

  // 未登录
  if (!user) {
    return (
      <div>
        <h2>请选择登录方式</h2>
        <button onClick={loginAsUser}>普通用户登录</button>
        <button onClick={loginAsAdmin}>管理员登录</button>
      </div>
    );
  }

  // 已登录
  return (
    <div>
      <h2>欢迎,{user.name}</h2>
      
      {/* 所有用户都能看到 */}
      <div>
        <h3>个人中心</h3>
        <p>查看个人信息</p>
      </div>

      {/* 只有管理员能看到 */}
      {user.role === 'admin' && (
        <div style={{ background: '#ffe', padding: '10px' }}>
          <h3>管理员功能</h3>
          <p>用户管理</p>
          <p>系统设置</p>
        </div>
      )}

      {/* 只有普通用户能看到 */}
      {user.role === 'user' && (
        <div>
          <p>普通用户权限有限</p>
        </div>
      )}

      <button onClick={logout}>退出登录</button>
    </div>
  );
}

export default UserDashboard;

# 7. 列表渲染:显示多个相似的元素

# 基础列表渲染

function FruitList() {
  const fruits = ['苹果', '香蕉', '橙子', '西瓜'];

  return (
    <div>
      <h2>水果列表</h2>
      <ul>
        {fruits.map((fruit, index) => (
          <li key={index}>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

export default FruitList;

浏览器显示:

水果列表
• 苹果
• 香蕉
• 橙子
• 西瓜

# 对象数组渲染

function UserList() {
  const users = [
    { id: 1, name: '张三', age: 28, job: '工程师' },
    { id: 2, name: '李四', age: 32, job: '设计师' },
    { id: 3, name: '王五', age: 25, job: '产品经理' }
  ];

  return (
    <div>
      <h2>员工列表</h2>
      <table border="1">
        <thead>
          <tr>
            <th>姓名</th>
            <th>年龄</th>
            <th>职位</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.age}</td>
              <td>{user.job}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default UserList;

# 可交互的列表

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', completed: false },
    { id: 2, text: '写代码', completed: true },
    { id: 3, text: '看文档', completed: false }
  ]);

  // 切换完成状态
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };

  // 删除任务
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <h2>待办事项</h2>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map((todo) => (
          <li
            key={todo.id}
            style={{
              padding: '10px',
              marginBottom: '5px',
              background: '#f5f5f5',
              display: 'flex',
              alignItems: 'center',
              gap: '10px'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span
              style={{
                flex: 1,
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#999' : '#000'
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

运行流程:

  1. 初始显示 3 个任务,其中"写代码"已完成(有删除线)
  2. 点击"学习 React"的复选框:
    • 触发 toggleTodo(1)
    • 更新 todos 数组,将 id 为 1 的项的 completed 改为 true
    • 组件重新渲染,"学习 React"出现删除线
  3. 点击"删除"按钮:
    • 触发 deleteTodo(id)
    • 从 todos 数组中过滤掉该项
    • 组件重新渲染,该任务从列表中消失

# 为什么需要 key?

import { useState } from 'react';

function KeyImportance() {
  const [items, setItems] = useState(['A', 'B', 'C']);

  const shuffle = () => {
    const shuffled = [...items].sort(() => Math.random() - 0.5);
    setItems(shuffled);
  };

  return (
    <div>
      <button onClick={shuffle}>随机排序</button>
      
      {/* ❌ 不好:使用索引作为 key */}
      <div>
        <h3>使用索引作为 key(不推荐)</h3>
        <ul>
          {items.map((item, index) => (
            <li key={index}>
              {item}
              <input type="text" placeholder={`${item} 的输入框`} />
            </li>
          ))}
        </ul>
      </div>

      {/* ✅ 好:使用唯一标识作为 key */}
      <div>
        <h3>使用内容作为 key(推荐)</h3>
        <ul>
          {items.map((item) => (
            <li key={item}>
              {item}
              <input type="text" placeholder={`${item} 的输入框`} />
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default KeyImportance;

实验:

  1. 在两个列表的输入框中输入内容
  2. 点击"随机排序"按钮
  3. 观察:使用索引作为 key 的列表,输入框的内容会错位;使用内容作为 key 的列表,输入框会跟着正确的项移动

key 的规则:

  • key 必须在兄弟元素中唯一
  • key 应该稳定、可预测,不能随机生成
  • 通常使用数据的 id 作为 key
  • 只有在列表顺序永远不变时,才可以用索引作为 key

# 四、Hooks(钩子)

Hooks 是 React 16.8 引入的特性,让函数组件拥有类组件的能力。你可以把 Hooks 理解为给函数组件"增加超能力"的工具。

# 什么是 Hooks?

在没有 Hooks 之前,如果想在组件中使用状态或生命周期,必须使用 class 组件。Hooks 让函数组件也能做到这些事。

Hooks 的命名规则:

  • 所有 Hooks 都以 use 开头
  • 必须在组件顶层调用,不能在循环、条件或嵌套函数中调用

# useState - 状态管理

我们在前面已经见过 useState,现在深入了解它。

# 基础用法回顾

import { useState } from 'react';

function Counter() {
  // useState 返回一个数组:[状态值, 更新函数]
  const [count, setCount] = useState(0);  // 0 是初始值

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

export default Counter;

# 多个状态

一个组件可以使用多个 useState:

import { useState } from 'react';

function UserProfile() {
  const [name, setName] = useState('张三');
  const [age, setAge] = useState(25);
  const [email, setEmail] = useState('zhangsan@example.com');
  const [isEditing, setIsEditing] = useState(false);

  const handleSave = () => {
    setIsEditing(false);
    alert('保存成功!');
  };

  if (isEditing) {
    return (
      <div>
        <h2>编辑个人信息</h2>
        <div>
          <label>姓名:</label>
          <input value={name} onChange={(e) => setName(e.target.value)} />
        </div>
        <div>
          <label>年龄:</label>
          <input
            type="number"
            value={age}
            onChange={(e) => setAge(Number(e.target.value))}
          />
        </div>
        <div>
          <label>邮箱:</label>
          <input value={email} onChange={(e) => setEmail(e.target.value)} />
        </div>
        <button onClick={handleSave}>保存</button>
        <button onClick={() => setIsEditing(false)}>取消</button>
      </div>
    );
  }

  return (
    <div>
      <h2>个人信息</h2>
      <p>姓名:{name}</p>
      <p>年龄:{age}</p>
      <p>邮箱:{email}</p>
      <button onClick={() => setIsEditing(true)}>编辑</button>
    </div>
  );
}

export default UserProfile;

运行流程:

  1. 初始显示个人信息和"编辑"按钮
  2. 点击"编辑",setIsEditing(true),切换到编辑模式
  3. 修改输入框内容,实时更新对应的状态
  4. 点击"保存",显示提示并切换回查看模式

# 对象状态的正确更新

import { useState } from 'react';

function UserForm() {
  const [user, setUser] = useState({
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com',
    address: {
      city: '北京',
      district: '朝阳区'
    }
  });

  // ❌ 错误:直接修改对象
  const wrongUpdate = () => {
    user.age = 26;  // 不会触发重新渲染!
    setUser(user);
  };

  // ✅ 正确:创建新对象(浅拷贝)
  const updateAge = () => {
    setUser({ ...user, age: user.age + 1 });
  };

  // ✅ 更新嵌套对象
  const updateCity = (newCity) => {
    setUser({
      ...user,
      address: {
        ...user.address,
        city: newCity
      }
    });
  };

  // ✅ 更新单个字段(通用方法)
  const handleChange = (field, value) => {
    setUser({ ...user, [field]: value });
  };

  return (
    <div>
      <h2>用户信息</h2>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <p>邮箱:{user.email}</p>
      <p>地址:{user.address.city} {user.address.district}</p>

      <div style={{ marginTop: '20px' }}>
        <button onClick={updateAge}>年龄 +1</button>
        <button onClick={() => updateCity('上海')}>迁移到上海</button>
        <button onClick={() => handleChange('name', '李四')}>
          改名为李四
        </button>
      </div>
    </div>
  );
}

export default UserForm;

# 数组状态的更新

import { useState } from 'react';

function ShoppingList() {
  const [items, setItems] = useState(['苹果', '香蕉']);
  const [inputValue, setInputValue] = useState('');

  // 添加项
  const addItem = () => {
    if (inputValue.trim()) {
      setItems([...items, inputValue]);  // 创建新数组
      setInputValue('');
    }
  };

  // 删除项
  const removeItem = (index) => {
    setItems(items.filter((_, i) => i !== index));
  };

  // 更新项
  const updateItem = (index, newValue) => {
    const newItems = [...items];
    newItems[index] = newValue;
    setItems(newItems);
  };

  // 清空列表
  const clearAll = () => {
    setItems([]);
  };

  return (
    <div>
      <h2>购物清单</h2>

      <div>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addItem()}
          placeholder="添加商品"
        />
        <button onClick={addItem}>添加</button>
      </div>

      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {item}
            <button onClick={() => removeItem(index)}>删除</button>
          </li>
        ))}
      </ul>

      <p>共 {items.length} 件商品</p>
      <button onClick={clearAll}>清空</button>
    </div>
  );
}

export default ShoppingList;

# 函数式更新

当新状态依赖于旧状态时,使用函数式更新更安全:

import { useState } from 'react';

function UpdateComparison() {
  const [count, setCount] = useState(0);

  // 场景:连续多次更新
  const handleMultipleUpdates = () => {
    // ❌ 问题:可能不会按预期工作
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // 结果:count 只会 +1(不是 +3)
  };

  // ✅ 正确:使用函数式更新
  const handleMultipleUpdatesCorrect = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    // 结果:count 会 +3
  };

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={handleMultipleUpdates}>错误的多次更新</button>
      <button onClick={handleMultipleUpdatesCorrect}>正确的多次更新</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

export default UpdateComparison;

# useEffect - 副作用处理

useEffect 用于处理副作用,比如数据获取、订阅、手动修改 DOM 等。

# 什么是副作用?

在 React 中,副作用是指那些影响组件外部的操作:

  • 发送网络请求
  • 修改 DOM
  • 设置定时器
  • 订阅事件
  • 读写 localStorage

# 基础用法

import { useState, useEffect } from 'react';

function DocumentTitle() {
  const [count, setCount] = useState(0);

  // 每次组件渲染后执行
  useEffect(() => {
    // 副作用:修改页面标题
    document.title = `你点击了 ${count} 次`;
  });

  return (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
    </div>
  );
}

export default DocumentTitle;

运行流程:

  1. 组件首次渲染,count = 0
  2. 渲染完成后,执行 useEffect,页面标题变为"你点击了 0 次"
  3. 点击按钮,count = 1
  4. 组件重新渲染
  5. 渲染完成后,再次执行 useEffect,标题变为"你点击了 1 次"

# 依赖数组

import { useState, useEffect } from 'react';

function EffectDependencies() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三');

  // 情况1:没有依赖数组 - 每次渲染都执行
  useEffect(() => {
    console.log('每次渲染都执行');
  });

  // 情况2:空依赖数组 - 只在挂载时执行一次
  useEffect(() => {
    console.log('组件挂载了');
  }, []);

  // 情况3:有依赖项 - 依赖项变化时执行
  useEffect(() => {
    console.log('count 变化了,新值:', count);
  }, [count]);  // 只有 count 变化时才执行

  useEffect(() => {
    console.log('name 变化了,新值:', name);
  }, [name]);  // 只有 name 变化时才执行

  return (
    <div>
      <p>计数:{count}</p>
      <p>姓名:{name}</p>
      <button onClick={() => setCount(count + 1)}>增加计数</button>
      <button onClick={() => setName('李四')}>改名</button>
    </div>
  );
}

export default EffectDependencies;

实验:

  1. 组件加载时,控制台输出:
    • "每次渲染都执行"
    • "组件挂载了"
    • "count 变化了,新值:0"
    • "name 变化了,新值:张三"
  2. 点击"增加计数":
    • "每次渲染都执行"
    • "count 变化了,新值:1"
  3. 点击"改名":
    • "每次渲染都执行"
    • "name 变化了,新值:李四"

# 清理函数

有些副作用需要清理,比如定时器、事件监听等。

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    if (!isRunning) return;

    console.log('启动定时器');

    // 设置定时器
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // 清理函数:组件卸载或依赖项变化时执行
    return () => {
      console.log('清理定时器');
      clearInterval(timer);
    };
  }, [isRunning]);  // isRunning 变化时,会先清理旧定时器,再创建新的

  return (
    <div>
      <h2>计时器</h2>
      <p>已运行:{seconds} 秒</p>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? '暂停' : '开始'}
      </button>
      <button onClick={() => setSeconds(0)}>重置</button>
    </div>
  );
}

export default Timer;

运行流程:

  1. 点击"开始",isRunning 变为 true
  2. useEffect 执行,创建定时器
  3. 每秒更新 seconds
  4. 点击"暂停",isRunning 变为 false
  5. useEffect 的清理函数执行,清除定时器

# 数据获取示例

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 标记是否已取消请求
    let cancelled = false;

    // 获取数据
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const data = await response.json();

        // 如果组件已卸载,不更新状态
        if (!cancelled) {
          setUsers(data);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      }
    };

    fetchUsers();

    // 清理函数:组件卸载时标记为已取消
    return () => {
      cancelled = true;
    };
  }, []);  // 空数组表示只在挂载时获取一次

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

  if (error) {
    return <div>错误:{error}</div>;
  }

  return (
    <div>
      <h2>用户列表</h2>
      <ul>
        {users.slice(0, 5).map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

# 监听窗口大小

import { useState, useEffect } from 'react';

function WindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    // 事件处理函数
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // 添加事件监听
    window.addEventListener('resize', handleResize);

    console.log('添加了 resize 监听');

    // 清理函数:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
      console.log('移除了 resize 监听');
    };
  }, []);  // 空数组:只在挂载时添加,卸载时移除

  return (
    <div>
      <h2>窗口尺寸</h2>
      <p>宽度:{windowSize.width}px</p>
      <p>高度:{windowSize.height}px</p>
      <p style={{ color: '#666', fontSize: '14px' }}>
        调整浏览器窗口大小试试
      </p>
    </div>
  );
}

export default WindowSize;

# useRef - 引用 DOM 或保持值

useRef 有两个主要用途:

  1. 访问 DOM 元素
  2. 保存一个在组件生命周期中不变的值(修改它不会触发重新渲染)

# 访问 DOM 元素

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦到输入框
    inputRef.current.focus();
  }, []);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  const handleClear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <h2>输入框操作</h2>
      <input ref={inputRef} type="text" placeholder="自动聚焦" />
      <div>
        <button onClick={handleFocus}>聚焦</button>
        <button onClick={handleClear}>清空并聚焦</button>
      </div>
    </div>
  );
}

export default AutoFocusInput;

工作原理:

  1. const inputRef = useRef(null) 创建一个 ref 对象
  2. <input ref={inputRef} /> 将 DOM 元素赋值给 inputRef.current
  3. inputRef.current.focus() 直接操作 DOM 元素

# 视频播放器示例

import { useRef, useState } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const togglePlay = () => {
    if (isPlaying) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };

  const handleRestart = () => {
    videoRef.current.currentTime = 0;
    videoRef.current.play();
    setIsPlaying(true);
  };

  return (
    <div>
      <h2>视频播放器</h2>
      <video
        ref={videoRef}
        width="400"
        src="https://www.w3schools.com/html/mov_bbb.mp4"
      />
      <div>
        <button onClick={togglePlay}>
          {isPlaying ? '暂停' : '播放'}
        </button>
        <button onClick={handleRestart}>重新播放</button>
      </div>
    </div>
  );
}

export default VideoPlayer;

# 保持值(不触发重新渲染)

import { useState, useRef } from 'react';

function ClickCounter() {
  const [renderCount, setRenderCount] = useState(0);
  const clickCountRef = useRef(0);

  const handleClick = () => {
    clickCountRef.current += 1;
    console.log('总点击次数:', clickCountRef.current);
    // 注意:修改 ref 不会触发重新渲染
  };

  const handleRender = () => {
    setRenderCount(renderCount + 1);
    // 这会触发重新渲染
  };

  return (
    <div>
      <h2>点击计数器</h2>
      <p>组件渲染次数:{renderCount}</p>
      <p>总点击次数:{clickCountRef.current}</p>
      <button onClick={handleClick}>
        点击(不重新渲染,查看控制台)
      </button>
      <button onClick={handleRender}>触发渲染</button>
    </div>
  );
}

export default ClickCounter;

# 对比 useState 和 useRef

import { useState, useRef } from 'react';

function StateVsRef() {
  const [stateValue, setStateValue] = useState(0);
  const refValue = useRef(0);

  console.log('组件渲染了');

  return (
    <div>
      <h2>State vs Ref</h2>

      <div>
        <h3>useState</h3>
        <p>值:{stateValue}</p>
        <button onClick={() => setStateValue(stateValue + 1)}>
          增加(会重新渲染)
        </button>
      </div>

      <div>
        <h3>useRef</h3>
        <p>值:{refValue.current}</p>
        <button onClick={() => {
          refValue.current += 1;
          console.log('Ref 值:', refValue.current);
          alert(`Ref 值已更新为 ${refValue.current},但界面不会自动更新`);
        }}>
          增加(不会重新渲染)
        </button>
      </div>

      <button onClick={() => setStateValue(s => s + 0)}>
        强制重新渲染
      </button>
    </div>
  );
}

export default StateVsRef;

# useContext - 跨组件传递数据

useContext 用于在组件树中传递数据,避免层层传递 props(称为"props drilling")。

# 问题:层层传递 Props

// ❌ 繁琐的方式:层层传递
function App() {
  const [theme, setTheme] = useState('light');
  return <Parent theme={theme} />;
}

function Parent({ theme }) {
  return <Child theme={theme} />;
}

function Child({ theme }) {
  return <GrandChild theme={theme} />;
}

function GrandChild({ theme }) {
  return <div>当前主题:{theme}</div>;
}

# 解决方案:使用 Context

import { createContext, useContext, useState } from 'react';

// 1. 创建 Context
const ThemeContext = createContext('light');

// 2. Provider 组件
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <div>
        <h1>主题切换示例</h1>
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          切换主题
        </button>
        <Parent />
      </div>
    </ThemeContext.Provider>
  );
}

// 3. 中间组件不需要知道 theme
function Parent() {
  return (
    <div>
      <h2>Parent 组件</h2>
      <Child />
    </div>
  );
}

function Child() {
  return (
    <div>
      <h3>Child 组件</h3>
      <GrandChild />
    </div>
  );
}

// 4. 最深层组件直接使用 Context
function GrandChild() {
  const theme = useContext(ThemeContext);  // 获取主题

  return (
    <div
      style={{
        padding: '20px',
        background: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff'
      }}
    >
      当前主题:{theme}
    </div>
  );
}

export default App;

# 完整示例:用户信息共享

import { createContext, useContext, useState } from 'react';

// 创建 Context
const UserContext = createContext(null);

// 根组件
function App() {
  const [user, setUser] = useState(null);

  const login = (username) => {
    setUser({ name: username, role: 'user' });
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      <div>
        <Header />
        <Main />
        <Footer />
      </div>
    </UserContext.Provider>
  );
}

// 头部组件
function Header() {
  const { user, logout } = useContext(UserContext);

  return (
    <header style={{ background: '#f0f0f0', padding: '10px' }}>
      <h1>我的网站</h1>
      {user ? (
        <div>
          <span>欢迎,{user.name}</span>
          <button onClick={logout}>退出</button>
        </div>
      ) : (
        <span>未登录</span>
      )}
    </header>
  );
}

// 主要内容
function Main() {
  const { user, login } = useContext(UserContext);
  const [inputValue, setInputValue] = useState('');

  if (!user) {
    return (
      <main style={{ padding: '20px' }}>
        <h2>请先登录</h2>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入用户名"
        />
        <button onClick={() => login(inputValue)}>登录</button>
      </main>
    );
  }

  return (
    <main style={{ padding: '20px' }}>
      <h2>主要内容</h2>
      <p>你好,{user.name}!</p>
      <UserProfile />
    </main>
  );
}

// 用户资料组件
function UserProfile() {
  const { user } = useContext(UserContext);

  return (
    <div style={{ border: '1px solid #ddd', padding: '10px' }}>
      <h3>个人资料</h3>
      <p>用户名:{user.name}</p>
      <p>角色:{user.role}</p>
    </div>
  );
}

// 底部组件
function Footer() {
  return (
    <footer style={{ background: '#f0f0f0', padding: '10px', marginTop: '20px' }}>
      <p>© 2024 我的网站</p>
    </footer>
  );
}

export default App;

运行流程:

  1. 初始状态:Header 显示"未登录",Main 显示登录表单
  2. 输入用户名"张三",点击"登录"
  3. 调用 login('张三'),更新 user 状态
  4. Header 显示"欢迎,张三"和"退出"按钮
  5. Main 显示主要内容和用户资料
  6. Footer 始终显示,但它不需要知道用户信息

# useMemo - 缓存计算结果

useMemo 用于缓存计算结果,避免每次渲染都重新计算。

# 基础示例

import { useState, useMemo } from 'react';

function ExpensiveCalculation() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // 模拟耗时计算
  const expensiveValue = useMemo(() => {
    console.log('执行耗时计算...');
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += i;
    }
    return result + count;
  }, [count]);  // 只有 count 变化时才重新计算

  return (
    <div>
      <h2>useMemo 示例</h2>

      <div>
        <p>计数:{count}</p>
        <button onClick={() => setCount(count + 1)}>增加</button>
      </div>

      <div>
        <p>计算结果:{expensiveValue}</p>
      </div>

      <div>
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="输入文本(不会触发重新计算)"
        />
      </div>
    </div>
  );
}

export default ExpensiveCalculation;

观察:

  • 点击"增加"按钮:控制台输出"执行耗时计算..."(需要重新计算)
  • 在输入框输入文本:没有控制台输出(使用缓存的结果)

# 实用示例:过滤列表

import { useState, useMemo } from 'react';

function FilteredList() {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');

  const allUsers = [
    { id: 1, name: '张三', age: 28 },
    { id: 2, name: '李四', age: 32 },
    { id: 3, name: '王五', age: 25 },
    { id: 4, name: '赵六', age: 30 },
    { id: 5, name: '张小明', age: 22 }
  ];

  // 缓存过滤和排序结果
  const filteredAndSortedUsers = useMemo(() => {
    console.log('重新过滤和排序...');

    // 过滤
    const filtered = allUsers.filter(user =>
      user.name.includes(searchTerm)
    );

    // 排序
    const sorted = [...filtered].sort((a, b) =>
      sortOrder === 'asc' ? a.age - b.age : b.age - a.age
    );

    return sorted;
  }, [searchTerm, sortOrder]);  // 只有搜索词或排序方式变化时才重新计算

  return (
    <div>
      <h2>用户列表</h2>

      <div>
        <input
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="搜索姓名"
        />
        <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
          按年龄排序({sortOrder === 'asc' ? '升序' : '降序'})
        </button>
      </div>

      <ul>
        {filteredAndSortedUsers.map(user => (
          <li key={user.id}>
            {user.name} - {user.age}岁
          </li>
        ))}
      </ul>

      <p>找到 {filteredAndSortedUsers.length} 个结果</p>
    </div>
  );
}

export default FilteredList;

# useCallback - 缓存函数

useCallback 用于缓存函数,避免每次渲染都创建新函数。

# 基础示例

import { useState, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // 不使用 useCallback - 每次渲染都创建新函数
  const handleClickNormal = () => {
    console.log('点击了', count);
  };

  // 使用 useCallback - 只有依赖项变化时才创建新函数
  const handleClickMemo = useCallback(() => {
    console.log('点击了', count);
  }, [count]);

  return (
    <div>
      <h2>useCallback 示例</h2>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>

      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="输入文本"
      />

      <Child onClick={handleClickMemo} />
    </div>
  );
}

function Child({ onClick }) {
  console.log('Child 渲染了');
  return <button onClick={onClick}>子组件按钮</button>;
}

export default Parent;

# 实用示例:搜索功能

import { useState, useCallback, memo } from 'react';

function SearchApp() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // 模拟搜索 API
  const performSearch = useCallback((searchQuery) => {
    console.log('执行搜索:', searchQuery);

    // 模拟异步搜索
    setTimeout(() => {
      const mockResults = [
        'React 教程',
        'React Hooks',
        'React Router',
        'React Native'
      ].filter(item => item.toLowerCase().includes(searchQuery.toLowerCase()));

      setResults(mockResults);
    }, 300);
  }, []);  // 空依赖:函数永远不变

  return (
    <div>
      <h2>搜索功能</h2>
      <SearchInput onSearch={performSearch} />
      <SearchResults results={results} />
    </div>
  );
}

// 使用 memo 优化子组件
const SearchInput = memo(({ onSearch }) => {
  const [value, setValue] = useState('');

  console.log('SearchInput 渲染了');

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    onSearch(newValue);
  };

  return (
    <input
      value={value}
      onChange={handleChange}
      placeholder="输入搜索词"
    />
  );
});

const SearchResults = memo(({ results }) => {
  console.log('SearchResults 渲染了');

  return (
    <ul>
      {results.map((result, index) => (
        <li key={index}>{result}</li>
      ))}
    </ul>
  );
});

export default SearchApp;

# 自定义 Hooks

自定义 Hooks 让你可以提取组件逻辑,实现代码复用。

# 自定义 Hook:useLocalStorage

import { useState, useEffect } from 'react';

// 自定义 Hook:持久化状态到 localStorage
function useLocalStorage(key, initialValue) {
  // 从 localStorage 读取初始值
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // 值变化时保存到 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  }, [key, value]);

  return [value, setValue];
}

// 使用自定义 Hook
function App() {
  const [name, setName] = useLocalStorage('name', '');
  const [age, setAge] = useLocalStorage('age', 0);

  return (
    <div>
      <h2>个人信息(会保存到浏览器)</h2>

      <div>
        <label>姓名:</label>
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </div>

      <div>
        <label>年龄:</label>
        <input
          type="number"
          value={age}
          onChange={(e) => setAge(Number(e.target.value))}
        />
      </div>

      <p>刷新页面,数据仍然存在!</p>
    </div>
  );
}

export default App;

# 自定义 Hook:useToggle

import { useState } from 'react';

// 自定义 Hook:切换布尔值
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => setValue(!value);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

  return [value, { toggle, setTrue, setFalse }];
}

// 使用自定义 Hook
function App() {
  const [isVisible, { toggle, setTrue, setFalse }] = useToggle(false);
  const [isEnabled, toggleEnabled] = useToggle(true);

  return (
    <div>
      <h2>useToggle 示例</h2>

      <div>
        <button onClick={toggle}>切换显示</button>
        <button onClick={setTrue}>显示</button>
        <button onClick={setFalse}>隐藏</button>
        {isVisible && <p>我是可见的内容</p>}
      </div>

      <div>
        <button onClick={toggleEnabled.toggle}>
          {isEnabled ? '禁用' : '启用'}功能
        </button>
        <p>功能状态:{isEnabled ? '启用' : '禁用'}</p>
      </div>
    </div>
  );
}

export default App;

# 自定义 Hook:useFetch

import { useState, useEffect } from 'react';

// 自定义 Hook:数据获取
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(url);
        const json = await response.json();

        if (!cancelled) {
          setData(json);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

// 使用自定义 Hook
function UserList() {
  const { data, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/users'
  );

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;

  return (
    <div>
      <h2>用户列表</h2>
      <ul>
        {data?.slice(0, 5).map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

# Hooks 使用规则

  1. 只在顶层调用:不要在循环、条件或嵌套函数中调用
// ❌ 错误
function Bad() {
  if (condition) {
    const [state, setState] = useState(0);  // 不能在条件中
  }

  for (let i = 0; i < 10; i++) {
    useEffect(() => {});  // 不能在循环中
  }
}

// ✅ 正确
function Good() {
  const [state, setState] = useState(0);

  useEffect(() => {
    if (condition) {
      // 条件逻辑放在 Hook 内部
    }
  });
}
  1. 只在 React 函数中调用:只在函数组件或自定义 Hooks 中调用
// ❌ 错误
function normalFunction() {
  const [state, setState] = useState(0);  // 不能在普通函数中
}

// ✅ 正确
function MyComponent() {
  const [state, setState] = useState(0);  // 可以在组件中
}

function useCustomHook() {
  const [state, setState] = useState(0);  // 可以在自定义 Hook 中
}

# 五、实战示例:Todo 应用

import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, { 
        id: Date.now(), 
        text: input, 
        completed: false 
      }]);
      setInput('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  const remaining = todos.filter(t => !t.completed).length;

  return (
    <div className="todo-app">
      <h1>待办事项</h1>
      
      <div className="input-group">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="添加新任务..."
        />
        <button onClick={addTodo}>添加</button>
      </div>

      <ul className="todo-list">
        {todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      <div className="footer">
        还有 {remaining} 个任务未完成
      </div>
    </div>
  );
}

export default TodoApp;

配套的 CSS:

.todo-app {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
}

.input-group {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.input-group input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.input-group button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-list button {
  margin-left: auto;
  padding: 5px 10px;
  background: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.footer {
  margin-top: 20px;
  padding: 10px;
  background: #f8f9fa;
  text-align: center;
  border-radius: 4px;
}

# 六、最佳实践

# 1. 组件设计原则

  • 单一职责:每个组件只做一件事
  • 可复用性:设计通用的组件
  • Props 明确:清晰的接口定义
  • 保持简单:避免过度抽象
// ✅ 好的组件设计
function UserCard({ user, onEdit }) {
  return (
    <div className="card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.bio}</p>
      <button onClick={() => onEdit(user)}>编辑</button>
    </div>
  );
}

// ❌ 不好的设计(职责太多)
function UserCardWithEditModal({ user }) {
  const [showModal, setShowModal] = useState(false);
  // 混合了展示和编辑逻辑
}

# 2. 状态管理

  • 状态提升:共享状态放到公共父组件
  • 本地状态优先:不是所有状态都需要全局管理
  • 状态最小化:避免冗余状态
// 状态提升示例
function Parent() {
  const [selectedId, setSelectedId] = useState(null);
  
  return (
    <>
      <List items={items} onSelect={setSelectedId} />
      <Detail id={selectedId} />
    </>
  );
}

# 3. 性能优化

  • 避免不必要的渲染:使用 React.memo
  • 使用 key:列表渲染时提供稳定的 key
  • 懒加载:按需加载组件
  • 虚拟化长列表:使用 react-window
import { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  // 只有 data 变化时才重新渲染
  return <div>{/* 复杂渲染逻辑 */}</div>;
});

# 4. 代码组织

src/
├── components/        # 通用组件
│   ├── Button/
│   │   ├── Button.jsx
│   │   └── Button.css
│   └── Card/
├── features/          # 功能模块
│   ├── auth/
│   └── todos/
├── hooks/             # 自定义 Hooks
│   └── useLocalStorage.js
├── utils/             # 工具函数
└── App.jsx

# 七、常用工具和库

# 路由

npm install react-router-dom
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

# 状态管理

常用的状态管理库:

  • Redux Toolkit:复杂应用的状态管理
  • Zustand:轻量级状态管理
  • Jotai / Recoil:原子化状态管理

# UI 组件库

  • Ant Design:企业级 UI 组件库
  • Material-UI:Google Material Design
  • Chakra UI:现代化组件库
  • shadcn/ui:基于 Radix UI 的组件集合

# 开发工具

  • React DevTools:浏览器扩展,调试 React 应用
  • ESLint:代码质量检查
  • Prettier:代码格式化

# 八、学习资源

# 官方文档

  • React 官方文档 (opens new window)(推荐)
  • React 中文文档 (opens new window)

# 进阶主题

  • TypeScript 集成
  • 服务端渲染(Next.js)
  • 性能优化技巧
  • 测试(Jest、React Testing Library)
  • 状态管理模式

# 实践项目

建议从以下项目开始练习:

  1. Todo 应用(本文示例)
  2. 计算器
  3. 天气查询应用
  4. 博客系统
  5. 电商购物车

# 九、总结

React 的核心概念:

  1. 组件:构建 UI 的基本单元
  2. JSX:在 JavaScript 中编写标记
  3. Props:组件间传递数据
  4. State:组件内部状态
  5. Hooks:函数组件的能力扩展

开始使用 React 的步骤:

  1. 创建项目:npm create vite@latest my-app -- --template react
  2. 学习 JSX 和组件
  3. 掌握 useState 和 useEffect
  4. 实践小项目
  5. 深入学习其他 Hooks 和高级特性

React 强大的生态系统和活跃的社区使它成为构建现代 Web 应用的绝佳选择。持续实践和探索,你会发现 React 的更多可能性!

祝你变得更强!

编辑 (opens new window)
#React#前端框架
上次更新: 2025/12/25
Nuxt.js极简入门
UmiJS快速入门

← Nuxt.js极简入门 UmiJS快速入门→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code 最佳实践(个人版)
08-01
03
高扩展-弹性伸缩设计
06-05
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式