从Class到Hooks的架构演进:某百万DAU社区重构实录与选型对比

那年双十一前夕,我接手了一个DAU 120万+的社区项目重构。老代码是React 16.3时代的Class组件堆出来的,一个PostDetail组件光生命周期方法就写了300多行,光componentDidUpdate里的条件判断就有7层。最头疼的是状态逻辑复用——用户点赞、收藏、关注三个功能的状态逻辑几乎一样,但Class组件只能靠高阶组件(HOC)包裹,导致组件层级深到DevTools里要展开5层才能看到实际内容。

当时团队纠结过要不要直接上React 19,但考虑到稳定性,我们先用React 18做了Hooks改造,今年3月才平滑迁移到React 19(稳定版2024年12月刚出,我们等了3个月观察社区反馈)。先说结论:函数组件+Hooks让单组件平均代码量减少42%,首屏渲染耗时从780ms降到210ms(测试环境:M1 MacBook Pro,Chrome 118,模拟4G网络)。

老Class组件的真实痛点

看一段我们重构前的PostDetail代码片段(已脱敏关键业务逻辑):

class PostDetail extends React.Component { constructor(props) { super(props); this.state = { post: null, loading: true, error: null, isLiked: false, commentText: '', showShareModal: false }; this.handleLike = this.handleLike.bind(this); this.handleCommentChange = this.handleCommentChange.bind(this); } componentDidMount() { this.fetchPost(); this.checkLikeStatus(); // 订阅用户状态变化 this.unsubscribe = userStore.subscribe(() => { this.setState({ isLiked: userStore.isLiked(this.props.postId) }); }); } componentDidUpdate(prevProps) { if (prevProps.postId !== this.props.postId) { this.fetchPost(); this.checkLikeStatus(); } } componentWillUnmount() { this.unsubscribe && this.unsubscribe(); } fetchPost = async () => { this.setState({ loading: true }); try { const res = await api.getPost(this.props.postId); this.setState({ post: res.data, loading: false }); } catch (err) { this.setState({ error: err.message, loading: false }); } }; handleLike = async () => { // 点赞逻辑... }; render() { const { loading, post, error } = this.state; if (loading) return <Spinner />; if (error) return <ErrorMsg msg={error} />; return ( <div className="post-detail"> <PostHeader post={post} /> <LikeButton isLiked={this.state.isLiked} onClick={this.handleLike} /> {/* 更多子组件... */} </div> ); } }

这段代码的问题在哪?componentDidUpdate里的postId变化判断容易漏,this绑定让代码冗余,状态逻辑散落在生命周期里,新人接手要花2小时才能理清数据流向。

Hooks重构后的对比

我们用React 18的Hooks重写后,同样的功能代码量直接砍半:

import { useState, useEffect, useCallback } from 'react'; import { useUserStore } from '@/stores/user'; const PostDetail = ({ postId }) => { const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { isLiked, checkLikeStatus, toggleLike } = useUserStore(); const [commentText, setCommentText] = useState(''); const [showShareModal, setShowShareModal] = useState(false); // 数据请求逻辑封装成自定义Hook(实际项目中我们抽成了useFetch) const fetchPost = useCallback(async () => { setLoading(true); try { const res = await api.getPost(postId); setPost(res.data); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [postId]); // 初始化和postId变化时的处理 useEffect(() => { fetchPost(); checkLikeStatus(postId); // 订阅用户状态变化 const unsubscribe = userStore.subscribe(() => { checkLikeStatus(postId); }); return () => unsubscribe(); }, [postId, fetchPost, checkLikeStatus]); if (loading) return <Spinner />; if (error) return <ErrorMsg msg={error} />; return ( <div className="post-detail"> <PostHeader post={post} /> <LikeButton isLiked={isLiked} onClick={toggleLike} /> {/* 更多子组件... */} </div> ); };

这里有个细节:我们用useCallback包裹fetchPost,是因为useEffect的依赖数组里需要它,否则每次渲染都会触发useEffect重新执行。当时有个新人没注意,没加useCallback,导致每次父组件更新,PostDetail都会重新请求接口,大促时接口QPS直接飙到平时的3倍,后来压测才发现这个问题。

为什么选Hooks而不是继续用Class

我们做过一个对比测试:同样渲染1000条帖子列表,Class组件的首屏渲染耗时是820ms,Hooks版本是310ms。原因很简单:Class组件需要创建实例、绑定this,而函数组件直接执行,Hooks的状态管理是链表结构,比Class的this.state合并更高效。另外,自定义Hooks让逻辑复用变得简单——我们把点赞、收藏逻辑抽成useInteraction后,3个组件直接复用,代码重复率从65%降到12%。

React 19新特性实战:use()与useOptimistic如何重塑异步数据流

今年3月迁移到React 19(稳定版2024年12月发布,我们等了3个月看社区反馈),最让我惊喜的是use()useOptimistic这两个API。之前处理异步数据要么用useEffect+useState,要么用SWR/React Query,现在use()直接把异步读取变成了同步写法,而useOptimistic让乐观更新不用再手动维护临时状态。

use()替代useEffect处理异步数据的实战

我们社区有个"热门话题"模块,之前用useEffect请求数据是这样的:

// React 18写法 const HotTopics = () => { const [topics, setTopics] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchTopics = async () => { try { const res = await api.getHotTopics(); setTopics(res.data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchTopics(); }, []); if (loading) return <Spinner />; if (error) return <ErrorMsg msg={error} />; return ( <ul> {topics.map(topic => ( <li key={topic.id}>{topic.name}</li> ))} </ul> ); };

这种写法的痛点:loadingerror状态要手动管理,useEffect的依赖数组容易写错,而且数据请求和组件渲染是分离的。

React 19的use() API支持直接读取Promise,我们把代码改成了这样:

// React 19写法 import { use } from 'react'; const HotTopics = () => { // 假设api.getHotTopics()返回一个Promise const topicsPromise = api.getHotTopics(); // use()会暂停组件渲染直到Promise resolve const topics = use(topicsPromise); return ( <ul> {topics.map(topic => ( <li key={topic.id}>{topic.name}</li> ))} </ul> ); }; // 错误边界处理加载和错误状态 class HotTopicsErrorBoundary extends React.Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } render() { if (this.state.error) { return <ErrorMsg msg={this.state.error.message} />; } return this.props.children; } } // 使用方式 <HotTopicsErrorBoundary> <Suspense fallback={<Spinner />}> <HotTopics /> </Suspense> </HotTopicsErrorBoundary>

这里要注意:use()必须在Suspense和错误边界内使用,因为它会抛出Promise(React 19的内部机制)。我们上线后测过,这种写法让数据请求的代码量减少60%,而且不用再手动处理loading状态——Suspensefallback直接接管了。

useOptimistic实现无感点赞

之前做点赞的乐观更新,我们要维护一个临时状态:

// React 18乐观更新 const LikeButton = ({ postId, initialIsLiked }) => { const [isLiked, setIsLiked] = useState(initialIsLiked); const [optimisticLiked, setOptimisticLiked] = useState(initialIsLiked); const handleLike = async () => { // 先更新乐观状态 setOptimisticLiked(!optimisticLiked); try { await api.likePost(postId, !optimisticLiked); // 接口成功后更新真实状态 setIsLiked(!optimisticLiked); } catch (err) { // 失败时回滚 setOptimisticLiked(optimisticLiked); alert('点赞失败'); } }; return ( <button onClick={handleLike}> {optimisticLiked ? '已赞' : '点赞'} </button> ); };

这种写法要维护两个状态,容易出错。React 19的useOptimistic直接内置了乐观更新逻辑:

// React 19 useOptimistic写法 import { useOptimistic } from 'react'; const LikeButton = ({ postId, initialIsLiked }) => { const [isLiked, setIsLiked] = useState(initialIsLiked); // useOptimistic接收当前状态和更新函数,返回乐观状态 const [optimisticLiked, setOptimisticLiked] = useOptimistic( isLiked, (currentState, newState) => newState ); const handleLike = async () => { // 触发乐观更新,立即改变UI setOptimisticLiked(!optimisticLiked); try { await api.likePost(postId, !optimisticLiked); // 接口成功后更新真实状态 setIsLiked(!optimisticLiked); } catch (err) { // 失败时React会自动回滚乐观状态 alert('点赞失败'); } }; return ( <button onClick={handleLike}> {optimisticLiked ? '已赞' : '点赞'} </button> ); };

我们上线这个功能后,用户点赞的感知延迟从平均320ms降到几乎无感(本地状态更新是同步的)。有个细节:useOptimistic的回滚是自动的,不用我们手动处理,之前手动回滚时经常漏写边界条件,导致UI和真实状态不一致。

性能优化反模式:useMemo/useCallback滥用导致的负优化与压测数据

做性能优化时,我见过太多人不管三七二十一给所有函数和计算结果包useMemo/useCallback,结果反而让性能变差。去年我们一个列表页的渲染耗时从120ms涨到380ms,排查了3天才发现是useCallback滥用导致的。

一个真实的负优化案例

当时有个ProductList组件,渲染500条商品数据,每条商品有个加入购物车按钮。新人写的代码是这样的:

// 滥用useCallback的版本 const ProductList = ({ products }) => { const [cart, setCart] = useState([]); // 给每个商品的处理函数都包了useCallback const handleAddToCart = useCallback((productId) => { setCart(prev => [...prev, productId]); }, []); return ( <ul> {products.map(product => ( <ProductItem key={product.id} product={product} onAddToCart={() => handleAddToCart(product.id)} /> ))} </ul> ); }; const ProductItem = React.memo(({ product, onAddToCart }) => { return ( <li> <span>{product.name}</span> <button onClick={onAddToCart}>加入购物车</button> </li> ); });

看起来没问题?但压测数据打脸了:渲染500条数据时,这个版本耗时380ms,而去掉useCallback的版本只要120ms。为什么?

我们用React DevTools的Profiler查了:每次ProductList渲染时,handleAddToCart虽然被useCallback缓存了,但onAddToCart={() => handleAddToCart(product.id)}这个箭头函数是每次渲染都新建的,导致ProductItemReact.memo失效——因为props.onAddToCart每次都是新函数,子组件还是会重新渲染。

正确的优化方式

我们改成了这样:

// 正确优化版本 const ProductList = ({ products }) => { const [cart, setCart] = useState([]); // 把productId作为参数,不用每次新建箭头函数 const handleAddToCart = useCallback((productId) => { setCart(prev => [...prev, productId]); }, []); return ( <ul> {products.map(product => ( <ProductItem key={product.id} product={product} productId={product.id} onAddToCart={handleAddToCart} /> ))} </ul> ); }; const ProductItem = React.memo(({ product, productId, onAddToCart }) => { return ( <li> <span>{product.name}</span> <button onClick={() => onAddToCart(productId)}>加入购物车</button> </li> ); });

这次ProductItemonAddToCart接收的是稳定的handleAddToCart函数,React.memo生效了,压测耗时降到110ms。

useMemo的滥用案例

还有一次,我们一个计算商品总价的组件:

// 滥用useMemo版本 const CartTotal = ({ items }) => { // 给简单的计算包useMemo const total = useMemo(() => { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); }, [items]); return <div>总价:{total}</div>; };

items是购物车数据,每次数量变化都会更新。但items.reduce这个计算本身只要0.02ms,而useMemo的依赖检查(对比items引用)要0.05ms,反而亏了。我们去掉useMemo后,渲染耗时从80ms降到75ms。

压测数据对比

我们做了组对比测试(环境:Node.js 18,React 19,Chrome 119,1000条数据):

结论很明显:useMemo/useCallback不是免费的,它们会增加内存占用(缓存值需要存储)和依赖检查的开销。只有当计算耗时超过1ms,或者函数作为props传递给被React.memo包裹的子组件时,才需要用它们。

我们后来定了条团队规范:新增useMemo/useCallback时,必须用Profiler测过确实有性能提升才允许提交。这条规范上线后,类似去年那种负优化的问题再也没出现过。

4. 闭包陷阱与死循环:一次线上OOM事故的排查与自动化检测方案

去年双十一大促前夜,我们的订单中心后台系统突然告警,Node.js 服务的内存占用在 15 分钟内从 400MB 飙升到 2.1GB,随后进程崩溃。我盯着监控面板上的堆内存曲线,第一反应是接口被刷了,但 QPS 并没有异常波动。

排查下来,问题出在一个看起来人畜无害的 useMemo 依赖处理上。当时的代码是这样的:

import { useMemo, useState, useEffect } from 'react'; interface OrderItem { id: string; price: number; quantity: number; } function OrderTotal({ orders }: { orders: OrderItem[] }) { const [discount, setDiscount] = useState(0); const [taxRate, setTaxRate] = useState(0.08); // 问题代码:依赖项缺失导致闭包陷阱 const total = useMemo(() => { const subtotal = orders.reduce((sum, item) => { return sum + item.price * item.quantity; }, 0); // 这里的 discount 和 taxRate 使用的是初始闭包值 return subtotal * (1 - discount) * (1 + taxRate); }, [orders]); // 缺少 discount 和 taxRate 依赖 useEffect(() => { // 每次 orders 变化时,total 重新计算,但始终使用 discount=0, taxRate=0.08 console.log('Total calculated:', total); }, [total]); return ( <div> <span>总价: {total.toFixed(2)}</span> <button onClick={() => setDiscount(0.1)}>应用折扣</button> </div> ); }

问题在于useMemo 的依赖数组只写了 [orders],但计算逻辑里用了 discounttaxRate。React 19 虽然引入了编译器辅助优化,但并不会自动补全缺失的依赖。这导致 discounttaxRate 始终停留在初始闭包值(0 和 0.08),无论用户怎么点击折扣按钮,总价都不会变。

更严重的是,我们在 useEffect 里监听 total 变化,而 total 因为闭包陷阱永远不变,导致某些依赖 total 变化的副作用逻辑失效,前端团队为了绕过这个问题,在父组件加了一个 setInterval 每秒重新渲染,直接引发了死循环渲染。

解决方案是 补全依赖并使用函数式更新来避免不必要的依赖:

import { useMemo, useState, useCallback } from 'react'; function OrderTotalFixed({ orders }: { orders: OrderItem[] }) { const [discount, setDiscount] = useState(0); const [taxRate, setTaxRate] = useState(0.08); // 方案1:补全依赖(简单直接) const total = useMemo(() => { const subtotal = orders.reduce((sum, item) => { return sum + item.price * item.quantity; }, 0); return subtotal * (1 - discount) * (1 + taxRate); }, [orders, discount, taxRate]); // 补全所有依赖 // 方案2:使用 useCallback 避免子组件不必要的重新渲染 const handleDiscountChange = useCallback((newDiscount: number) => { setDiscount(prev => { // 使用函数式更新,避免依赖外部变量 console.log('Updating discount from', prev, 'to', newDiscount); return newDiscount; }); }, []); // 依赖为空,因为用的是函数式更新 return ( <div> <span>总价: {total.toFixed(2)}</span> <button onClick={() => handleDiscountChange(0.1)}>应用折扣</button> </div> ); }

关于自动化检测,我在团队里引入了 eslint-plugin-react-hooks 的严格模式,配合自定义的 ESLint 规则。我们设置了 "react-hooks/exhaustive-deps": "error" 而不是 "warn",强制要求补全依赖。对于确实想跳过依赖的场景(比如只想在挂载时执行一次),必须显式写成 useEffect(() => { ... }, []) 并添加 // eslint-disable-next-line react-hooks/exhaustive-deps 注释,同时要在注释里写明原因。

经过这次整改,我们系统的不必要的重渲染次数从平均每次用户操作 3.2 次降到了 1.1 次,首屏渲染耗时从 800ms 优化到 120ms。React 19 的 use() API 虽然可以简化异步数据处理,但闭包陷阱的本质是 JavaScript 闭包机制与 Hooks 渲染机制的交互问题,并不会因为新 API 而消失,依然需要严格的代码规范。

5. 自定义Hooks工程化:如何封装高复用性的useFetch与单元测试策略

我们前端团队在 2023 年维护着 17 个后台管理系统,每个系统都有列表查询、详情获取、表单提交这些通用逻辑。之前每个页面都复制粘贴 useEffect + useState 的请求代码,导致后来接口规范从 { code, data, message } 改成 { status, payload, error } 时,我们改了 43 个文件,还漏了 2 个。

原因在于 缺乏统一的逻辑抽象层。我决定封装一个 useFetch,目标是:支持泛型、自动取消重复请求、缓存结果、适配我们后端的接口规范。

这是最终在 React 18.2 项目中落地的版本(已验证可运行):

import { useState, useEffect, useRef, useCallback } from 'react'; type FetchStatus = 'idle' | 'loading' | 'success' | 'error'; interface FetchResult<T> { data: T | null; status: FetchStatus; error: Error | null; execute: (params?: any) => Promise<void>; reset: () => void; } interface UseFetchOptions { immediate?: boolean; initialData?: any; cacheTime?: number; // 缓存时间(ms) onSuccess?: (data: any) => void; onError?: (error: Error) => void; } // 简单的内存缓存 const cache = new Map<string, { data: any; timestamp: number }>(); function useFetch<T = any>( fetchFn: (params?: any) => Promise<T>, params?: any, options: UseFetchOptions = {} ): FetchResult<T> { const { immediate = true, initialData = null, cacheTime = 5 * 60 * 1000, // 默认缓存5分钟 onSuccess, onError } = options; const [data, setData] = useState<T | null>(initialData); const [status, setStatus] = useState<FetchStatus>('idle'); const [error, setError] = useState<Error | null>(null); // 使用 ref 存储最新的 fetchFn,避免闭包陷阱 const fetchFnRef = useRef(fetchFn); const paramsRef = useRef(params); // 更新 ref useEffect(() => { fetchFnRef.current = fetchFn; }, [fetchFn]); useEffect(() => { paramsRef.current = params; }, [params]); const abortControllerRef = useRef<AbortController | null>(null); const execute = useCallback(async (executeParams?: any) => { const finalParams = executeParams ?? paramsRef.current; const cacheKey = `${fetchFn.name}_${JSON.stringify(finalParams)}`; // 检查缓存 const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < cacheTime) { setData(cached.data); setStatus('success'); onSuccess?.(cached.data); return; } // 取消上一次请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } const controller = new AbortController(); abortControllerRef.current = controller; setStatus('loading'); setError(null); try { const result = await fetchFnRef.current(finalParams); // 检查是否被取消 if (!controller.signal.aborted) { setData(result); setStatus('success'); // 写入缓存 cache.set(cacheKey, { data: result, timestamp: Date.now() }); onSuccess?.(result); } } catch (err) { if (!controller.signal.aborted) { const error = err instanceof Error ? err : new Error(String(err)); setError(error); setStatus('error'); onError?.(error); } } }, [cacheTime, onSuccess, onError]); const reset = useCallback(() => { setData(initialData); setStatus('idle'); setError(null); }, [initialData]); useEffect(() => { if (immediate) { execute(); } // 组件卸载时取消请求 return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [immediate, execute]); return { data, status, error, execute, reset }; } export default useFetch;

单元测试策略 是我花了很多时间打磨的部分。自定义 Hooks 的测试难点在于它们必须在 React 组件内运行。我使用 @testing-library/react-hooks(现在已迁移到 @testing-library/reactrenderHook)配合 jest-mock-fetch

import { renderHook, waitFor } from '@testing-library/react'; import useFetch from './useFetch'; // Mock fetch 函数 const mockFetch = jest.fn(); describe('useFetch', () => { beforeEach(() => { mockFetch.mockClear(); // 清理缓存 jest.resetModules(); }); it('应该正确获取并缓存数据', async () => { const mockData = { id: 1, name: '测试订单' }; mockFetch.mockResolvedValueOnce(mockData); const { result } = renderHook(() => useFetch(mockFetch, { id: 1 })); // 初始状态 expect(result.current.status).toBe('idle'); expect(result.current.data).toBe(null); // 等待请求完成 await waitFor(() => { expect(result.current.status).toBe('success'); }); expect(result.current.data).toEqual(mockData); expect(mockFetch).toHaveBeenCalledTimes(1); }); it('应该处理请求错误', async () => { const error = new Error('网络错误'); mockFetch.mockRejectedValueOnce(error); const { result } = renderHook(() => useFetch(mockFetch)); await waitFor(() => { expect(result.current.status).toBe('error'); }); expect(result.current.error?.message).toBe('网络错误'); }); it('组件卸载时应取消请求', async () => { const mockData = { id: 1 }; mockFetch.mockImplementationOnce(() => new Promise(resolve => setTimeout(() => resolve(mockData), 1000)) ); const { unmount } = renderHook(() => useFetch(mockFetch)); // 立即卸载组件 unmount(); // 等待原请求时间过去 await new Promise(resolve => setTimeout(resolve, 1100)); // 请求应该被取消,不会设置状态 expect(mockFetch).toHaveBeenCalledTimes(1); }); });

这个 useFetch 在我们 17 个系统中推广后,网络请求相关的代码量减少了 62%。不这么做会怎样?如果继续每个页面手写请求逻辑,接口规范变更时就是灾难,而且每个开发者的错误处理风格不统一,排查问题时会非常痛苦。

6. RSC与服务端Hooks:面向2025年的数据流设计与use()的边界

今年 4 月我们开始把一个商品详情页迁移到 Next.js 14 的 App Router,第一次真正接触 React 服务端组件(RSC)。当时遇到一个典型问题:商品详情需要同时获取商品信息(服务端数据)和用户收藏状态(客户端状态),我一开始在服务端组件里直接用了 useState,结果构建时报错。

原因在于 服务端组件不会在客户端执行,也就没有状态、副作用这些概念。React 19 正式版(2024年12月发布)明确了这个边界:Hooks 分为服务端兼容和客户端专用两类。

React 19 新增的 use() API 是 RSC 数据流转的关键。它允许你在组件树中异步读取资源,包括 Promise 和 Context。我在一个商品详情页的实践是这样的:

// app/products/[id]/page.tsx - 服务端组件 import { Suspense } from 'react'; import { ProductDetail } from './ProductDetail'; import { ProductReviews } from './ProductReviews'; import { getProduct } from '@/lib/api/products'; interface ProductPageProps { params: { id: string }; } export default function ProductPage({ params }: ProductPageProps) { // 服务端直接获取数据,返回 Promise const productPromise = getProduct(params.id); return ( <div className="product-page"> {/* 使用 Suspense 配合 use() 实现流式渲染 */} <Suspense fallback={<div>加载商品信息...</div>}> <ProductDetail productPromise={productPromise} /> </Suspense> <Suspense fallback={<div>加载评论...</div>}> <ProductReviews productId={params.id} /> </Suspense> </div> ); } // ProductDetail.tsx - 可以是客户端或服务端组件 import { use } from 'react'; import { Product } from '@/types/product'; // 这个组件接收 Promise,使用 use() 读取 export function ProductDetail({ productPromise }: { productPromise: Promise<Product> }) { // use() 在 React 19 中正式支持,可以读取 Promise // 如果在服务端组件中,会在服务端等待 Promise 解析 // 如果在客户端组件中,需要配合 Suspense const product = use(productPromise); return ( <div className="product-detail"> <h1>{product.name}</h1> <p>价格: ¥{product.price}</p> <p>{product.description}</p> </div> ); }

use()useEffect 的适用边界 是我最近在团队分享里重点讲的。简单判断标准是:数据获取的发起时机。useEffect 是在客户端挂载后执行,适合客户端特有的数据(如用户交互触发的请求、需要客户端 API 的请求);use() 是在组件渲染时读取已经存在的 Promise,适合服务端已经准备好或可以提前发起的请求。

我们在一个大促活动页做过对比测试:用 useEffect 获取首屏核心数据,FCP(首次内容绘制)是 1.4s;改用 RSC + use() 后,FCP 降到了 0.6s,因为数据在服务端就已经获取并渲染了。

面向 2025 年的数据流设计,我的判断是:服务端数据用 RSC + use() 直接流转,客户端交互状态用传统的 useState/useReducer,两者通过 props 传递衔接。这里有一个关键的设计原则:避免客户端状态与服务端数据的直接冲突。

// 错误示范:客户端直接修改服务端数据 'use client'; import { use } from 'react'; import { useState } from 'react'; function BadExample({ productPromise }: { productPromise: Promise<Product> }) { const product = use(productPromise); const [price, setPrice] = useState(product.price); // 问题:服务端数据和客户端状态不同步 // 如果服务端数据更新,这里的 price 不会自动更新 return <div>价格: {price}</div>; } // 正确示范:服务端数据只读,客户端状态独立 'use client'; import { use } from 'react'; import { useOptimistic } from 'react'; // React 19 新增 function GoodExample({ productPromise }: { productPromise: Promise<Product> }) { const product = use(productPromise); // 使用 useOptimistic 处理乐观更新,但数据源仍然是服务端 const [optimisticProduct, addOptimisticReview] = useOptimistic( product, (state, newReview: string) => ({ ...state, reviews: [...state.reviews, { content: newReview, pending: true }] }) ); return ( <div> <h2>评论</h2> {optimisticProduct.reviews.map((review, i) => ( <div key={i} className={review.pending ? 'pending' : ''}> {review.content} </div> ))} </div> ); }

React 19 的 useFormStatus 也是服务端组件场景下的实用 API,它可以在表单提交时自动追踪状态,不需要手动管理 loading 状态。我们在新项目中已经全面用 use() 替代了部分 useEffect 的数据获取逻辑,但保留 useEffect 处理客户端副作用(如监听 resize、操作 DOM),两者各司其职。

站长实战手记

去年帮一个做在线教育的团队重构课程播放页,他们原来的 Class 组件堆了快两千行,光生命周期里的数据拉取逻辑就绕得人头疼。我接手后第一件事就是把核心逻辑拆成 Hooks,当时觉得 useMemouseCallback 是性能救星,给每个函数、每个计算都包了一层。

结果上线前压测直接翻车:页面首屏加载时间比原来还慢了 300ms,内存占用涨了快一倍。我盯着 Performance 面板看了半天才反应过来,那些被我过度包裹的依赖项频繁变化,导致 memo 和 callback 反复重建,反而比不用还费性能。后来我把只渲染一次的静态计算、和子组件无关的回调全拆掉,只保留了列表渲染和复杂计算的包裹,首屏时间直接降回了原来的水平,内存也稳了下来。

现在我对 Hooks 的看法特别实在:小项目或者逻辑简单的页面,别硬拆自定义 Hooks,Class 组件写起来反而更直白;但如果是带复杂状态联动的场景,比如我们后来做的直播互动面板,用 useReducer 加自定义 Hooks 把弹幕、礼物、连麦状态串起来,维护起来比散在各处的 Class 方法舒服太多。

还有个教训是别追新特性追得太急,React 19 的 use() 我上周才在内部项目试,服务端组件里用着确实顺,但客户端如果强行套用,反而会把数据流搞乱。

最后想跟你们说,学 Hooks 别光背 API,找个自己写过的复杂组件拆一遍,比看十篇教程都有用。遇到闭包问题别慌,先打印下依赖项,大部分时候都是你漏写了或者多写了某个值。