Skip to content

React 学习笔记

一、描述用户界面

组件基础规则

  1. 组件命名:必须以大写字母开头
  2. 纯函数原则:保持组件纯净,相同输入产生相同输出
  3. JSX 语法:标签与 return 不在同一行时,必须用括号包裹
  4. 组件嵌套:永远不要在组件内部定义其他组件
    • ❌ 错误:嵌套定义会导致每次渲染创建不同的函数,React 会重置所有状态
    • ✅ 正确:在顶层定义每个组件

JSX 规则

  • 属性命名:使用驼峰式 camelCase,包括内联 style 属性
  • 大括号使用
    • 作为 JSX 标签内的文本内容
    • 作为紧跟在 = 之后的属性值
  • 对象语法 只是 JSX 中的对象字面量

Props 和 Children

  • props 是组件的唯一参数,可指定默认值
  • 内容嵌套在 JSX 标签中时,父组件通过 children prop 接收
  • Props 不可变性:不应直接修改 props

列表渲染

推荐方法

  • filter() - 过滤数组
  • map() - 转换数组
  • concat() / [...] - 合并数组
  • slice() - 复制数组

Key 的使用

jsx
// 需要 key 但不能用 <></> 时使用 Fragment
<Fragment key={person.id}>
  <td>{person.name}</td>
</Fragment>
  • 如果没有指定 key,React 默认使用 index
  • Key 帮助 React 识别哪些项发生了变化

二、添加交互性

事件处理命名规范

  • 事件处理函数:命名为 handle + 事件名,如 handleClick
  • 事件处理 Props:以 on 开头 + 大写字母,如 onClick

事件传播

所有事件在 React 中都会传播,除了 onScroll(仅作用于附加的 JSX 标签)

组件内局部变量的问题

  1. 局部变量不会在渲染之间持续存在
  2. 对局部变量的更改不会触发渲染

三、状态管理核心概念

React 更新三步骤:Trigger → Render → Commit

React 快照(Snapshot)

React 快照是组件在特定时间点的状态,包括 props 和 state。

关键特性

  • 每个渲染的状态值都是固定的
  • 状态变量的值在渲染中永远不会改变
  • 设置 state 会请求一个新的渲染
  • React 将状态存储在组件外部
  • 每个渲染都有自己的事件处理程序
  • 过去创建的事件处理程序持有创建时的状态值

批处理(Batching)

React 会等到事件处理程序中的所有代码都运行完毕后,才处理状态更新。

核心原则

  • 设置状态不会更改当前渲染中的变量,只会请求新渲染
  • React 在事件处理程序完成后批量处理状态更新
  • 在一个事件中多次更新状态:使用 setNumber(n => n + 1) 函数式更新

不可变性原则

对象和数组应视为只读,不应直接修改 state 中的对象或数组。

正确做法

jsx
// 使用扩展语法创建新对象
setUser({ ...user, name: 'New Name' });

// 使用 Immer 简化更新逻辑

四、状态管理最佳实践

Key 的作用

改变组件的 key 值会导致 React 认为这是新组件,从而重新渲染并重置状态

声明式 vs 命令式

声明式编程意味着为每个视觉状态描述 UI,而不是微观管理(命令式)UI。

开发组件的步骤

  1. 识别所有视觉状态
  2. 确定状态变化的触发器(人为和程序)
  3. 使用 useState 建模状态
  4. 删除非必要状态以避免错误和悖论
  5. 连接事件处理程序以设置状态

状态提升规则

  • 不要将 props 初始化赋值给 useState(子组件可直接使用 props)
  • 除非你想忽略 props 的更新

受控 vs 非受控组件

  • 受控组件:由 props 驱动
  • 非受控组件:由内部 state 驱动

状态保留和重置

只要组件在 UI 树中的相同位置渲染,React 就会保留状态。移除或在同一位置渲染不同组件时,会丢弃状态。

重置状态的方法

  1. 在不同位置渲染组件
  2. 给组件明确的 key 标识(key 不是全局唯一,只在父组件中唯一)

五、Refs - 逃生舱口

Ref 的使用场景

当组件需要"记住"信息,但不想触发新渲染时使用 ref。

关键特性

  • 不应在渲染期间读取或写入 current
  • ref 是纯 JavaScript 对象
  • 改变 ref 的 current 值时立即生效
  • 不影响渲染逻辑

Ref 的高级用法

  • Ref 回调:管理 refs 列表
  • forwardRef:转发子组件内部 ref 到外部
  • useImperativeHandle:限制暴露给父组件的 ref 能力

DOM 操作注意事项

  • 使用 flushSync 强制 React 同步更新 DOM
  • 避免更改 React 管理的 DOM 节点
  • 可以安全修改 React 无需更新的 DOM 部分

六、useEffect 深度理解

Effect 的本质

Effect 允许指定由渲染本身引起的副作用,而不是特定事件。

依赖数组规则

  • React 使用 Object.is() 比较依赖值
  • 无依赖数组:每次渲染都执行
  • 空数组 []:仅在组件挂载时执行
  • 不需要添加:refsetState 函数(它们在每次渲染都是同一对象)

Effect 的生命周期

  • 每个 Effect 从其对应的渲染中"捕获"值
  • React 会在下次运行 Effect 前以及卸载时调用清理函数
  • 如果所有依赖项与上次相同,React 会跳过 Effect

useEffect 适用场景

应该使用

  • 控制非 React 组件
  • 订阅事件
  • 触发动画
  • 获取数据
  • 发送分析数据

不应使用

  • 根据 props/state 更新状态 → 渲染期间计算
  • 缓存昂贵计算 → useMemo
  • props 改变时重置所有状态 → 设置 key
  • props 改变时调整部分状态 → 渲染期间计算
  • 事件处理程序间共享逻辑 → 提取公共函数
  • 发送 POST 请求 → 事件处理程序
  • 计算链 → 渲染期间计算
  • 初始化应用 → 顶级变量或模块初始化
  • 通知父组件状态变化 → 状态提升
  • 订阅外部 store → useSyncExternalStore

Effect 思维模式转变

旧思维:从生命周期角度思考("渲染后"、"卸载前") ✅ 新思维:专注于启动/停止同步循环

核心原则

  • 每个 Effect 代表独立的同步过程
  • 描述如何开始和停止同步
  • 不同功能拆分为多个 Effect
  • 组件内所有值(props、state、变量)都是响应式的

接口竞态处理

jsx
useEffect(() => {
  let ignore = false;

  const fetchData = async () => {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  };

  fetchData();

  return () => {
    ignore = true;
  };
}, [userId]);

优化依赖

  • 向 linter 证明不需要某个依赖
  • 避免对象和函数依赖:将它们移到组件外或 Effect 内

useEffectEvent(实验性)

可以将非响应式逻辑(读取最新值而不"响应"它)从 Effect 移到 Effect Event。

注意

  • 仅从 Effect 内部调用
  • 不要传递给其他组件或 Hooks

七、React Hooks 完整指南

State Hooks

useState

  • 自动批处理(React 18+)
  • React 18 之前仅在事件回调中批处理
  • 使用 flushSync 强制同步执行(读取新 DOM 数据时)

useReducer

  • 复杂状态逻辑的替代方案
  • useContext 结合可替代 Redux(小型应用)

Context Hooks

useContext

  • 读取和订阅 context
  • 避免 prop drilling

Ref Hooks

useRef

  1. 获取 DOM 元素属性
  2. 缓存数据(不触发渲染)

useImperativeHandle

  • 子组件调用
  • 自定义暴露给父组件的实例值

Effect Hooks

useEffect

  • 执行时机:DOM 更新后,异步执行
  • 不会阻塞浏览器绘制
  • 注意:虽然在 commit 阶段后执行,但可能因内部函数阻塞 UI 进程

useLayoutEffect

  • 执行时机:DOM 更新完成后,浏览器绘制前
  • 同步执行,会阻塞浏览器绘制
  • 等价于 componentDidMount / componentDidUpdate / componentWillUnmount
  • 使用场景:需要读取布局并立即重绘时

useInsertionEffect

  • 执行时机:DOM 更新前
  • 用于提前注入 <style> 脚本
  • 仅建议 CSS-in-JS 库使用
  • 避免布局确定前修改样式导致重排重绘

注意:三者的 cleanup 函数都在 create 之前相邻调用,且同类型批量调用。


Performance Hooks

useMemo

  • 缓存计算结果
  • 返回函数运行的结果

useCallback

  • 缓存函数引用
  • 配合 React.memo 使用

useTransition

  • 标记非紧急更新为低优先级
  • 可用于 function 组件或其他 hooks
  • 返回 isPending 状态

useDeferredValue

  • 让 state 延迟生效
  • 当前无紧急更新时才变为最新值

Other Hooks

useDebugValue

  • 在 DevTools 中显示自定义 hook 的调试信息

useId

  • 生成唯一 ID
  • 支持客户端和服务端生成相同 ID,避免 hydration 不兼容
  • 原理:ID 代表组件在树中的层级结构

useSyncExternalStore

  • 在 Concurrent Mode 下安全读取外部数据源
  • 处理并发渲染的数据一致性问题
  • 主要供三方状态管理库使用

八、React Components

Fragment

  • <></>,但可以添加属性(如 key)

Profiler

  • 性能检测工具
  • 测量组件渲染性能开销

Suspense

  • 解决 IO 问题
  • 降低子树优先级,减少闪屏

支持场景

  • React.lazy
  • React fetch 库改造的异步请求
  • useTransition
  • useDeferredValue
  • Promise throw error(React 内部 catch)

StrictMode

  • 开启严格模式
  • 帮助发现潜在问题

九、React APIs

createContext

  • 创建 context 对象

forwardRef

  • 转发 ref 到子组件

lazy

  • 动态导入组件
  • 必须接受返回 Promise 的函数
  • Promise 需 resolve 一个 default 导出的 React 组件
  • 必须配合 Suspense 使用

memo

  • 高阶组件
  • 函数组件和类组件都可用
  • 仅根据 props 决定是否重新渲染

startTransition

  • 用于不能使用 hooks 的场景(如 class 组件)
  • 相比 useTransition 无法获取 isPending
  • 协调过程每 5ms 中断一次
  • 连续输入不会重置 transition 更新

十、React DOM APIs

createPortal

  • 将子节点渲染到组件外的其他 DOM 节点

flushSync

  • 将更新任务放到高优先级队列
  • 强制同步刷新,立即更新 DOM

createRoot(客户端)

  • 控制容器节点内容
  • 首次调用时替换所有现有 DOM
  • 后续调用使用 diff 算法高效更新

十一、Custom Hooks 最佳实践

命名规范

  • 必须以 use + 大写字母开头

核心原则

  • 只共享状态逻辑,不共享状态本身
  • 可以在 hooks 间传递响应式值
  • 每次组件重新渲染时,所有 hooks 都会重新运行
  • 代码应保持纯净

事件处理

  • 将 Custom Hook 接收的事件处理程序包装到 Effect Events 中

设计建议

  • 不要创建像 useMount 这样的通用 hooks
  • 保持目的明确

十二、Legacy APIs

createElement

  • JSX 的底层实现

isValidElement

  • 验证是否是 React 元素

未来特性

OffScreen

  • 即将推出的新特性

总结

React 的核心是声明式 UI + 响应式状态管理。掌握以下几点可以写出高质量的 React 代码:

  1. 保持组件纯净:相同输入产生相同输出
  2. 理解 React 快照:每次渲染都是独立的状态快照
  3. 合理使用 Effect:只在真正需要同步外部系统时使用
  4. 优化性能:使用 memouseMemouseCallback 避免不必要的渲染
  5. 正确使用 ref:用于不触发渲染的数据和 DOM 操作
  6. 遵循命名规范:让代码更易读和维护