React 题目整理

3/14/2026 reactinterview

# 👋 React 题目整理

# React 生命周期

# 类组件

生命周期流程:挂载时 constructorrendercomponentDidMount;更新时 rendercomponentDidUpdate;卸载时 componentWillUnmount

触发重新渲染setStateprops 变化或父组件渲染都会触发 render(父组件更新会导致所有子组件重新渲染,可用 PureComponentshouldComponentUpdate 优化)。

// 类组件
import React, { Component } from 'react';

class LifecycleDemo extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    console.log('Component is mounted.');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Component updated. Previous count: ' + prevState.count);
  }

  componentWillUnmount() {
    console.log('Component will unmount.');
  }

  render() {
    return (
      ...
    );
  }
}

export default LifecycleDemo;

# 函数式组件

使用 React Hooks 来模拟类组件的生命周期:

import { useState, useEffect, useRef } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const isMounted = useRef(false);

  // componentDidMount
  useEffect(() => {
    console.log('组件挂载');
    isMounted.current = true;

    // componentWillUnmount
    return () => {
      console.log('组件卸载');
      isMounted.current = false;
    };
  }, []);

  // componentDidUpdate (特定状态 - count)
  useEffect(() => {
    if (isMounted.current) {
      console.log('count 更新了:', count);
    }
  }, [count]);

  // 通用的 componentDidUpdate
  useEffect(() => {
    if (isMounted.current) {
      console.log('组件更新了');
    }
  }); // 注意:没有依赖数组

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

# react 调用 setState 之后发生了什么

  1. React 调用 setState 之后,React 会将新的 state 和 props 传递给组件的 render 方法,然后 React 会生成新的 DOM 元素。
  2. React 会将新的 DOM 元素渲染到 DOM 中
  3. React 会将新的 DOM 元素与旧的 DOM 元素进行比较,并生成一个更新计划
  4. React 会将更新计划应用到旧的 DOM 元素上,从而实现更新。

# useState 快照及累加问题

# useState 快照是什么?

在 React 中,状态变量在每次渲染时都是 固定的快照。设置状态会触发重新渲染,在新渲染中 React 会提供新的状态值。

// 不断进行累加会出现的问题
function Counter() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    // 问题:使用当前渲染的快照值
    setCount(count + 1); // count 是渲染时的快照
    setCount(count + 1); // 这里 count 还是同一个值!
    // 结果:只增加 1,而不是 2
  };

  // 正确的方式:使用函数更新
  const handleIncrementCorrectly = () => {
    setCount((prevCount) => prevCount + 1);
    setCount((prevCount) => prevCount + 1);
    // 结果:增加 2
  };

  // 在闭包中的问题
  const handleAsyncIncrement = () => {
    setTimeout(() => {
      setCount(count + 1); // 这里使用的是创建闭包时的 count 值
    }, 1000);
  };

  // 正确的异步更新
  const handleAsyncIncrementCorrectly = () => {
    setTimeout(() => {
      setCount((prevCount) => prevCount + 1);
    }, 1000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>错误累加(只+1</button>
      <button onClick={handleIncrementCorrectly}>正确累加(+2</button>
      <button onClick={handleAsyncIncrement}>错误异步</button>
      <button onClick={handleAsyncIncrementCorrectly}>正确异步</button>
    </div>
  );
}

# useState 和 useReducer 的区别

# 基本概念对比

useState - 简单状态管理

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((prev) => prev + 1)}>+1 (函数式)</button>
    </div>
  );
}

useReducer - 复杂状态管理

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}
特性 useState useReducer
适用场景 简单状态、独立状态 复杂状态、关联状态、复杂逻辑
状态结构 单个状态值 多个相关联的状态对象
更新逻辑 直接设置或简单转换 通过 action 和 reducer 处理
可预测性 较低 较高(纯函数)
测试性 较难测试 容易测试(reducer 是纯函数)
代码组织 逻辑分散在组件中 逻辑集中在 reducer 中

# useState/useReducer 深度嵌套更新问题

useState 的陷阱:

function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    profile: { age: 25, address: { city: 'Beijing' } },
  });

  // 错误:直接修改嵌套对象
  const updateCity = (city: string) => {
    user.profile.address.city = city; // ❌ 直接修改
    setUser(user); // ❌ 不会触发重新渲染
  };

  // 正确:需要深度拷贝(代码冗长)
  const updateCityCorrectly = (city: string) => {
    setUser((prev) => ({
      ...prev,
      profile: {
        ...prev.profile,
        address: { ...prev.profile.address, city: city },
      },
    }));
  };
}

useReducer 的解决方案:

type UserAction = { type: 'UPDATE_CITY'; payload: string } | { type: 'UPDATE_AGE'; payload: number };

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case 'UPDATE_CITY':
      return {
        ...state,
        profile: {
          ...state.profile,
          address: { ...state.profile.address, city: action.payload },
        },
      };
    default:
      return state;
  }
}

// 使用:代码更清晰
const updateCity = (city: string) => dispatch({ type: 'UPDATE_CITY', payload: city });

# useMemo / useCallback / useEffect 三者区别

这三个 Hook 解决的问题完全不同,可以用一句话区分:

Hook 解决什么问题 执行时机 一句话概括
useEffect 副作用处理 渲染后 "渲染完做某事"(请求数据、修改 DOM)
useCallback 函数引用稳定 渲染时 "保持函数身份不变"(避免子组件重渲染)
useMemo 计算结果缓存 渲染时 "记住计算结果"(避免重复计算)

# 1️⃣ useEffect - 处理副作用

作用:在组件渲染之后执行副作用操作(API 请求、订阅、手动 DOM 操作)

// 每次 count 变化后,打印日志
useEffect(() => {
  console.log('count 变化了:', count);
}, [count]);

// 组件挂载时请求数据
useEffect(() => {
  fetchData();
}, []); // 空数组 = 只执行一次

关键点

  • 在渲染之后异步执行
  • 可以返回清理函数(用于取消订阅等)
  • 不阻塞渲染

# 2️⃣ useCallback - 缓存函数引用

作用:保持函数引用稳定,防止因函数重建导致子组件不必要的重渲染

// ❌ 每次渲染都创建新函数,ChildComponent 会重渲染
const handleClick = () => {
  setValue(value + 1);
};

// ✅ 函数引用不变,ChildComponent 不会重渲染(假设用了 React.memo)
const handleClick = useCallback(() => {
  setValue(value + 1);
}, [value]); // value 不变,函数引用就不变

使用场景:传递给经过 React.memo 优化的子组件

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

  // 不用 useCallback:Child 每次都重渲染
  // 用 useCallback:只在 count 变化时重渲染
  const handleClick = useCallback(() => {
    console.log('点击了,当前值:', count);
  }, [count]);

  return <Child onClick={handleClick} />;
}

# 3️⃣ useMemo - 缓存计算结果

作用:缓存昂贵的计算结果,避免每次渲染都重新计算

// ❌ 每次渲染都重新计算(即使 list 没变)
const filteredData = dataList.filter((item) => item.price > 100);

// ✅ 只在 dataList 变化时重新计算
const filteredData = useMemo(() => {
  console.log('执行过滤...');
  return dataList.filter((item) => item.price > 100);
}, [dataList]);

使用场景

  • 昂贵的计算(排序、过滤、复杂运算)
  • 保持引用稳定(避免子组件重渲染)
// 场景:避免因对象引用变化导致子组件重渲染
const style = useMemo(
  () => ({
    fontSize: '16px',
    color: theme.color,
  }),
  [theme.color]
); // theme.color 不变,style 对象引用就不变

# 快速记忆

// 渲染后做事(异步)
useEffect(() => {
  document.title = count;
}, [count]);

// 渲染时用(同步)
useCallback(() => {}, [deps]); // 缓存函数
useMemo(() => value, [deps]); // 缓存值

本质区别

  • useEffect = 副作用(与渲染无关)
  • useCallback = useMemo(fn, deps) 的语法糖(缓存函数)
  • useMemo = 计算缓存(性能优化)

# Virtual DOM 和 Diff 算法

# 什么是 Virtual DOM?

Virtual DOM 是真实 DOM 的 JavaScript 对象表示,React 用它来提高渲染性能。

// 真实 DOM
<div class="container">
  <h1>Hello</h1>
</div>

// Virtual DOM(JavaScript 对象)
{
  type: 'div',
  props: {
    className: 'container',
    children: {
      type: 'h1',
      props: {
        children: 'Hello'
      }
    }
  }
}

# Virtual DOM 的工作流程

  1. 创建:React 创建 Virtual DOM 树
  2. 对比:状态变化时,React 创建新的 Virtual DOM 树并与旧树对比(Diff 算法)
  3. 更新:计算出最小的差异,只更新需要变化的部分到真实 DOM

# React Diff 算法的三个策略

1. 只对同层级节点比较

// ❌ 不会跨层级比较
// 旧树
<div>
  <A />
</div>

// 新树
<div>
  <B />
  <A />
</div>
// React 会销毁 A,创建 B,再创建 A(不会复用 A)

// ✅ 正确做法:使用 key 标识节点

2. 不同类型元素直接替换

// 旧
<div>Hello</div>

// 新
<span>Hello</span>
// React 会销毁 div 及其子节点,创建新的 span

3. 通过 key 识别节点

// ❌ 没有 key,React 按索引比较
// 旧:[A, B, C]
// 新:[A, C, B]
// React 会保留 A,更新 B→C,更新 C→B(浪费性能)

// ✅ 使用 key
<ul>
  {items.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
// React 通过 key 精确找到对应节点,只移动位置

# 为什么 Virtual DOM 更快?

  • 减少 DOM 操作:批量更新,避免频繁操作真实 DOM
  • 跨浏览器兼容:抽象了浏览器差异
  • 性能优化:只更新变化的部分

# React 中 key 的作用

# key 是什么?

key 是 React 用来识别列表项的唯一标识符。

// ❌ 使用索引(不推荐)
{
  items.map((item, index) => <li key={index}>{item.name}</li>);
}

// ✅ 使用唯一 ID(推荐)
{
  items.map((item) => <li key={item.id}>{item.name}</li>);
}

# key 的作用原理

React 使用 key 来:

  1. 识别节点:判断节点是新增、移动还是删除
  2. 复用节点:避免不必要的销毁和重建
  3. 保持状态:确保组件状态正确关联
// 示例:key 对组件状态的影响
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', checked: false },
    { id: 2, text: '写代码', checked: true },
  ]);

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

# 常见错误

1. 使用索引作为 key

// ❌ 问题:列表顺序变化时,状态会错乱
{
  items.map((item, index) => <TodoItem key={index} item={item} />);
}

// 场景:删除第一项,第二项的 key 从 1 变成 0
// React 会复用第一个组件,状态混乱

2. key 重复

// ❌ 错误:key 必须唯一
{
  items.map((item) => (
    <div key={item.type}>{item.name}</div>
    // 如果多个 item 的 type 相同,key 就重复了
  ));
}

3. 随机生成 key

// ❌ 错误:每次渲染都变化
{
  items.map((item) => (
    <div key={Math.random()}>{item.name}</div>
    // 每次都创建新节点,失去 key 的意义
  ));
}

# 正确使用

// ✅ 推荐:使用稳定的唯一 ID
{
  users.map((user) => <UserCard key={user.id} user={user} />);
}

// ✅ 组合 key(没有唯一 ID 时)
{
  items.map((item, index) => <div key={`${item.type}-${index}`}>{item.name}</div>);
}

# React 性能优化方法

# 1. 使用 React.memo 避免不必要的重渲染

// ❌ 每次父组件渲染,子组件都渲染
function Child({ name }) {
  console.log('Child 渲染');
  return <div>{name}</div>;
}

// ✅ 使用 React.memo 进行浅比较
const Child = React.memo(({ name }) => {
  console.log('Child 渲染');
  return <div>{name}</div>;
});

// 自定义比较函数
const Child = React.memo(
  ({ name }) => {
    console.log('Child 渲染');
    return <div>{name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true 表示不需要重渲染
    return prevProps.name === nextProps.name;
  }
);

# 2. 使用 useMemo 缓存计算结果

function ProductList({ products, filter }) {
  // ❌ 每次渲染都重新计算
  const filtered = products.filter((p) => p.category === filter);

  // ✅ 只在 products 或 filter 变化时计算
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  return <div>{filtered.map(/* ... */)}</div>;
}

# 3. 使用 useCallback 缓存函数

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

  // ✅ 函数引用不变,Child 不会因父组件渲染而重渲染
  const handleClick = useCallback(() => {
    console.log('点击了');
  }, []); // 空依赖,函数永远不变

  return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => {
  console.log('Child 渲染');
  return <button onClick={onClick}>点击</button>;
});

# 4. 列表渲染使用 key

// ✅ 必须使用稳定的 key
{
  items.map((item) => <div key={item.id}>{item.name}</div>);
}

# 5. 避免不必要的状态

// ❌ 冗余状态:可以从 props 派生
function User({ user }) {
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(`${user.firstName} ${user.lastName}`);
  }, [user]);

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

// ✅ 直接计算
function User({ user }) {
  const fullName = `${user.firstName} ${user.lastName}`;
  return <div>{fullName}</div>;
}

# 6. 懒加载组件

import { lazy, Suspense } from 'react';

// ✅ 路由级别的代码分割
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

# 7. 虚拟滚动(长列表优化)

// 使用 react-window 或 react-virtualized
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  return (
    <FixedSizeList height={400} itemCount={items.length} itemSize={50} width="100%">
      {({ index, style }) => <div style={style}>{items[index].name}</div>}
    </FixedSizeList>
  );
}

# 性能优化总结

优化方法 使用场景 注意事项
React.memo 组件渲染昂贵,props 很少变化 只做浅比较,对象 props 要注意
useMemo 昂贵计算、保持引用稳定 不要过度使用,计算本身也有成本
useCallback 传递给优化过的子组件 配合 React.memo 使用
懒加载 路由、大组件 首屏加载性能提升明显
虚拟滚动 长列表(100+ 项) 简单列表没必要

# 受控组件 vs 非受控组件

# 核心区别

特性 受控组件 非受控组件
数据源 React state DOM
值更新 通过 setState 直接操作 DOM
实时验证 ✅ 支持 ❌ 困难
使用场景 表单验证、动态表单 简单表单、集成第三方库

# 受控组件

表单元素的值由 React state 控制。

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData); // 直接使用 state
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name} // ✅ 受控:值来自 state
        onChange={handleChange}
      />
      <input
        name="email"
        value={formData.email} // ✅ 受控
        onChange={handleChange}
      />
      <textarea
        name="message"
        value={formData.message} // ✅ 受控
        onChange={handleChange}
      />
      <button type="submit">提交</button>
    </form>
  );
}

优点

  • ✅ 即时验证
  • ✅ 条件禁用/启用输入
  • ✅ 强制输入格式
// 实时验证示例
function PasswordInput() {
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    setPassword(value);

    // 实时验证
    if (value.length < 8) {
      setError('密码至少8位');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input type="password" value={password} onChange={handleChange} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

# 非受控组件

表单元素的值由 DOM 自己管理,通过 ref 获取。

function ContactForm() {
  const nameRef = useRef();
  const emailRef = useRef();
  const messageRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    // 通过 ref 获取值
    console.log({
      name: nameRef.current.value,
      email: emailRef.current.value,
      message: messageRef.current.value,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={nameRef} // ✅ 非受控:通过 ref 操作
        defaultValue="默认值" // 注意:用 defaultValue
      />
      <input ref={emailRef} type="email" />
      <textarea ref={messageRef} defaultValue="" />
      <button type="submit">提交</button>
    </form>
  );
}

优点

  • ✅ 代码更简洁
  • ✅ 更少的 state
  • ✅ 集成第三方库(如日期选择器)
// 集成第三方库
function DatePicker() {
  const dateRef = useRef();

  const handleSubmit = () => {
    // 直接使用第三方库的 API
    const date = dateRef.current.getDate();
    console.log(date);
  };

  return (
    <div>
      <SomeThirdPartyDatePicker ref={dateRef} />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

# 如何选择?

// ✅ 使用受控组件的场景
- 需要实时验证
- 动态表单(字段数量变化)
- 需要条件禁用/启用
- 表单提交前需要所有数据

// ✅ 使用非受控组件的场景
- 简单表单(如登录框)
- 集成第三方库
- 不需要实时验证
- 性能敏感场景(大量表单项)

# React Context 的使用场景和原理

# 什么是 Context?

Context 提供了一种在组件树中跨层级传递数据的方式,避免了 props 逐层传递(props drilling)。

// ❌ Props Drilling:逐层传递
function App() {
  const user = { name: 'John' };
  return <Header user={user} />;
}

function Header({ user }) {
  return <Navbar user={user} />;
}

function Navbar({ user }) {
  return <UserMenu user={user} />;
}

function UserMenu({ user }) {
  return <span>{user.name}</span>; // 终于用到了
}

// ✅ 使用 Context
const UserContext = createContext();

function App() {
  const user = { name: 'John' };
  return (
    <UserContext.Provider value={user}>
      <Header />
    </UserContext.Provider>
  );
}

function UserMenu() {
  const user = useContext(UserContext);
  return <span>{user.name}</span>; // 直接获取
}

# Context 的基本使用

1. 创建 Context

import { createContext, useContext } from 'react';

// 创建 Context
const ThemeContext = createContext();

export default ThemeContext;

2. 提供 Context

import ThemeContext from './ThemeContext';

function App() {
  const [theme, setTheme] = useState('light');

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

3. 消费 Context

import { useContext } from 'react';
import ThemeContext from './ThemeContext';

function Header() {
  // 方式1:使用 useContext Hook
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <header className={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换主题</button>
    </header>
  );
}

// 方式2:使用 Consumer(类组件)
class Footer extends React.Component {
  render() {
    return <ThemeContext.Consumer>{({ theme }) => <footer className={theme}>Footer</footer>}</ThemeContext.Consumer>;
  }
}

# Context 的常见使用场景

1. 主题切换

const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState('light');

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

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

function Button() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button onClick={toggleTheme} style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      切换主题
    </button>
  );
}

2. 用户信息

const AuthContext = createContext();

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

  return <AuthContext.Provider value={{ user, setUser }}>{user ? <Dashboard /> : <Login />}</AuthContext.Provider>;
}

function Login() {
  const { setUser } = useContext(AuthContext);
  return <button onClick={() => setUser({ name: 'John' })}>登录</button>;
}

3. 国际化(i18n)

const I18nContext = createContext();

const translations = {
  en: { welcome: 'Welcome' },
  zh: { welcome: '欢迎' },
};

function App() {
  const [lang, setLang] = useState('zh');

  return (
    <I18nContext.Provider value={{ lang, translations }}>
      <Page />
    </I18nContext.Provider>
  );
}

# Context 的注意事项

1. Context 更新会导致所有消费者重渲染

// ❌ 问题:频繁更新导致性能问题
function App() {
  const [data, setData] = useState({});
  const [count, setCount] = useState(0);

  return (
    <DataContext.Provider value={{ data, setData }}>
      <CountContext.Provider value={{ count, setCount }}>
        <Child />
      </CountContext.Provider>
    </DataContext.Provider>
  );
}

// ✅ 解决:拆分 Context,按需更新
const DataContext = createContext();
const CountContext = createContext();

2. 默认值只在没有 Provider 时生效

const ThemeContext = createContext('light'); // 默认值

function App() {
  // 没有 Provider,使用默认值
  return <Header />;
}

function Header() {
  const theme = useContext(ThemeContext); // 'light'
  return <div className={theme}>Header</div>;
}

3. 性能优化

// ❌ 每次都创建新对象,导致消费者重渲染
<ThemeContext.Provider value={{ theme, setTheme }}>

// ✅ 使用 useMemo 稳定引用
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>

# Context vs Redux

特性 Context Redux
复杂度 简单 较复杂
学习曲线
状态管理 基础 强大(中间件、时间旅行)
性能 可能重渲染较多 细粒度控制
适用场景 小型应用、主题、用户信息 大型应用、复杂状态逻辑

选择建议

  • 🎯 简单的全局配置(主题、语言) → Context
  • 🎯 复杂的状态管理(多个状态、异步逻辑) → Redux / Zustand

# Redux/Mobx 的核心概念

Redux 和 MobX 是 React 生态中最流行的状态管理库,它们采用了不同的状态管理哲学

# Redux 核心概念

Redux 是一个可预测的状态容器,采用 单向数据流不可变数据

# 三大原则
  1. 单一数据源:整个应用的 state 存储在单一 store 的对象树中
  2. State 只读:修改 state 的唯一方式是触发 action
  3. 使用纯函数执行修改:通过 reducer 纯函数计算新 state
# 核心概念
// 1. Action - 描述发生了什么
const ADD_TODO = 'ADD_TODO';

const addTodo = (text) => ({
  type: ADD_TODO,
  payload: text,
});

// 2. Reducer - 纯函数,根据 action 计算新 state
const todosReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      // ✅ 不可变更新:返回新对象
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map((todo) => (todo.id === action.id ? { ...todo, completed: !todo.completed } : todo));
    default:
      return state;
  }
};

// 3. Store - 存储 state
import { createStore } from 'redux';

const store = createStore(todosReducer);

// 4. 使用
console.log(store.getState()); // 获取 state
store.dispatch(addTodo('学习 Redux')); // 触发 action
store.subscribe(() => console.log(store.getState())); // 监听变化
# Redux 数据流
┌─────────┐     ┌──────────┐     ┌─────────┐
│  View   │────▶│ Action   │────▶│ Store   │
└─────────┘     └──────────┘     └─────────┘
     ▲                                 │
     └───────── Reducer ◀───────────────┘
  1. View 触发 Action
  2. Store 接收 Action,交给 Reducer
  3. Reducer 返回新 State
  4. Store 通知 View 更新
# Redux Toolkit(现代 Redux)

Redux Toolkit(RTK)是官方推荐的 Redux 编写方式,简化了配置。

import { createSlice, configureStore } from '@reduxjs/toolkit';

// 1. 创建 slice(包含 reducer + actions)
const todoSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      // ✅ Immer 允许直接修改
      state.push({ id: Date.now(), text: action.payload, completed: false });
    },
    toggleTodo: (state, action) => {
      const todo = state.find((t) => t.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

// 2. 提取 actions
const { addTodo, toggleTodo } = todoSlice.actions;

// 3. 创建 store
const store = configureStore({
  reducer: {
    todos: todoSlice.reducer,
  },
});

// 4. 使用
store.dispatch(addTodo('学习 RTK'));
# Redux 异步处理(Redux Thunk)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 创建异步 thunk
const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await fetch('/api/todos');
  return response.json();
});

const todoSlice = createSlice({
  name: 'todos',
  initialState: { items: [], status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

// 在组件中使用
function TodoList() {
  const dispatch = useDispatch();
  const { items, status } = useSelector((state) => state.todos);

  useEffect(() => {
    dispatch(fetchTodos());
  }, [dispatch]);

  if (status === 'loading') return <div>加载中...</div>;
  return (
    <ul>
      {items.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

# MobX 核心

MobX 采用 响应式编程可变状态,通过装饰器或函数自动追踪依赖。

# 核心概念
  1. Observable State:可观察的状态
  2. Computed Values:派生值(自动更新)
  3. Actions:修改状态的方法
  4. Reactions:响应状态变化
// 1. 创建 Store
import { makeAutoObservable } from 'mobx';

class TodoStore {
  todos = [];

  constructor() {
    makeAutoObservable(this);
  }

  addTodo(text) {
    this.todos.push({ id: Date.now(), text });
  }
}

const todoStore = new TodoStore();
export default todoStore;

// 2. React 组件
import React from 'react';
import { observer } from 'mobx-react';
import todoStore from './store';

const App = observer(() => {
  const handleSubmit = (e) => {
    e.preventDefault();
    const input = e.target.elements.todo;
    todoStore.addTodo(input.value);
    input.value = '';
  };

  return (
    <div>
      <h1>MobX 待办</h1>

      <form onSubmit={handleSubmit}>
        <input name="todo" placeholder="添加任务" />
        <button type="submit">添加</button>
      </form>

      <ul>
        {todoStore.todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
});

export default App;

# Redux vs MobX 对比

特性 Redux MobX
理念 单向数据流、函数式编程 响应式编程、面向对象
状态可变性 不可变(immutable) 可变(mutable)
代码风格 冗长但规范 简洁灵活
学习曲线 陡峭 平缓
性能 需要手动优化 自动优化
调试工具 完善(Redux DevTools) 相对简单
类型支持 TypeScript 支持好 需要额外配置
样板代码

# 代码对比
// ========== Redux ==========
// action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';

// action creators
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });

// reducer
function todosReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case TOGGLE_TODO:
      return state.map((todo) => (todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo));
    default:
      return state;
  }
}

// store
const store = createStore(todosReducer);

// 组件中使用
function TodoList() {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  return (
    <>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => dispatch(toggleTodo(todo.id))}>Toggle</button>
        </div>
      ))}
      <button onClick={() => dispatch(addTodo('New Todo'))}>Add</button>
    </>
  );
}

// ========== MobX ==========
class TodoStore {
  todos = observable([]);

  addTodo = action((text) => {
    this.todos.push({ id: Date.now(), text, completed: false });
  });

  toggleTodo = action((id) => {
    const todo = this.todos.find((t) => t.id === id);
    if (todo) todo.completed = !todo.completed;
  });
}

// 组件中使用
const TodoList = observer(({ store }) => {
  return (
    <>
      {store.todos.map((todo) => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => store.toggleTodo(todo.id)}>Toggle</button>
        </div>
      ))}
      <button onClick={() => store.addTodo('New Todo')}>Add</button>
    </>
  );
});

# 如何选择?

选择 Redux 的场景:

✅ 大型应用,复杂的状态逻辑

✅ 需要可预测的状态变化

✅ 团队协作,需要规范化

✅ 需要时间旅行调试

✅ 状态变化需要记录和追溯

// Redux 典型场景:电商应用
const store = {
  cart: {
    items: [],
    total: 0,
    discount: 0,
  },
  products: {
    list: [],
    filters: { category: null, priceRange: null },
    sortBy: 'price',
  },
  user: {
    profile: null,
    orders: [],
    preferences: {},
  },
};

选择 MobX 的场景:

✅ 中小型应用

✅ 快速开发原型

✅ 团队更熟悉面向对象

✅ 不想写太多样板代码

✅ 性能敏感场景

// MobX 典型场景:表单应用
class FormStore {
  formData = observable({
    username: '',
    email: '',
    password: '',
  });

  errors = observable({});

  get isValid() {
    return this.formData.username && this.formData.email && this.formData.password;
  }

  validate = action(() => {
    this.errors = {};
    if (!this.formData.username) this.errors.username = '必填';
    if (!this.formData.email) this.errors.email = '必填';
  });
}

选择 Zustand(新兴方案):

Zustand 是一个更轻量、更简单的状态管理库。

import { create } from 'zustand';

const useStore = create((set) => ({
  todos: [],
  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, completed: false }],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)),
    })),
}));

// 组件中使用
function TodoList() {
  const { todos, addTodo, toggleTodo } = useStore();

  return (
    <>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggleTodo(todo.id)}>Toggle</button>
        </div>
      ))}
      <button onClick={() => addTodo('New')}>Add</button>
    </>
  );
}

现代状态管理推荐

  • 大型项目:Redux Toolkit(官方推荐)
  • 中小型项目:Zustand(简洁、轻量)
  • 快速原型:MobX(灵活、简单)
  • 简单全局状态:React Context(内置方案)

# ref 是什么?为什么需要 ref?

ref 是 React 提供的一种访问 DOM 元素或组件实例的方式。

React 是数据驱动的,通常通过 state 和 props 来控制 UI。但有些场景需要直接访问 DOM,比如:聚焦输入框、播放视频、测量元素尺寸等。这时就需要 ref。

# ref 的三种创建方式

1. useRef Hook(推荐)

import { useRef } from 'react';

function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    // 直接操作 DOM
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="点击按钮聚焦" />
      <button onClick={focusInput}>聚焦</button>
    </div>
  );
}

2. createRef(类组件)

class Form extends React.Component {
  inputRef = React.createRef<HTMLInputElement>();

  focusInput = () => {
    this.inputRef.current?.focus();
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} />
        <button onClick={this.focusInput}>聚焦</button>
      </div>
    );
  }
}

3. 回调 ref

function Form() {
  let inputRef: HTMLInputElement | null = null;

  const focusInput = () => {
    inputRef?.focus();
  };

  return (
    <div>
      <input ref={(node) => (inputRef = node)} />
      <button onClick={focusInput}>聚焦</button>
    </div>
  );
}

# ref 的常见使用场景

function Examples() {
  const inputRef = useRef<HTMLInputElement>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const divRef = useRef<HTMLDivElement>(null);

  // 1️⃣ 聚焦输入框
  const focus = () => inputRef.current?.focus();

  // 2️⃣ 控制视频播放
  const playVideo = () => videoRef.current?.play();
  const pauseVideo = () => videoRef.current?.pause();

  // 3️⃣ 获取元素尺寸
  const getSize = () => {
    const width = divRef.current?.offsetWidth;
    const height = divRef.current?.offsetHeight;
    console.log({ width, height });
  };

  // 4️⃣ 滚动到元素
  const scrollTo = () => {
    divRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focus}>聚焦</button>

      <video ref={videoRef} src="video.mp4" />
      <button onClick={playVideo}>播放</button>

      <div ref={divRef} style={{ height: '200px', overflow: 'auto' }}>
        内容区域
      </div>
      <button onClick={scrollTo}>滚动到位置</button>
    </div>
  );
}

# ref VS state

特性 ref state
更新 ref.current = xxx(立即) setState()(异步重渲染)
读取 ref.current 直接读取 state
触发渲染 ❌ 不会触发 ✅ 会触发
用途 存储不触发渲染的数据 存储触发渲染的 UI 数据
function Counter() {
  const [count, setCount] = useState(0); // 会触发渲染
  const renderCount = useRef(0); // 不会触发渲染

  // ❌ 错误:用 ref 存储会导致不同步
  const handleClick = () => {
    renderCount.current += 1; // 不会触发重渲染,UI 不更新
  };

  // ✅ 正确:用 state 存储需要渲染的数据
  const handleClick = () => {
    setCount(count + 1); // 会触发重渲染,UI 更新
  };

  // ✅ ref 适合存储不需要渲染的数据
  const logRenderCount = () => {
    renderCount.current += 1;
    console.log('渲染次数:', renderCount.current);
  };

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

# ref 的注意事项

// ❌ 错误:不要在渲染期间读取/写入 ref
function Component() {
  const ref = useRef(0);

  // 错误:渲染期间修改 ref
  ref.current = ref.current + 1;

  // ✅ 正确:在事件或 useEffect 中修改
  useEffect(() => {
    ref.current = ref.current + 1;
  }, []);

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

// ❌ 错误:ref 只能绑定到 DOM 元素
function Parent() {
  const ref = useRef(null);

  // 如果 Child 是函数组件,ref 无法传递
  return <Child ref={ref} />;
}

// ✅ 正确:使用 forwardRef
const Child = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

# React.forwardRef 是什么

forwardRef 是 React 提供的高阶组件,用于将 ref 从父组件传递到子组件的 DOM 元素。

为什么需要 forwardRef?

默认情况下,React 组件无法直接接收 ref(ref 只能绑定到 DOM 元素或类组件实例)。forwardRef 解决了这个问题。

# 基本用法

// ❌ 错误:ref 无法传递
const MyInput = ({ value }: { value: string }) => {
  return <input value={value} />;
};

// ✅ 正确:使用 forwardRef
const MyInput = React.forwardRef<HTMLInputElement, { value: string }>((props, ref) => {
  return <input ref={ref} value={props.value} />;
});

// 使用
const inputRef = useRef<HTMLInputElement>(null);

function App() {
  const handleClick = () => {
    inputRef.current?.focus(); // 可以访问 DOM
  };

  return (
    <div>
      <MyInput ref={inputRef} value="Hello" />
      <button onClick={handleClick}>聚焦</button>
    </div>
  );
}

# forwardRef + useImperativeHandle

将自定义方法暴露给父组件:

const MyInput = React.forwardRef<{ focus: () => void; reset: () => void }, { value: string }>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    reset: () => {
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    },
  }));

  return <input ref={inputRef} value={props.value} />;
});

// 父组件
function App() {
  const inputRef = useRef<{ focus: () => void; reset: () => void }>(null);

  return (
    <>
      <MyInput ref={inputRef} value="Hello" />
      <button onClick={() => inputRef.current?.focus()}>聚焦</button>
      <button onClick={() => inputRef.current?.reset()}>重置</button>
    </>
  );
}

# 常见使用场景

场景 示例
表单组件 Input、Select 组件
UI 组件库 Button、Modal 组件
动画库 暴露 play/pause 方法
第三方封装 封装原生 DOM 元素

# 注意事项

// ❌ 避免:过度使用 forwardRef
const Button = forwardRef<HTMLButtonElement>((props, ref) => {
  return <button ref={ref}>点击</button>;
});

// ✅ 推荐:仅在需要访问 DOM 时使用
const Button = ({ onClick }: { onClick: () => void }) => {
  return <button onClick={onClick}>点击</button>;
};