React 学习笔记
一、描述用户界面
组件基础规则
- 组件命名:必须以大写字母开头
- 纯函数原则:保持组件纯净,相同输入产生相同输出
- JSX 语法:标签与
return不在同一行时,必须用括号包裹 - 组件嵌套:永远不要在组件内部定义其他组件
- ❌ 错误:嵌套定义会导致每次渲染创建不同的函数,React 会重置所有状态
- ✅ 正确:在顶层定义每个组件
JSX 规则
- 属性命名:使用驼峰式 camelCase,包括内联
style属性 - 大括号使用:
- 作为 JSX 标签内的文本内容
- 作为紧跟在
=之后的属性值
- 对象语法:
只是 JSX 中的对象字面量
Props 和 Children
props是组件的唯一参数,可指定默认值- 内容嵌套在 JSX 标签中时,父组件通过
childrenprop 接收 - 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 标签)
组件内局部变量的问题
- 局部变量不会在渲染之间持续存在
- 对局部变量的更改不会触发渲染
三、状态管理核心概念
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。
开发组件的步骤
- 识别所有视觉状态
- 确定状态变化的触发器(人为和程序)
- 使用
useState建模状态 - 删除非必要状态以避免错误和悖论
- 连接事件处理程序以设置状态
状态提升规则
- 不要将 props 初始化赋值给
useState(子组件可直接使用 props) - 除非你想忽略 props 的更新
受控 vs 非受控组件
- 受控组件:由 props 驱动
- 非受控组件:由内部 state 驱动
状态保留和重置
只要组件在 UI 树中的相同位置渲染,React 就会保留状态。移除或在同一位置渲染不同组件时,会丢弃状态。
重置状态的方法:
- 在不同位置渲染组件
- 给组件明确的
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()比较依赖值 - 无依赖数组:每次渲染都执行
- 空数组
[]:仅在组件挂载时执行 - 不需要添加:
ref、setState函数(它们在每次渲染都是同一对象)
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
- 获取 DOM 元素属性
- 缓存数据(不触发渲染)
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 库改造的异步请求
useTransitionuseDeferredValue- 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 代码:
- 保持组件纯净:相同输入产生相同输出
- 理解 React 快照:每次渲染都是独立的状态快照
- 合理使用 Effect:只在真正需要同步外部系统时使用
- 优化性能:使用
memo、useMemo、useCallback避免不必要的渲染 - 正确使用 ref:用于不触发渲染的数据和 DOM 操作
- 遵循命名规范:让代码更易读和维护