海川的日志

React 完全指南

作者:海川,发表于:2026年1月28日 03:00:00

从原生JS的痛点,到jQuery的改进,再到React的革新。理解技术演进的背景,掌握React的设计哲学、Hooks的应用逻辑、性能优化策略和10+实战模式

前言

很多开发者学 React 时,直接从”组件、Props、State”开始,但这样学习就像在雾中摸象——**为什么需要 React?为什么要这样设计?为什么需要各种 Hook?**都说不清楚。

本文采用技术演进路径,从原生 JavaScript 的痛点谈起,经历 jQuery 的改进,再到 React 的革新,层层递进。你会理解每个 React 特性出现的原因,而不是机械地学习语法。


一、技术演进史:为什么需要 React?

原生 HTML + CSS + JS 的黑暗时代

想象一个需求:用户点击按钮,计数器 +1,同时更新页面显示

<!-- 原生HTML -->
<button id="btn">Click me</button>
<div id="count">Count: 0</div>

<script>
  let count = 0;

  document.getElementById("btn").addEventListener("click", () => {
    count++;
    // 手动更新 DOM
    document.getElementById("count").textContent = `Count: ${count}`;
  });
</script>

看起来简单,但问题开始出现

<!-- 如果需求变复杂:多个按钮、多个状态、互相影响 -->
<button id="btn1">Count++</button>
<button id="btn2">Reset</button>
<button id="btn3">Add 10</button>

<div id="count">Count: 0</div>
<div id="status">Status: idle</div>
<div id="log">Logs:</div>

<script>
  let count = 0;
  let logs = [];

  // ❌ 问题 1:状态分散
  // count、logs 散落在全局作用域

  // ❌ 问题 2:DOM 更新繁琐
  // 每次状态改变都要手动更新多个 DOM 元素
  const updateUI = () => {
    document.getElementById("count").textContent = `Count: ${count}`;
    document.getElementById("log").textContent = `Logs: ${logs.join(", ")}`;
    document.getElementById("status").textContent =
      count > 10 ? "Status: high" : "Status: low";
  };

  // ❌ 问题 3:事件监听器混乱
  // 每个按钮都要单独绑定,逻辑散落各处
  document.getElementById("btn1").addEventListener("click", () => {
    count++;
    logs.push("increment");
    updateUI();
  });

  document.getElementById("btn2").addEventListener("click", () => {
    count = 0;
    logs = [];
    updateUI();
  });

  // ❌ 问题 4:代码维护困难
  // 不知道改哪个变量会影响哪些 DOM
  // 添加新功能要修改多处地方
  // 很容易出现状态不一致的 BUG
</script>

核心痛点

痛点问题
状态和 DOM 不同步状态改了,DOM 要手动更新;DOM 改了,状态要手动保存
代码散落各处同一个业务逻辑的代码分散在多个事件监听器中
维护困难不清楚改一个变量会影响哪些 DOM;添加功能要触及多个地方
重复代码多类似的功能要重复写很多遍
缺乏组织结构大项目就是一堆”意大利面条代码”(spaghetti code)

jQuery 时代:部分改进

jQuery 的出现(约 2006 年)改进了一些问题:

// jQuery 让 DOM 操作更简洁
$("#btn").click(function () {
  count++;
  $("#count").text("Count: " + count);
});

// 支持链式操作
$("#btn")
  .click(function () {
    count++;
  })
  .on("mouseenter", function () {
    $(this).addClass("hover");
  });

jQuery 解决的问题

✅ DOM 操作语法更简洁($("#id")
✅ 跨浏览器兼容
✅ 事件处理更统一

但 jQuery 仍然无法解决根本问题

// ❌ 状态和 DOM 仍然分离
let count = 0; // 状态在这里
$("#btn").click(() => {
  count++; // 改变状态
  $("#count").text("Count: " + count); // 手动更新 DOM
});

// 如果忘记更新 DOM,就会出现不同步的 BUG
// ❌ 问题依然存在:状态和 DOM 是两个独立的系统

jQuery 的局限

  • 还是命令式编程(告诉计算机怎么做
  • 开发者需要手动管理状态和 DOM 的同步
  • 当应用复杂时,代码仍然混乱

React 的革命:声明式 UI

React(约 2013 年)采取了完全不同的思路:

// React:让 UI 跟随状态自动更新
function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <div>Count: {count}</div>
    </div>
  );
}

关键转变

命令式(jQuery):我要找到 DOM,我要改它,我要更新它
声明式(React):当状态是 0 时,显示这样;当状态是 1 时,显示那样

React 的核心思想

UI = f(state)

意思是:UI 是状态的函数。状态改变 → React 自动重新渲染 UI。

彻底解决的问题

状态和 UI 同步 - 只改状态,UI 自动跟随
组件化 - 可以写可复用的组件
单向数据流 - 数据流清晰,更易调试
声明式语法 - 代码表达意图,更易理解
性能自动优化 - React 会自动找出哪些 DOM 需要更新


二、核心速览(60 秒)

React 的三层演进

阶段特点例子
原生 JS命令式,手动同步状态和 DOMelement.textContent = value
jQuery简化 DOM 操作,但仍命令式$("#id").text(value)
React声明式,UI 自动同步状态<div>{value}</div>

React 核心概念速查

概念作用为什么需要
JSX描述 UI 结构比字符串模板更直观
组件可复用的 UI 单元避免重复代码
Props父子通信(单向)组件需要参数化
State组件内部可变数据应对用户交互
Hooks函数式组件的状态和副作用避免类组件的复杂性

三、JSX 和组件:UI 的声明方式

为什么需要 JSX?

在没有 JSX 之前:

// ❌ 原生 JS 写法:字符串拼接(易出错、难维护)
const html = `
  <div class="container">
    <h1>${title}</h1>
    <p>${description}</p>
  </div>
`;

// ❌ 或者手动创建 DOM(冗长繁琐)
const div = document.createElement("div");
div.className = "container";
const h1 = document.createElement("h1");
h1.textContent = title;
div.appendChild(h1);

JSX 解决的问题

✅ 使用 HTML 语法来描述 UI(直观)
✅ 支持 JavaScript 表达式(灵活)
✅ 编译检查(防止错误)

// ✅ JSX 写法:清晰简洁
const element = (
  <div className="container">
    <h1>{title}</h1>
    <p>{description}</p>
  </div>
);

JSX 的本质

JSX 不是字符串,而是 JavaScript 语法糖

// JSX
const element = <div className="box">Hello {name}</div>;

// 编译后(实际运行的代码)
const element = React.createElement(
  "div",
  { className: "box" },
  "Hello ",
  name,
);

理解这一点很重要:JSX 的每个 {} 都是真正的 JavaScript 表达式。

// ✅ 可以使用任意 JS 表达式
const element = (
  <div>
    <h1>{2 + 2}</h1>
    <p>{isLoggedIn ? "Welcome" : "Please login"}</p>
    <button onClick={() => console.log("clicked")}>Click</button>
  </div>
);

组件:代码复用的基石

原生 JS 的困境

// ❌ 如果要多个计数器,怎么办?
// 方案 1:复制粘贴代码(维护地狱)
let count1 = 0;
let count2 = 0;
let count3 = 0;

document.getElementById("btn1").addEventListener("click", () => {
  count1++;
  document.getElementById("display1").textContent = count1;
});

document.getElementById("btn2").addEventListener("click", () => {
  count2++;
  document.getElementById("display2").textContent = count2;
});
// ... 如果要 100 个计数器,就要写 100 遍?

React 的解决方案:组件

// ✅ 定义一个可复用的组件
function Counter({ title }) {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <h2>{title}</h2>
      <button onClick={() => setCount(count + 1)}>+</button>
      <p>Count: {count}</p>
    </div>
  );
}

// 直接复用,无需重复代码
function App() {
  return (
    <div>
      <Counter title="Counter 1" />
      <Counter title="Counter 2" />
      <Counter title="Counter 3" />
    </div>
  );
}

组件的本质

组件 = 函数 + JSX + 状态管理

四、Props 和 State:数据流的两层

为什么需要分开 Props 和 State?

jQuery 时代的问题

// ❌ 数据和 DOM 混乱
function createButton(label, value) {
  // label 是配置,value 是状态
  // 两者混在一起,分不清
  const btn = $(`<button>${label}</button>`);
  btn.data("value", value); // 把数据藏在 DOM 里
  btn.click(() => {
    // 改了状态,DOM 不会自动更新
    value++;
  });
  return btn;
}

React 的清晰设计

// ✅ Props:配置信息(父组件→子组件,不可修改)
function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// ✅ State:可变数据(组件内部管理)
function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <p>Count: {count}</p>
    </div>
  );
}

Props 的单向流动

为什么 Props 不能从子组件修改?

// ❌ 为什么这样设计是错的?
function Child({ name }) {
  name = "Bob"; // 直接修改 Props

  // 问题 1:父组件不知道这个改变
  // 问题 2:其他使用这个组件的地方也会受到影响
  // 问题 3:数据流变得混乱,难以调试
}

// ✅ 正确做法:通过回调函数告诉父组件
function Child({ name, onNameChange }) {
  return <input value={name} onChange={(e) => onNameChange(e.target.value)} />;
}

function Parent() {
  const [name, setName] = React.useState("Alice");

  return <Child name={name} onNameChange={setName} />;
}

单向数据流的优势

✅ 数据流清晰:下一层组件需要什么,父组件显式提供
✅ 易于调试:改了什么,直接看父组件的 Props
✅ 防止 BUG:不会有意外的数据修改


State:为什么需要 setState?

初学者常犯的错误

function Counter() {
  let count = 0; // ❌ 普通变量

  return (
    <>
      <button
        onClick={() => {
          count++; // 改变了,但 UI 不会更新!
        }}
      >
        +
      </button>
      <p>Count: {count}</p>
    </>
  );
}

// 为什么?因为 React 不知道 count 改变了
// React 只在调用 setState 时才会重新渲染

React 的设计

function Counter() {
  const [count, setCount] = React.useState(0);
  // 为什么这样设计?

  return (
    <button
      onClick={() => {
        setCount(count + 1); // 通知 React:"状态改变了,需要重新渲染"
      }}
    >
      Count: {count}
    </button>
  );
}

setState 的两个作用

  1. 通知 React:状态改变了,需要重新渲染
  2. 批量更新:React 会合并多个 setState,避免频繁重渲染
// React 会将这三个 setState 合并为一次渲染
function handleClick() {
  setCount(count + 1);
  setTotal(total + 1);
  setLog([...log, "clicked"]);

  // React 不会渲染 3 次,而是渲染 1 次
  // 这样性能更好
}

五、为什么需要 Hooks?从类组件的困境说起

类组件时代的问题

React 最初使用类组件来管理状态:

// 类组件(ES6 之前的标准做法)
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  // ❌ 问题 1:必须绑定 this
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  // ❌ 问题 2:生命周期方法分散逻辑
  componentDidMount() {
    // 初始化逻辑
    const subscription = subscribe();
    this.subscription = subscription;
  }

  componentDidUpdate(prevProps) {
    // 更新逻辑,但和 componentDidMount 做的事重复了
    if (prevProps.userId !== this.props.userId) {
      const subscription = subscribe();
      this.subscription = subscription;
    }
  }

  componentWillUnmount() {
    // 清理逻辑
    this.subscription.unsubscribe();
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>+</button>
        <p>Count: {this.state.count}</p>
      </div>
    );
  }
}

类组件的五大困境

困境问题描述
this 绑定方法需要绑定 this,容易出错
逻辑分散相关逻辑被分散到不同的生命周期方法
代码重复mount 和 update 经常需要重复相同的逻辑
难以复用状态逻辑无法有效提取和复用
代码冗长即使简单功能也要写很多模板代码

Hooks:函数式的优雅解决方案

React 16.8(2019 年)推出 Hooks,彻底改变了这一切:

// ✅ 函数式组件 + Hooks:简洁优雅
function Counter() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    // 初始化 + 更新逻辑都在这里
    console.log("订阅数据");

    return () => {
      // 清理逻辑
      console.log("取消订阅");
    };
  }, []); // 依赖数组控制执行时机

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Hooks 的优势

代码简洁 - 70% 的代码量减少
逻辑组织 - 相关逻辑集中在一个 Hook 中
易于复用 - 可以提取自定义 Hooks
没有 this 问题 - 函数式编程避免 this 绑定
组合优于继承 - 通过 Hooks 组合实现复杂逻辑


六、useState:管理组件状态

useState 出现的原因

类组件的痛点

// ❌ 类组件:状态和方法混在一起
class Counter extends React.Component {
  state = { count: 0, name: "", items: [] };

  increment = () => this.setState({ count: this.state.count + 1 });
  setName = (name) => this.setState({ name });
  addItem = (item) => this.setState({ items: [...this.state.items, item] });

  // 问题:状态多了以后,很难追踪哪个方法改哪个状态
}

useState 的解决方案

// ✅ 每个状态独立管理
function Counter() {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState("");
  const [items, setItems] = React.useState([]);

  // 清晰明了:每个 useState 管理一个状态
}

为什么需要依赖项数组?

初学者困惑:为什么 count 改变了,setCount 就要重新执行?

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

  // ❌ 问题:如果没有依赖数组,这会无限循环
  React.useEffect(() => {
    setCount(count + 1); // 改变 count
  }); // 没有依赖数组 → 每次渲染都执行 → count 改变 → 重新渲染 → 无限循环

  // ✅ 解决:指定依赖数组
  React.useEffect(() => {
    // 只在挂载时执行一次
    setCount(count + 1);
  }, []); // 空数组 = 只执行一次
}

七、useEffect:处理副作用

什么是副作用?为什么需要特殊处理?

纯函数 - 没有副作用:

// ✅ 纯函数:相同输入,总是相同输出,无副作用
function add(a, b) {
  return a + b;
}

有副作用的函数 - 需要 useEffect 处理:

// ❌ 副作用:修改全局变量、发送网络请求、修改 DOM 等
function Component() {
  // 这些应该在 useEffect 里,而不是在 render 时执行
  const data = fetch("/api/data"); // 发送网络请求
  localStorage.setItem("count", count); // 修改本地存储
  console.log("Component rendered"); // 副作用
}

useEffect 的作用控制何时执行副作用

function Component() {
  const [count, setCount] = React.useState(0);

  // ✅ 只在挂载和卸载时执行一次
  React.useEffect(() => {
    const timer = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);

    return () => clearInterval(timer); // 清理
  }, []);

  // ✅ 每当 count 改变时执行
  React.useEffect(() => {
    console.log(`Count is now ${count}`);
  }, [count]);

  // ✅ 获取用户数据,当 userId 改变时重新获取
  React.useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [userId]);
}

useEffect 解决的根本问题

jQuery 时代:代码在任何时候都可能执行,很难控制
React + useEffect:明确指定副作用何时执行


八、useCallback 和 useMemo:为什么需要性能优化?

问题:函数引用改变导致的重渲染

没有 useCallback 时

function Parent() {
  const [count, setCount] = React.useState(0);

  // 每次 Parent 渲染,这个函数都被重新创建
  const handleClick = () => {
    console.log(count);
  };

  // 子组件认为 onClick 改变了(引用不同),会重新渲染
  return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => {
  console.log("Child rendered"); // 会不必要地重新渲染
  return <button onClick={onClick}>Click</button>;
});

useCallback 的目的保持函数引用不变

function Parent() {
  const [count, setCount] = React.useState(0);

  // ✅ 当依赖不变时,保持相同的函数引用
  const handleClick = React.useCallback(() => {
    console.log(count);
  }, [count]); // 只有当 count 改变时,才创建新函数

  // Child 的 onClick 引用没变,不会重新渲染
  return <Child onClick={handleClick} />;
}

为什么需要 useMemo?

没有 useMemo 时

function Component({ items }) {
  // 每次渲染都重新排序(即使 items 没变)
  const sortedItems = items.sort((a, b) => a - b);

  return <List items={sortedItems} />;
}

useMemo 的目的缓存昂贵的计算

function Component({ items }) {
  // ✅ 只有当 items 改变时,才重新计算
  const sortedItems = React.useMemo(() => {
    return items.sort((a, b) => a - b);
  }, [items]);

  return <List items={sortedItems} />;
}

九、useRef:访问 DOM 和保存值

useRef 出现的原因

什么时候需要访问 DOM?

// ❌ 在 React 中,应该尽量避免直接操作 DOM
// 但有些场景无法避免

function TextInput() {
  // 场景 1:焦点管理(无法用 React 实现)
  const handleFocus = () => {
    // 如何让 input 获得焦点?需要访问 DOM
  };

  // 场景 2:播放/暂停视频(需要调用 DOM API)
  const handlePlay = () => {
    // 如何播放视频?需要访问 video 元素
  };

  // 场景 3:获取 input 值(通常用 useState,但某些场景需要 ref)
  const handleSubmit = () => {
    // 如何获取 input 的值?
  };
}

useRef 的解决方案

function TextInput() {
  const inputRef = React.useRef(null);

  const handleFocus = () => {
    inputRef.current.focus(); // 访问真实的 DOM 元素
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </>
  );
}

useRef 的另一个用途:保存不需要重渲染的值

// ❌ 问题:使用普通变量
function Stopwatch() {
  let intervalId; // 每次渲染都重新创建

  const start = () => {
    intervalId = setInterval(() => {
      // ...
    }, 100);
  };

  const stop = () => {
    clearInterval(intervalId); // 可能是 undefined(因为 intervalId 被重置)
  };
}

// ✅ 解决:使用 useRef
function Stopwatch() {
  const intervalRef = React.useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => {
      // ...
    }, 100);
  };

  const stop = () => {
    clearInterval(intervalRef.current); // 总是能获取到正确的 ID
  };
}

useRef 的特点

✅ 返回的对象在组件整个生命周期内保持不变
✅ 修改 ref 不会触发重渲染
✅ 可以用来保存任何值


十、useContext:跨层级通信的必要性

Props Drilling 的困境

想象这样的组件树

App
  ├─ Header
  │   └─ Nav
  │       └─ UserMenu
  │           └─ Avatar(需要 theme)

如果 App 有一个 theme 状态,Avatar 需要使用它:

// ❌ Props Drilling:theme 要一层层向下传递
function App() {
  const [theme, setTheme] = React.useState("light");

  return <Header theme={theme} />;
}

function Header({ theme }) {
  return <Nav theme={theme} />; // 只是转发 Props
}

function Nav({ theme }) {
  return <UserMenu theme={theme} />; // 只是转发 Props
}

function UserMenu({ theme }) {
  return <Avatar theme={theme} />; // 只是转发 Props
}

function Avatar({ theme }) {
  return <div className={theme}>Avatar</div>;
}

问题

❌ 中间层(Header、Nav、UserMenu)不需要 theme,但要转发
❌ 如果增加更多全局状态(language、user),Props 会爆炸
❌ 代码冗余,维护困难


useContext 的优雅解决

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

// 2. 提供者:在顶层提供值
function App() {
  const [theme, setTheme] = React.useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
    </ThemeContext.Provider>
  );
}

// 3. 中间层不需要任何改变
function Header() {
  return <Nav />;
}

function Nav() {
  return <UserMenu />;
}

// 4. 需要的组件直接获取
function Avatar() {
  const { theme } = React.useContext(ThemeContext);
  return <div className={theme}>Avatar</div>;
}

useContext 解决的问题

✅ 避免 Props Drilling
✅ 全局共享数据(主题、语言、用户信息等)
✅ 中间层组件无需关心


十一、自定义 Hooks:逻辑复用的最高形式

为什么需要自定义 Hooks?

类组件时代,逻辑复用很困难

// ❌ 高阶组件(HOC)的方式很冗长
function withFetchData(Component) {
  return function WrappedComponent(props) {
    const [data, setData] = React.useState(null);
    const [loading, setLoading] = React.useState(true);

    React.useEffect(() => {
      fetch(props.url)
        .then((res) => res.json())
        .then((data) => {
          setData(data);
          setLoading(false);
        });
    }, [props.url]);

    return <Component data={data} loading={loading} {...props} />;
  };
}

function UserList(props) {
  return props.loading ? <div>Loading...</div> : <div>{props.data}</div>;
}

const UserListWithFetch = withFetchData(UserList);

自定义 Hooks 更简洁

// ✅ 自定义 Hook:只是一个函数
function useFetch(url) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
}

// 直接使用
function UserList({ url }) {
  const { data, loading } = useFetch(url);
  return loading ? <div>Loading...</div> : <div>{data}</div>;
}

自定义 Hooks 的优势

✅ 简洁清晰
✅ 易于测试
✅ 易于共享
✅ 组合灵活


常见自定义 Hooks

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

  React.useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, error, loading };
}

// useDebounce:防抖
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = React.useState(value);

  React.useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// useLocalStorage:本地存储
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = React.useState(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  const setValue = (value) => {
    setStoredValue(value);
    localStorage.setItem(key, JSON.stringify(value));
  };

  return [storedValue, setValue];
}

// usePrevious:保存前一个值
function usePrevious(value) {
  const ref = React.useRef();

  React.useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

十二、状态管理进阶:从 useState 到 useReducer


核心速览:Hooks 的演进树

useState(基础状态)
  ├─ 单个值:const [count, setCount] = useState(0)
  └─ 缺陷:多个相关状态容易混乱

useEffect(副作用管理)
  ├─ 替代生命周期方法
  └─ 依赖数组控制执行时机

useReducer(复杂状态)
  ├─ 当 useState 不够用时
  └─ 多个相关状态的首选

useCallback + useMemo(性能优化)
  ├─ 缓存函数引用和计算结果
  └─ 避免不必要的重渲染

useRef(DOM 访问和值保存)
  └─ 访问真实 DOM 或保存引用

useContext(跨层通信)
  └─ 避免 Props Drilling

当 useState 不够用时

当有多个相关的状态时,useState 会变得混乱:

// ❌ useState 管理多个相关状态
function TodoList() {
  const [todos, setTodos] = React.useState([]);
  const [filter, setFilter] = React.useState("all");
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [sortBy, setSortBy] = React.useState("date");

  // 问题:状态之间有逻辑关系
  // 改 todos 时,需要同时改 loading 和 error
  // 很容易遗漏某个状态

  const fetchTodos = async () => {
    setLoading(true); // 如果忘记这行,BUG 就出现了
    try {
      const data = await fetch("/api/todos");
      setTodos(data);
      setError(null);
    } catch (err) {
      setError(err);
      setLoading(false);
    }
    setLoading(false);
  };
}

useReducer 的解决方案

// ✅ useReducer:相关状态统一管理
function TodoList() {
  const [state, dispatch] = React.useReducer(todoReducer, {
    todos: [],
    filter: "all",
    loading: false,
    error: null,
    sortBy: "date",
  });

  const fetchTodos = async () => {
    dispatch({ type: "FETCH_START" });
    try {
      const data = await fetch("/api/todos");
      dispatch({ type: "FETCH_SUCCESS", payload: data });
    } catch (err) {
      dispatch({ type: "FETCH_ERROR", payload: err });
    }
  };
}

function todoReducer(state, action) {
  switch (action.type) {
    case "FETCH_START":
      return { ...state, loading: true, error: null };
    case "FETCH_SUCCESS":
      return {
        ...state,
        todos: action.payload,
        loading: false,
      };
    case "FETCH_ERROR":
      return { ...state, loading: false, error: action.payload };
    case "SET_FILTER":
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

useReducer 的优势

✅ 相关状态统一管理,一次 dispatch 更新多个状态
✅ 状态转移逻辑清晰(纯函数 reducer)
✅ 易于测试(reducer 是纯函数)
✅ 易于调试(每个 action 都能追踪)


十三、性能优化:为什么重渲染会成为问题?

React 的重渲染机制

React 何时重渲染?

function App() {
  const [count, setCount] = React.useState(0);

  // ❌ 每当 count 改变,整个 App 组件都会重新渲染
  // 包括下面的所有子组件
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent /> {/* 不需要重渲染,但会重新渲染 */}
      <HeavyList items={items} /> {/* 不需要重渲染,但会重新渲染 */}
    </div>
  );
}

为什么是问题?

function ExpensiveComponent() {
  console.log("ExpensiveComponent rendered"); // 每次都打印

  // 假设这是一个复杂的计算
  const result = fibonacci(40); // 耗时操作

  return <div>{result}</div>;
}

React.memo:防止不必要的重渲染

什么时候需要 React.memo?

// ❌ 没有 React.memo
const Header = ({ title }) => {
  console.log("Header rendered");
  return <h1>{title}</h1>;
};

function App() {
  const [count, setCount] = React.useState(0);

  return (
    <>
      <Header title="My App" /> {/* 即使 title 没变,也会重新渲染 */}
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  );
}

使用 React.memo

// ✅ React.memo:只有 props 改变才重渲染
const Header = React.memo(({ title }) => {
  console.log("Header rendered"); // 只在 title 改变时打印
  return <h1>{title}</h1>;
});

function App() {
  const [count, setCount] = React.useState(0);

  return (
    <>
      <Header title="My App" /> {/* title 没变,Header 不会重新渲染 */}
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  );
}

性能优化的完整方案

// ✅ React.memo + useCallback 的组合
function App() {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState("Alice");

  // ✅ useCallback:缓存函数引用
  const handleIncrement = React.useCallback(() => {
    setCount((c) => c + 1);
  }, []); // 没有依赖,函数永不改变

  return (
    <div>
      <Header name={name} />
      {/* 
        即使 count 改变,Header 的 props 没变
        加上 React.memo,Header 不会重新渲染
      */}
      <Button onClick={handleIncrement}>Count: {count}</Button>
    </div>
  );
}

const Header = React.memo(({ name }) => <h1>Hello {name}</h1>);

const Button = React.memo(({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
));

useMemo:缓存计算结果

// ❌ 没有 useMemo
function ProductList({ products, filter }) {
  // 每次渲染都重新排序和过滤(即使 products 没变)
  const filtered = products
    .filter((p) => p.category === filter)
    .sort((a, b) => a.price - b.price);

  return <List items={filtered} />;
}

// ✅ useMemo:缓存计算结果
function ProductList({ products, filter }) {
  const filtered = React.useMemo(() => {
    return products
      .filter((p) => p.category === filter)
      .sort((a, b) => a.price - b.price);
  }, [products, filter]); // 只有这两个改变才重新计算

  return <List items={filtered} />;
}

十四、10+ 实战模式

1. 受控表单 vs 非受控表单

// ❌ 非受控表单:DOM 保存状态,React 无法控制
function LoginForm() {
  const handleSubmit = (e) => {
    e.preventDefault();
    const email = document.getElementById("email").value; // 从 DOM 读取
    // 问题:React 不知道表单值
  };

  return (
    <form onSubmit={handleSubmit}>
      <input id="email" type="email" />
      <button>Login</button>
    </form>
  );
}

// ✅ 受控表单:React 保存状态
function LoginForm() {
  const [form, setForm] = React.useState({ email: "", password: "" });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // form 就是当前的表单数据
    console.log(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <button>Login</button>
    </form>
  );
}

2. 自动保存草稿

function Editor() {
  const [content, setContent] = React.useState(() => {
    // 从 localStorage 恢复
    const saved = localStorage.getItem("draft");
    return saved || "";
  });

  // 自动保存
  React.useEffect(() => {
    const timer = setTimeout(() => {
      localStorage.setItem("draft", content);
    }, 1000); // 1 秒无输入后保存

    return () => clearTimeout(timer);
  }, [content]);

  return (
    <textarea
      value={content}
      onChange={(e) => setContent(e.target.value)}
      placeholder="Start typing..."
    />
  );
}

3. 防抖搜索

function SearchUsers() {
  const [query, setQuery] = React.useState("");
  const [results, setResults] = React.useState([]);

  // 防抖:延迟 500ms 后执行搜索
  React.useEffect(() => {
    const timer = setTimeout(() => {
      if (query) {
        fetch(`/api/search?q=${query}`)
          .then((res) => res.json())
          .then(setResults);
      }
    }, 500);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

4. 数据获取和错误处理

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    let isMounted = true; // 防止内存泄漏

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) setUser(data);
      })
      .catch((err) => {
        if (isMounted) setError(err);
      })
      .finally(() => {
        if (isMounted) setLoading(false);
      });

    return () => {
      isMounted = false; // 清理:标记组件已卸载
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{user.name}</div>;
}

5. 模态框管理

function useModal(initialOpen = false) {
  const [isOpen, setIsOpen] = React.useState(initialOpen);

  return {
    isOpen,
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen((v) => !v),
  };
}

function App() {
  const modal = useModal();

  return (
    <>
      <button onClick={modal.open}>Open Modal</button>

      {modal.isOpen && (
        <div className="modal-overlay" onClick={modal.close}>
          <div className="modal-content" onClick={(e) => e.stopPropagation()}>
            <h2>Modal Title</h2>
            <button onClick={modal.close}>Close</button>
          </div>
        </div>
      )}
    </>
  );
}

6. 分页

function usePagination(items, pageSize = 10) {
  const [currentPage, setCurrentPage] = React.useState(1);

  const totalPages = Math.ceil(items.length / pageSize);
  const startIndex = (currentPage - 1) * pageSize;
  const currentItems = items.slice(startIndex, startIndex + pageSize);

  return {
    currentPage,
    totalPages,
    currentItems,
    goToPage: (page) => {
      const validPage = Math.max(1, Math.min(page, totalPages));
      setCurrentPage(validPage);
    },
  };
}

function ItemList({ items }) {
  const { currentItems, currentPage, totalPages, goToPage } = usePagination(
    items,
    5,
  );

  return (
    <>
      <ul>
        {currentItems.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <div>
        <button onClick={() => goToPage(currentPage - 1)}>Prev</button>
        <span>
          {currentPage} / {totalPages}
        </span>
        <button onClick={() => goToPage(currentPage + 1)}>Next</button>
      </div>
    </>
  );
}

7. 全局主题管理

const ThemeContext = React.createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState(() => {
    return localStorage.getItem("theme") || "light";
  });

  React.useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("theme", theme);
  }, [theme]);

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  return React.useContext(ThemeContext);
}

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header>
      <h1>My App ({theme})</h1>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  );
}

8. 列表过滤和排序

function ProductList({ products }) {
  const [filter, setFilter] = React.useState("");
  const [sortBy, setSortBy] = React.useState("name");

  const filtered = React.useMemo(() => {
    return products
      .filter((p) => p.name.toLowerCase().includes(filter.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === "name") return a.name.localeCompare(b.name);
        if (sortBy === "price") return a.price - b.price;
        return 0;
      });
  }, [products, filter, sortBy]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter..."
      />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>

      <ul>
        {filtered.map((p) => (
          <li key={p.id}>
            {p.name} - ${p.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

9. 无限滚动

function useInfiniteScroll(callback) {
  const observerTarget = React.useRef(null);

  React.useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        callback();
      }
    });

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => observer.disconnect();
  }, [callback]);

  return observerTarget;
}

function InfiniteList() {
  const [items, setItems] = React.useState([]);
  const [page, setPage] = React.useState(1);
  const [hasMore, setHasMore] = React.useState(true);

  const loadMore = React.useCallback(() => {
    if (!hasMore) return;

    fetch(`/api/items?page=${page}`)
      .then((res) => res.json())
      .then((data) => {
        setItems((prev) => [...prev, ...data.items]);
        setHasMore(data.hasMore);
        setPage((p) => p + 1);
      });
  }, [page, hasMore]);

  const targetRef = useInfiniteScroll(loadMore);

  return (
    <>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <div ref={targetRef}>Loading more...</div>
    </>
  );
}

10. 错误边界和回退 UI

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 可以在这里记录错误
    console.error("Error caught:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
        </div>
      );
    }

    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

11. 组件懒加载

const HeavyComponent = React.lazy(() => import("./HeavyComponent"));

function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </React.Suspense>
  );
}

十五、常见陷阱和解决方案

❌ 陷阱 1:闭包陷阱

// ❌ 错误:count 总是 0
function Component() {
  const [count, setCount] = React.useState(0);

  const handleClick = React.useCallback(() => {
    console.log(count); // 总是 0
  }, []); // 缺少 count 依赖

  return (
    <>
      <button onClick={handleClick}>Log Count</button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </>
  );
}

// ✅ 正确
const handleClick = React.useCallback(() => {
  console.log(count);
}, [count]); // 包含 count

❌ 陷阱 2:直接修改 State

// ❌ 错误
const [user, setUser] = React.useState({ name: "Alice" });
user.name = "Bob"; // 不会触发重渲染

// ✅ 正确
setUser({ ...user, name: "Bob" });

// ❌ 错误:修改数组
const [items, setItems] = React.useState([1, 2, 3]);
items.push(4); // 不会触发重渲染

// ✅ 正确
setItems([...items, 4]);

❌ 陷阱 3:忘记依赖数组

// ❌ 无限循环
function Component() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    setCount(count + 1); // count 改变 → effect 重新运行 → count 改变 → ...
  }); // 没有依赖数组
}

// ✅ 正确
React.useEffect(() => {
  setCount(count + 1);
}, []); // 只运行一次

❌ 陷阱 4:列表 key 错误

// ❌ 错误:用 index 作为 key
items.map((item, index) => <li key={index}>{item}</li>);

// 问题:当列表顺序改变时,React 会错误地复用 DOM

// ✅ 正确:用唯一标识符
items.map((item) => <li key={item.id}>{item}</li>);

❌ 陷阱 5:在条件中调用 Hook

// ❌ 错误
function Component({ show }) {
  if (show) {
    const [count, setCount] = React.useState(0); // Hook 顺序改变
  }
}

// ✅ 正确
function Component({ show }) {
  const [count, setCount] = React.useState(0);
  // 在条件中使用 count,不是在条件中调用 Hook
  if (show) {
    console.log(count);
  }
}

❌ 陷阱 6:忘记清理副作用

// ❌ 错误:内存泄漏
function Component() {
  React.useEffect(() => {
    const timer = setInterval(() => {
      console.log("tick");
    }, 1000);
    // 忘记清理 timer
  }, []);
}

// ✅ 正确
React.useEffect(() => {
  const timer = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => clearInterval(timer); // 清理
}, []);

十六、最佳实践总结

1. 状态管理原则

// ✅ 原则 1:尽可能将状态放在低处
function Parent() {
  return (
    <div>
      <Child1 /> {/* Child1 有自己的状态 */}
      <Child2 /> {/* Child2 有自己的状态 */}
    </div>
  );
}

// ❌ 反面:将所有状态都放在顶层(Props Drilling)
function Parent() {
  const [state1, setState1] = React.useState(0);
  const [state2, setState2] = React.useState(0);

  return (
    <div>
      <Child1 state={state1} setState={setState1} />
      <Child2 state={state2} setState={setState2} />
    </div>
  );
}

2. 使用自定义 Hooks 提取逻辑

// ✅ 将相关逻辑提取到自定义 Hook
function useUserData(userId) {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);

  return { user, loading, error };
}

// 组件保持简洁
function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{user.name}</div>;
}

3. 合理使用 Context

// ✅ Context 适合:全局配置、主题、用户信息
const UserContext = React.createContext();

function App() {
  const [user, setUser] = React.useState(null);

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

// ❌ Context 不适合:频繁变化的状态(如表单输入)
// 因为任何改变都会导致所有消费者重渲染

4. 避免过度优化

// ❌ 过度优化:为不必要的东西用 useMemo
const name = React.useMemo(() => user.name, [user]);

// ✅ 适度优化:只优化昂贵的计算
const sortedItems = React.useMemo(() => {
  return items.sort((a, b) => a.priority - b.priority);
}, [items]);

5. Props 类型检查(TypeScript 或 PropTypes)

// 使用 TypeScript
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

十七、总结:React 学习路径

核心概念回顾

概念为什么需要何时使用
JSX声明式描述 UI所有组件
组件代码复用拆分 UI
Props参数化组件父→子通信
State响应用户交互可变数据
useState管理简单状态单个或少数独立状态
useEffect控制副作用执行时机所有副作用
useCallback/useMemo避免不必要重渲染性能优化
useContext避免 Props Drilling全局状态
useReducer管理复杂状态逻辑多个相关状态
useRef访问 DOM 和保存值DOM 操作、引用保存

技术演进的完整链条

原生 JavaScript(命令式)
  ↓ 问题:状态和 DOM 分离,维护困难
jQuery(改进的 DOM 操作)
  ↓ 问题:仍然手动同步状态和 DOM
React(声明式,UI 自动同步状态)
  ├─ JSX:直观的 UI 描述
  ├─ 组件:可复用的 UI 单元
  ├─ Props + State:数据流清晰
  └─ Hooks:函数式的优雅方案
      ├─ useState:基础状态
      ├─ useEffect:副作用管理
      ├─ useCallback/useMemo:性能优化
      ├─ useContext:跨层通信
      └─ useReducer:复杂状态

学习建议

第一阶段(基础):理解为什么需要 React,掌握 JSX、组件、Props、State

第二阶段(进阶):深入理解每个 Hook 出现的背景,学会自定义 Hooks

第三阶段(高阶):性能优化、状态管理模式、实战项目开发


常见错误 Top 10

  1. ❌ 直接修改 State → ✅ 用 setState
  2. ❌ 用 index 作为列表 key → ✅ 用唯一标识符
  3. ❌ 在条件中调用 Hook → ✅ 在顶层调用
  4. ❌ 缺少 useEffect 依赖 → ✅ 包含所有使用的外层变量
  5. ❌ 对象/数组作为依赖 → ✅ 用 useMemo 缓存
  6. ❌ 忘记清理副作用 → ✅ 在 useEffect 中 return 清理函数
  7. ❌ Props Drilling → ✅ 用 useContext
  8. ❌ 过度优化 → ✅ 只优化有性能问题的地方
  9. ❌ 在 useCallback 中忽视依赖 → ✅ 包含所有使用的外层变量
  10. ❌ 异步操作在 useEffect 外 → ✅ 放在 useEffect 中

快速检查清单

编写 React 组件时,问自己:

// 状态管理
□ 状态放在合适的位置了吗?
□ 用了正确的状态管理方式(useState/useReducer)?
□ 有没有直接修改 State?

// 副作用
□ 副作用都在 useEffect 中吗?
□ 依赖数组正确吗?
□ 有没有忘记清理?

// 性能
□ 有没有不必要的重渲染?
□ 列表有正确的 key 吗?
□ 有没有过度优化?

// 数据流
□ 父子通信清晰吗?
□ 有没有 Props Drilling?
□ 用了合适的通信方式(Props/回调/Context)?

// 错误处理
□ 有没有处理加载状态?
□ 有没有处理错误状态?
□ 有没有处理边界情况?

后记

React 的强大不在于语法多复杂,而在于它提供了一套清晰的思想体系

  1. 声明式编程 - 描述 UI 应该是什么,而不是如何改变
  2. 单向数据流 - 数据从上到下,事件从下到上
  3. 组件化 - UI 分解为独立、可复用的单元
  4. 函数式编程 - Hooks 让副作用和状态管理变得优雅

当你遵循这些原则时,代码会自然而然地变得清晰、可维护、高效。

现在,你已经理解了 React 背后的为什么,而不仅仅是怎么用。Go build something amazing! 🚀