跳到主要内容

11 篇博文 含有标签「React」

查看所有标签

AI 应用前端选型核对:React 19 的 Server Components、shadcn/ui、React Router 的嵌套路由

· 阅读需 18 分钟

一次选型讨论里落下来三条论断:

  1. React 19 的 Server Components 对 AI 应用有特殊意义——可以在服务端调用 AI 接口,减少客户端暴露
  2. shadcn/ui 风格现代、可定制性强,适合快速搭建 AI 产品界面
  3. React Router 的嵌套路由适合复杂的管理后台场景

三条听起来都对。但「听起来对」和「能拿出依据」之间差着一层:官方文档究竟怎么写的?哪部分是官方原话,哪部分是我自己的推论,哪部分是实测出来的?

这篇文章把三条论断逐条拆开核对。核对办法是三件事:curl 抓一手原文逐字比对、本机跑脚本实测、把推论明确标成推论。

不用框架,自己管好前端请求:缓存、竞态与取消的一般解法

· 阅读需 11 分钟

上一篇《TanStack Query 实战》里,一个收藏星标靠 useQuery / useMutation 把「缓存、竞态、取消、同步」全接管了,一行都没手写。但那篇留下一个反向的问题:如果不引入框架,这些事自己怎么写?

「库帮你做了」和「你知道它做了什么」是两回事。这篇把前端请求里最头疼的几件事——状态管理、缓存、去重、竞态、取消、重试——用纯手写的方式逐层拆开,讲清每一种的一般解法。理解了这些,再回头看 TanStack Query / ahooks,你就能看懂它们「为什么这么设计」。

useEffect 三种用法,在 commit 阶段的挂载时机(源码级辨析)

· 阅读需 7 分钟

「useEffect 是渲染后异步执行的副作用」——这句话对,但不完整。同样是 useEffect,useEffect(fn)、useEffect(fn, [])、useEffect(fn, [a]) 在 commit 阶段触发的时机和条件完全不同;再叠上 useLayoutEffect,时机又变了。

本文从 React 源码(open/react)出发,把「不同使用方式 → 不同 flag → commit 阶段不同挂载时机」这条链讲清楚。

React diff 算法源码拆解:子节点调和的四大步骤

· 阅读需 12 分钟

上一篇我们讲了 React 19 把属性级 diff(prepareUpdate/updatePayload)挪进了 commit 阶段。但大家常说的"React diff 算法",其实指的是另一个完全不同的东西——子节点调和(reconciliation):比较同一层级的 children,决定哪些 fiber 复用、哪些删除、哪些新建、哪些移动。

本文从源码角度拆解这个算法,核心是 reconcileChildrenArray 的四大步骤(这四步直接写在 React 源码 ReactChildFiber.js 的注释里)。先概述全局,再逐步骤分论。


TanStack Query 实战:一个「收藏星标」的乐观更新

· 阅读需 13 分钟

收藏功能几乎是每个内容产品的标配,但它恰恰是「手写 fetch 最容易写脏」的一类交互:点一下,UI 要立刻翻转,网络请求在后台跑,失败还得翻回去。这背后其实藏着一串问题——loading 怎么禁按钮?等网络还是先反馈?失败怎么恢复?详情页、列表页、收藏页三处的数据怎么同步?

这篇文章用项目里一个真实的 FavoriteStar 组件,讲清楚 TanStack Query 是怎么把这四件事全部接管的。你会发现:这些逻辑一行都没手写,全靠 useMutation 的生命周期钩子。

React 19:把属性 diff 从渲染阶段挪进 commit 阶段 —— prepareUpdate/updatePayload 的终结

· 阅读需 8 分钟

很多 React 教程(尤其是写 React 16–18 的书)里会有这样一段:"HostComponent 在 completeWork 的更新流程中,属性发生变化时,会把 diff 的结果以 ['title', 1, 'style', {'color': '#333'}] 这样的扁平数组保存在 updateQueue 里。"

这段描述是真的,但只对 React 16–18 成立。React 19 干了一件大事:把 prepareUpdate 整个删掉,属性 diff 从"渲染阶段预计算"挪到了"commit 阶段边 diff 边应用"。这个改动同时回答了一个经典困惑——updateQueue 到底是"环形链表"还是"key-value 数组"?答案是:它俩都对,因为 updateQueue 是"多态"的,而这个改动恰好把其中一个形态清掉了。

本文扒开这个改动的全部细节:前后机制、diffProperties 怎么生成数组、为什么要删(PR #26583)。


React 19 弃用 forwardRef:ref 传参方式的前后对比

· 阅读需 5 分钟

在 React 里,「把一个 DOM 节点的引用从父组件传给子组件」——这么简单一件事,React 18 及之前却得包一层 forwardRef:

import { forwardRef, useRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} {...props} />;
});

function App() {
const inputRef = useRef<HTMLInputElement>(null);
return <FancyInput ref={inputRef} />;
}

为什么不能直接写 function FancyInput(props) 然后取 props.ref?因为在 React 19 之前,ref 不是普通 prop——它是 React 内部的特殊属性:类组件通过它拿到实例引用,而函数组件默认什么都收不到,只能靠 forwardRef 显式接收再转发。

React 19 把这件事彻底改掉了:ref 变成了普通 prop,forwardRef 被官方标记为 deprecated(弃用,但兼容可用)。新代码不再需要它。

useEffectEvent:把"非响应式逻辑"从 Effect 里抽出来

· 阅读需 20 分钟

在 React 里,你迟早会遇到这么个两头堵的问题:Effect 里要读某个最新的 props / state,但又不想它一变就让 Effect 重跑。加进依赖数组吧,副作用会过度执行(反复重连、反复重置定时器);不加吧,闭包读到的永远是"过期的旧值",而且 ESLint 还会红字报警。

React 19.2(2025 年 10 月转正)带来了专门解决这个问题的 useEffectEvent。但要真正理解它为什么长这样、限制为什么那么多,得从 React 最底层的心智模型——不可变(Immutable)快照——说起。


1. 从 Immutable 说起:每次渲染都是一份"照片"​

React 的 props 和 state 是不可变的。这句话的意思是:更新 state 不是"把旧值改掉",而是丢弃旧快照、生成一个新快照、然后带着新快照重新渲染一次。

这个模型带来一个极其关键的推论:组件体里的每一个闭包,捕获的都是"这一次渲染"的快照值。

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

useEffect(() => {
const id = setTimeout(() => {
console.log(count); // 打印的是"创建 effect 时"的 count,不是最新的
}, 1000);
return () => clearTimeout(id);
}, []);

return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

上面这个例子里,不管点多少次按钮,setTimeout 回调里打印的 count 都是第一次渲染时的值(0)。因为 useEffect 的回调是在"渲染 #1"里创建的,它捕获的 count 就是渲染 #1 的快照——之后 state 变了,组件重新渲染,但旧闭包不会"自动看到"新值。

对比 Vue 的响应式模型会更容易看清这个差异:Vue 的 ref / reactive 是可变的代理,effect 里读 .value 永远是"运行时的当前值"(依赖收集 + 追踪);而 React 的值在渲染那一刻就是定死的,不存在"活的引用"这种东西。

所以 React 里"在 effect 里读最新值"天然是个难题——这就是 useEffectEvent 存在的全部理由。

useTransition 用法与原理:从"非紧急更新"到 React 的 lane 调度机制

· 阅读需 22 分钟

如果一个页面有 1 万个列表项,用户在搜索框每敲一个字符,列表就整体重新过滤、重新渲染一次——输入会卡得没法用。useTransition 就是为了解决这类问题:把"不紧急的渲染"和"用户正在操作的渲染"分开,让输入永远先响应,重的活放到后台慢慢干,还可以随时被新输入打断。

本文先讲用法和简单示例(React 19 的写法),再深入 useTransition 的底层机制——lane 优先级模型、startTransition 内部做了什么、渲染为什么能被中断。


1. 一个场景:为什么需要"非紧急"更新​

先看问题。一个受控输入框 + 昂贵过滤:

function SearchList() {
const [query, setQuery] = useState('');

const results = useMemo(() => filterProducts(query), [query]); // 1 万条数据的过滤+渲染

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

用户每敲一个字符:onChange → setQuery → 整棵列表重渲染。如果这次渲染要花 200ms,输入框就会滞后 200ms 才回显——因为输入框和列表共用同一个 state、同一次渲染,谁都跑不掉。

问题本质:"输入框回显"(紧急)和"过滤并渲染列表"(不紧急)被绑死在一次更新里了。

为什么 React 调度器用小顶堆做任务队列:数据结构、排序与时间复杂度的取舍

· 阅读需 14 分钟

React 的 Scheduler 本质是一个「任务队列 + 时间切片器」:任意时刻都可能有人提交任务,调度器要不断回答同一个问题——下一个执行谁? 它的答案是按「过期时间」排序,谁先过期谁最紧急,先执行谁。

实现这个队列用的数据结构,不是数组加排序,也不是链表,而是一个几十行的小顶堆(Min-Heap)。这篇文章从队列的需求出发,对比各种方案的时间复杂度,讲清楚为什么堆是最优解。