React 19新特性解析
React 19新特性解析
概述与背景
React 19是React团队在2024年发布的重大版本更新,带来了诸多令人期待的新特性。这些更新不仅简化了开发流程,还大幅提升了应用性能和用户体验。
graph TB
subgraph React 19核心架構
A[React 19] --> B[Actions<br/>表單處理]
A --> C[新Hooks<br/>useFormStatus等]
A --> D[服務端組件<br/>RSC]
A --> E[Ref簡化<br/>直接prop]
B --> F[統一數據流]
C --> G[狀態管理簡化]
D --> H[性能優化]
E --> I[開發體驗提升]
end
style A fill:#61dafb
style B fill:#c8e6c9
style C fill:#c8e6c9
style D fill:#c8e6c9
style E fill:#c8e6c9
graph LR
subgraph React版本對比
A[React 18] --> A1[onSubmit+狀態管理]
A --> A2[useEffect+fetch]
A --> A3[手動管理加載狀態]
A --> A4[forwardRef]
B[React 19] --> B1[Actions]
B --> B2[use Hook]
B --> B3[useFormStatus]
B --> B4[直接prop]
end
style A fill:#ffcdd2
style B fill:#c8e6c9
React 19的核心目标
- 简化数据获取:统一的服务端/客户端数据流
- 优化表单处理:Actions机制取代传统onSubmit
- 提升用户体验:内置乐观更新和加载状态
- 增强性能:服务端组件和流式渲染
- 改善开发者体验:简化API,减少样板代码
版本对比
| 特性 | React 18 | React 19 |
|---|---|---|
| 表单处理 | onSubmit + 状态管理 | Actions |
| 数据获取 | useEffect + fetch | use Hook |
| 加载状态 | 手动管理 | useFormStatus |
| 乐观更新 | 自行实现 | useOptimistic |
| Ref传递 | forwardRef | 直接prop |
| 元数据 | Helmet等库 | 原生支持 |
核心新特性详解
graph TD
subgraph Actions工作流程
A[用戶提交表單] --> B[Action函數]
B --> C{執行結果}
C -->|成功| D[更新UI]
C -->|失敗| E[顯示錯誤]
C -->|加載中| F[顯示加載狀態]
G[useFormStatus] --> F
H[useOptimistic] --> I[樂觀更新]
J[useActionState] --> K[狀態管理]
end
style A fill:#fff9c4
style B fill:#e1f5fe
style D fill:#c8e6c9
style E fill:#ffcdd2
sequenceDiagram
participant U as 用戶
participant F as 表單組件
participant A as Action
participant S as 服務器
Note over U,S: React 19 Actions流程
U->>F: 提交表單
F->>A: 調用Action函數
A->>S: 發送數據
A->>A: useOptimistic樂觀更新UI
S-->>A: 返回結果
alt 成功
A->>F: 更新最終狀態
F-->>U: 顯示成功
else 失敗
A->>F: 回滾樂觀更新
F-->>U: 顯示錯誤
end
1. Actions:简化表单处理
React 19引入了Actions概念,彻底改变了表单处理的方式。
传统方式的痛点
// React 18及之前:需要大量样板代码
function LoginForm() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
const formData = new FormData(e.target);
const response = await submitLogin(formData);
if (response.ok) {
setSuccess(true);
// 重定向、更新状态等
} else {
setError(response.message);
}
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit" disabled={isLoading}>
{isLoading ? '登录中...' : '登录'}
</button>
{error && <div className="error">{error}</div>}
{success && <div className="success">登录成功!</div>}
</form>
);
}
痛点分析:
- 需要管理多个状态(loading、error、success)
- 需要preventDefault阻止默认行为
- 需要手动构建FormData
- 错误处理逻辑分散
- 测试困难
React 19 Actions方式
// React 19:简洁优雅
function LoginForm() {
// 服务端Action
async function loginAction(formData) {
'use server'; // Next.js服务端标记
const result = await submitLogin(formData);
if (!result.ok) {
throw new Error(result.message);
}
redirect('/dashboard');
}
return (
<form action={loginAction}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<SubmitButton>登录</SubmitButton>
<ErrorBoundary />
</form>
);
}
// 提交按钮组件
function SubmitButton({ children }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '登录中...' : children}
</button>
);
}
优势:
- 无需手动管理loading状态
- 自动处理FormData
- 内置错误处理机制
- 代码量减少60%+
- 更容易测试
Actions的工作原理
用户提交表单
↓
1. 拦截提交事件(自动)
↓
2. 收集表单数据为FormData
↓
3. 调用Action函数
↓
4. 自动管理pending状态
↓
5. 处理返回结果或错误
↓
6. 更新UI或重定向
客户端Actions
Actions不仅限于服务端,也可以在纯客户端使用:
// 客户端Action
function ContactForm() {
const [state, formAction] = useActionState(async (prevState, formData) => {
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// 客户端验证
if (!name || !email || !message) {
return { error: '请填写所有字段' };
}
try {
await fetch('/api/contact', {
method: 'POST',
body: formData
});
return { success: true, message: '发送成功!' };
} catch (error) {
return { error: '发送失败,请重试' };
}
}, null);
return (
<form action={formAction}>
<input name="name" />
<input name="email" type="email" />
<textarea name="message" />
<button type="submit">发送</button>
{state?.error && <div className="error">{state.error}</div>}
{state?.success && <div className="success">{state.message}</div>}
</form>
);
}
2. useFormStatus Hook
配合Actions使用,轻松获取表单状态。
基础用法
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
// pending: boolean - 是否正在提交
// data: FormData | null - 表单数据
// method: 'get' | 'post' - 表单方法
// action: Function | null - Action函数
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
提交中...
</>
) : (
'提交'
)}
</button>
);
}
重要限制
useFormStatus必须从表单内部组件调用:
// ❌ 错误:在同一个组件中使用
function Form() {
const { pending } = useFormStatus(); // 总是返回false!
return (
<form action={submitForm}>
<input name="title" />
<button disabled={pending}>提交</button> {/* 不工作 */}
</form>
);
}
// ✅ 正确:从子组件中使用
function Form() {
return (
<form action={submitForm}>
<input name="title" />
<SubmitButton /> {/* 分离为子组件 */}
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus(); // 正确获取状态
return <button disabled={pending}>{pending ? '提交中' : '提交'}</button>;
}
实战示例:智能提交按钮
function SmartSubmitButton({ loadingText = '提交中...', children }) {
const { pending, data } = useFormStatus();
// 可以访问表单数据
const hasFiles = data?.has('attachments');
return (
<button
type="submit"
disabled={pending}
className={pending ? 'opacity-50 cursor-not-allowed' : ''}
>
{pending ? (
<span className="flex items-center gap-2">
<LoadingSpinner size="sm" />
{loadingText}
{hasFiles && <span className="text-sm">(上传中...)</span>}
</span>
) : children}
</button>
);
}
// 使用
function UploadForm() {
return (
<form action={uploadFiles}>
<input type="file" name="attachments" multiple />
<SmartSubmitButton loadingText="上传文件中...">
上传文件
</SmartSubmitButton>
</form>
);
}
3. useActionState Hook
管理Action的执行状态和结果,替代手动状态管理。
API签名
function useActionState<State, Payload>(
action: (state: State, payload: Payload) => State | Promise<State>,
initialState: State,
permalink?: string
): [state: State, dispatch: (payload: Payload) => void, isPending: boolean];
基础示例
import { useActionState } from 'react';
function TodoForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const title = formData.get('title');
// 验证
if (!title?.trim()) {
return { error: '标题不能为空', todos: prevState?.todos || [] };
}
// API调用
try {
const newTodo = await addTodo({ title });
return {
success: '添加成功!',
todos: [...(prevState?.todos || []), newTodo]
};
} catch (error) {
return {
error: error.message,
todos: prevState?.todos || []
};
}
},
{ todos: [], error: null, success: null }
);
return (
<div>
<form action={formAction}>
<input
name="title"
placeholder="输入待办事项"
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? '添加中...' : '添加'}
</button>
</form>
{/* 显示反馈 */}
{state.error && (
<div className="error-message">{state.error}</div>
)}
{state.success && (
<div className="success-message">{state.success}</div>
)}
{/* 待办列表 */}
<ul>
{state.todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
与useReducer对比
// useReducer方式(React 18)
function TodoForm() {
const [state, dispatch] = useReducer(
(state, action) => {
switch (action.type) {
case 'SUBMIT':
return { ...state, isLoading: true };
case 'SUCCESS':
return {
isLoading: false,
todos: [...state.todos, action.payload]
};
case 'ERROR':
return { isLoading: false, error: action.error };
default:
return state;
}
},
{ todos: [], isLoading: false, error: null }
);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT' });
try {
const formData = new FormData(e.target);
const todo = await addTodo(formData);
dispatch({ type: 'SUCCESS', payload: todo });
} catch (error) {
dispatch({ type: 'ERROR', error: error.message });
}
};
return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}
// useActionState方式(React 19)
function TodoForm() {
const [state, formAction, isPending] = useActionState(
async (prev, formData) => {
const todo = await addTodo(formData);
return { ...prev, todos: [...prev.todos, todo] };
},
{ todos: [] }
);
return (
<form action={formAction}>
{/* ... */}
</form>
);
}
4. useOptimistic:乐观更新
实现流畅的乐观更新体验,无需等待服务器响应。
什么是乐观更新?
乐观更新(Optimistic Update)是一种UI模式:先立即更新UI,给用户即时反馈,然后在后台发送请求。如果请求失败,再回滚更改。
用户体验对比:
| 模式 | 用户操作 | UI响应 | 服务器响应 | 用户感知 |
|---|---|---|---|---|
| 悲观更新 | 点击提交 | 等待… | 1-3秒后更新 | 卡顿、等待 |
| 乐观更新 | 点击提交 | 立即更新 | 1-3秒后确认 | 流畅、即时 |
基础用法
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => {
// 乐观更新:立即添加到列表
return [...state, { ...newTodo, pending: true }];
}
);
async function handleSubmit(formData) {
const title = formData.get('title');
const tempId = Date.now();
// 1. 乐观更新
addOptimisticTodo({ id: tempId, title, pending: true });
// 2. 实际请求
try {
const newTodo = await addTodo({ title });
// 服务器返回真实数据后,React会用真实数据替换临时数据
} catch (error) {
// 失败时自动回滚
console.error('添加失败:', error);
}
}
return (
<div>
<form action={handleSubmit}>
<input name="title" placeholder="输入待办事项" />
<button type="submit">添加</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li
key={todo.id}
className={todo.pending ? 'opacity-50 italic' : ''}
>
{todo.title}
{todo.pending && <span className="text-sm"> (保存中...)</span>}
</li>
))}
</ul>
</div>
);
}
完整的点赞功能示例
function LikeButton({ postId, initialLikes, isLiked }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(isLiked);
const [optimisticLikes, toggleLike] = useOptimistic(
{ count: likes, liked },
(state, newValue) => ({
count: state.count + (newValue ? 1 : -1),
liked: newValue
})
);
async function handleLike() {
// 立即更新UI
toggleLike(!optimisticLikes.liked);
// 发送请求
try {
const result = await fetch(`/api/posts/${postId}/like`, {
method: optimisticLikes.liked ? 'DELETE' : 'POST'
});
const data = await result.json();
// 更新真实数据
setLikes(data.likes);
setLiked(data.liked);
} catch (error) {
// 失败时自动回滚
toast.error('操作失败,请重试');
}
}
return (
<button
onClick={handleLike}
className={optimisticLikes.liked ? 'liked' : ''}
>
{optimisticLikes.liked ? '❤️' : '🤍'} {optimisticLikes.count}
</button>
);
}
评论列表示例
function CommentList({ postId, initialComments }) {
const [comments, setComments] = useState(initialComments);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, { ...newComment, sending: true }]
);
const submitComment = async (formData) => {
const content = formData.get('comment');
const tempId = `temp-${Date.now()}`;
// 乐观添加
addOptimisticComment({
id: tempId,
content,
author: '我',
createdAt: new Date().toISOString(),
sending: true
});
// 发送请求
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
body: formData
});
const newComment = await response.json();
// 替换临时评论为真实评论
setComments(prev =>
prev.map(c => c.id === tempId ? newComment : c)
);
};
return (
<div>
<ul>
{optimisticComments.map(comment => (
<li key={comment.id} className={comment.sending ? 'opacity-50' : ''}>
<strong>{comment.author}:</strong> {comment.content}
{comment.sending && <span> 发送中...</span>}
</li>
))}
</ul>
<form action={submitComment}>
<textarea name="comment" required />
<button type="submit">发送评论</button>
</form>
</div>
);
}
5. use API
统一的数据获取和Context读取方式。
API签名
function use<T>(resource: Promise<T> | Context<T>): T;
用途1:Promise数据获取
import { use } from 'react';
// 传统方式(需要Suspense边界)
function UserProfile({ userId }) {
const userPromise = fetch(`/api/users/${userId}`).then(r => r.json());
return (
<Suspense fallback={<Spinner />}>
<UserDetails userPromise={userPromise} />
</Suspense>
);
}
function UserDetails({ userPromise }) {
const user = use(userPromise); // 自动处理Promise
return (
<div>
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
用途2:Context读取
import { use } from 'react';
// 传统方式
function ThemeComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div className={theme}>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
// use API方式
function ThemeComponent() {
const { theme, toggleTheme } = use(ThemeContext);
return (
<div className={theme}>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
条件使用(突破Hook规则限制)
传统Hooks不能在条件语句中调用,但use可以:
function CommentSection({ showComments, commentsPromise }) {
// ✅ 条件中使用use
if (showComments) {
const comments = use(commentsPromise);
return <CommentList comments={comments} />;
}
return <button onClick={() => setShowComments(true)}>加载评论</button>;
}
完整的数据获取模式
function ProductPage({ productId }) {
// 在服务端或客户端获取数据
const productPromise = getProduct(productId);
const reviewsPromise = getReviews(productId);
return (
<div>
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productPromise={productPromise} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews reviewsPromise={reviewsPromise} />
</Suspense>
</div>
);
}
function ProductDetails({ productPromise }) {
const product = use(productPromise);
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
</article>
);
}
function Reviews({ reviewsPromise }) {
const reviews = use(reviewsPromise);
return (
<section>
<h2>用户评价</h2>
{reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</section>
);
}
6. 服务端组件(Server Components)增强
React 19进一步完善了服务端组件支持。
服务端 vs 客户端组件
// ============================================
// Server Component(默认)
// - 在服务端渲染
// - 可以直接访问数据库、文件系统
// - 不能使用客户端交互(useState、onClick等)
// - 减少发送到客户端的JS
async function ProductList() {
// 直接访问数据库
const products = await db.products.findMany();
return (
<ul>
{products.map(product => (
<li key={product.id}>
<ProductCard product={product} />
</li>
))}
</ul>
);
}
// ============================================
// Client Component(需显式声明)
// - 在客户端渲染和hydration
// - 可以使用所有React特性
// - 不能直接访问服务端资源
'use client';
import { useState } from 'react';
function ProductCard({ product }) {
const [isLiked, setIsLiked] = useState(false);
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<button onClick={() => setIsLiked(!isLiked)}>
{isLiked ? '❤️' : '🤍'}
</button>
</div>
);
}
混合使用模式
// Server Component
async function ProductPage({ productId }) {
// 服务端获取数据
const product = await db.products.find(productId);
const recommendedProducts = await getRecommendations(productId);
return (
<div>
{/* 服务端组件:静态内容 */}
<ProductHeader product={product} />
{/* 客户端组件:交互功能 */}
<AddToCartButton productId={product.id} />
{/* 服务端组件:推荐列表 */}
<ProductRecommendations products={recommendedProducts} />
{/* 客户端组件:评论系统 */}
<ReviewSection productId={product.id} />
</div>
);
}
// 服务端组件
function ProductHeader({ product }) {
return (
<header>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span className="price">${product.price}</span>
</header>
);
}
// 客户端组件
'use client';
function AddToCartButton({ productId }) {
const [quantity, setQuantity] = useState(1);
const handleAdd = async () => {
await addToCart(productId, quantity);
toast.success('已添加到购物车');
};
return (
<div>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
<button onClick={handleAdd}>加入购物车</button>
</div>
);
}
服务端Action
// app/actions/product.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createProduct(formData: FormData) {
const name = formData.get('name') as string;
const price = Number(formData.get('price'));
const description = formData.get('description') as string;
// 验证
if (!name || price <= 0) {
return { error: '请填写有效信息' };
}
// 数据库操作
await db.products.create({
data: { name, price, description }
});
// 重新验证缓存
revalidatePath('/products');
// 重定向
redirect('/products');
}
export async function deleteProduct(id: string) {
await db.products.delete({ where: { id } });
revalidatePath('/products');
}
// app/admin/products/page.tsx
import { createProduct, deleteProduct } from './actions';
export default function AdminProducts() {
return (
<div>
<h1>产品管理</h1>
<form action={createProduct}>
<input name="name" placeholder="产品名称" required />
<input name="price" type="number" step="0.01" required />
<textarea name="description" />
<button type="submit">创建产品</button>
</form>
<ProductList />
</div>
);
}
async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
<form action={deleteProduct.bind(null, product.id)}>
<button type="submit">删除</button>
</form>
</li>
))}
</ul>
);
}
7. 简化的Ref行为
ref作为prop传递,不再需要forwardRef。
对比示例
// ============================================
// React 18及之前:必须使用forwardRef
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => (
<input ref={ref} {...props} className="custom-input" />
));
MyInput.displayName = 'MyInput';
// 使用
function Form() {
const inputRef = useRef(null);
return (
<form>
<MyInput ref={inputRef} placeholder="输入内容" />
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</form>
);
}
// ============================================
// React 19:直接接收ref作为prop
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} className="custom-input" />;
}
// 使用方式完全相同
function Form() {
const inputRef = useRef(null);
return (
<form>
<MyInput ref={inputRef} placeholder="输入内容" />
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</form>
);
}
复合组件的Ref转发
// React 19复合组件示例
function FormField({ ref, label, error, ...props }) {
const id = useId();
return (
<div className="form-field">
<label htmlFor={id}>{label}</label>
<input
ref={ref}
id={id}
aria-invalid={!!error}
aria-describedby={error ? `${id}-error` : undefined}
{...props}
/>
{error && (
<span id={`${id}-error`} className="error">
{error}
</span>
)}
</div>
);
}
// 使用
function ContactForm() {
const emailRef = useRef(null);
return (
<form>
<FormField
ref={emailRef}
label="邮箱"
type="email"
name="email"
placeholder="your@email.com"
/>
</form>
);
}
8. 文档元数据支持
直接在组件中设置meta标签,无需Helmet等库。
基础用法
function BlogPost({ post }) {
return (
<>
{/* 页面标题 */}
<title>{post.title} | 我的博客</title>
{/* 基础meta标签 */}
<meta name="description" content={post.excerpt} />
<meta name="keywords" content={post.tags.join(', ')} />
<meta name="author" content={post.author} />
{/* Open Graph */}
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.coverImage} />
<meta property="og:type" content="article" />
<meta property="og:url" content={`https://example.com/posts/${post.slug}`} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.title} />
<meta name="twitter:description" content={post.excerpt} />
<meta name="twitter:image" content={post.coverImage} />
{/* 文章内容 */}
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</>
);
}
动态SEO组件
function SEOHead({
title,
description,
image = '/og-default.jpg',
url,
type = 'website'
}) {
const fullTitle = `${title} | 我的网站`;
return (
<>
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:type" content={type} />
<meta property="og:url" content={url} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{/* 结构化数据 */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": type === 'article' ? 'Article' : 'WebPage',
"headline": title,
"description": description,
"image": image,
"url": url
})}
</script>
</>
);
}
// 使用
function ProductPage({ product }) {
return (
<>
<SEOHead
title={product.name}
description={product.description}
image={product.images[0]}
url={`https://example.com/products/${product.id}`}
type="product"
/>
{/* 产品内容 */}
</>
);
}
9. 样式表支持
支持样式表优先级管理,避免CSS冲突。
基础用法
function Component() {
return (
<>
{/* 基础样式 */}
<link
rel="stylesheet"
href="/styles/base.css"
precedence="low"
/>
{/* 组件样式 */}
<link
rel="stylesheet"
href="/styles/component.css"
precedence="medium"
/>
{/* 关键样式 */}
<link
rel="stylesheet"
href="/styles/critical.css"
precedence="high"
/>
<div className="styled-content">
内容
</div>
</>
);
}
优先级说明
| 优先级 | 加载顺序 | 适用场景 |
|---|---|---|
| low | 最后加载 | 主题覆盖、第三方库 |
| medium | 中间加载 | 组件样式 |
| high | 最先加载 | 重置样式、关键CSS |
function App() {
return (
<>
{/* 即使组件顺序不同,样式也会按优先级排序 */}
<link rel="stylesheet" href="/theme.css" precedence="low" />
<link rel="stylesheet" href="/critical.css" precedence="high" />
<link rel="stylesheet" href="/components.css" precedence="medium" />
{/* 实际加载顺序:
1. critical.css (high)
2. components.css (medium)
3. theme.css (low)
*/}
</>
);
}
10. 资源预加载
新的API用于优化资源加载。
API列表
import {
preload, // 预加载资源
preinit, // 预初始化脚本
preconnect // 预连接域名
} from 'react-dom';
使用示例
function ProductPage({ productId }) {
// 预连接到API服务器
preconnect('https://api.example.com');
// 预加载字体
preload('/fonts/inter-var.woff2', {
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous'
});
// 预加载图片
preload(`/products/${productId}/hero.jpg`, {
as: 'image'
});
// 预初始化关键脚本
preinit('/scripts/analytics.js', {
as: 'script'
});
return (
<>
<ProductHero productId={productId} />
<ProductDetails productId={productId} />
</>
);
}
预加载策略
// 按需预加载
function ProductCard({ product }) {
const [isHovered, setIsHovered] = useState(false);
// 鼠标悬停时预加载详情页资源
useEffect(() => {
if (isHovered) {
preload(`/products/${product.id}/details.js`, {
as: 'script'
});
preload(`/products/${product.id}/hero.jpg`, {
as: 'image'
});
}
}, [isHovered, product.id]);
return (
<Link
href={`/products/${product.id}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<img src={product.thumbnail} alt={product.name} />
<h3>{product.name}</h3>
</Link>
);
}
// 路由级别预加载
function App() {
const pathname = usePathname();
useEffect(() => {
// 根据当前页面预加载可能访问的下一页
if (pathname === '/') {
preload('/pages/home.js', { as: 'script' });
} else if (pathname.startsWith('/products')) {
preload('/pages/cart.js', { as: 'script' });
}
}, [pathname]);
return <Router />;
}
实战案例:完整的全栈表单应用
项目结构
app/
├── actions/
│ ├── todo.ts # Todo服务端Action
│ └── auth.ts # 认证Action
├── components/
│ ├── TodoForm.tsx # Todo表单组件
│ ├── TodoList.tsx # Todo列表组件
│ └── SubmitButton.tsx # 智能提交按钮
├── lib/
│ └── db.ts # 数据库连接
└── page.tsx # 主页面
服务端Action
// app/actions/todo.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '../lib/db';
import { auth } from '../lib/auth';
// 创建Todo
export async function createTodo(formData: FormData) {
const session = await auth();
if (!session?.user) {
return { error: '请先登录' };
}
const title = formData.get('title') as string;
if (!title?.trim()) {
return { error: '标题不能为空' };
}
await db.todo.create({
data: {
title: title.trim(),
userId: session.user.id
}
});
revalidatePath('/todos');
return { success: true };
}
// 切换完成状态
export async function toggleTodo(id: string) {
const todo = await db.todo.findUnique({ where: { id } });
if (!todo) {
return { error: 'Todo不存在' };
}
await db.todo.update({
where: { id },
data: { completed: !todo.completed }
});
revalidatePath('/todos');
return { success: true };
}
// 删除Todo
export async function deleteTodo(id: string) {
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}
// 更新Todo
export async function updateTodo(id: string, formData: FormData) {
const title = formData.get('title') as string;
if (!title?.trim()) {
return { error: '标题不能为空' };
}
await db.todo.update({
where: { id },
data: { title: title.trim() }
});
revalidatePath('/todos');
return { success: true };
}
客户端组件
// app/components/SubmitButton.tsx
'use client';
import { useFormStatus } from 'react-dom';
interface SubmitButtonProps {
children: React.ReactNode;
loadingText?: string;
className?: string;
}
export function SubmitButton({
children,
loadingText = '处理中...',
className
💬 評論區