name: react-patterns description: React 19 模式,包括服务器组件、操作、Suspense、钩子和组件组合
React 模式
use() 钩子 (React 19)
use() 直接从 Promise 和 Context 中读取值在渲染中。与其他钩子不同,它可以在条件语句和循环中调用。
import { use } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
function ThemeButton() {
const theme = use(ThemeContext);
return <button style={{ background: theme.primary }}>Click</button>;
}
使用 use() 与 Promise 的组件需要用 <Suspense> 边界包裹。
服务器组件
// app/users/page.tsx - 服务器组件(默认,无需指令)
import { UserList } from './UserList';
export default async function UsersPage() {
const users = await fetch('https://api.example.com/users', {
next: { revalidate: 60 },
}).then(r => r.json());
return <UserList users={users} />;
}
// app/users/UserList.tsx - 仍然是服务器组件
export function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(u => (
<li key={u.id}>
{u.name}
<DeleteButton userId={u.id} />
</li>
))}
</ul>
);
}
尽可能深地推送 'use client'。只有需要交互性的叶子节点才应该是客户端组件。
服务器操作
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
await db.insert(posts).values({ title, body });
revalidatePath('/posts');
redirect('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="body" required />
<button type="submit">Create</button>
</form>
);
}
useActionState (React 19)
'use client';
import { useActionState } from 'react';
import { createUser } from './actions';
function SignupForm() {
const [state, formAction, isPending] = useActionState(createUser, {
errors: {},
message: '',
});
return (
<form action={formAction}>
<input name="email" />
{state.errors.email && <p>{state.errors.email}</p>}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Sign Up'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
useOptimistic (React 19)
'use client';
import { useOptimistic } from 'react';
import { likePost } from './actions';
function LikeButton({ count, postId }: { count: number; postId: string }) {
const [optimisticCount, addOptimistic] = useOptimistic(count);
async function handleLike() {
addOptimistic(prev => prev + 1);
await likePost(postId);
}
return (
<form action={handleLike}>
<button type="submit">{optimisticCount} Likes</button>
</form>
);
}
Suspense 边界
import { Suspense } from 'react';
function Dashboard() {
return (
<div>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<div className="grid grid-cols-2">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ListSkeleton />}>
<RecentActivity />
</Suspense>
</div>
</div>
);
}
将 Suspense 边界放在独立的数据获取单元周围。避免将整个页面包裹在单个边界中(这会破坏流式传输的目的)。
错误边界
'use client';
import { Component, type ReactNode } from 'react';
class ErrorBoundary extends Component<
{ fallback: ReactNode; children: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
reportError(error, info.componentStack);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
或者使用 Next.js error.tsx 约定进行路由级错误处理。
自定义钩子
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
自定义钩子的规则:
- 以
use为前缀 - 当逻辑在 2 个或更多组件之间共享时提取
- 保持钩子专注于单个关注点
- 返回元组
[value, setter]或对象{ data, error, loading }
复合组件
function Tabs({ children }: { children: ReactNode }) {
const [active, setActive] = useState(0);
return (
<TabsContext value={{ active, setActive }}>
<div role="tablist">{children}</div>
</TabsContext>
);
}
Tabs.Tab = function Tab({ index, children }: { index: number; children: ReactNode }) {
const { active, setActive } = use(TabsContext);
return (
<button
role="tab"
aria-selected={active === index}
onClick={() => setActive(index)}
>
{children}
</button>
);
};
Tabs.Panel = function Panel({ index, children }: { index: number; children: ReactNode }) {
const { active } = use(TabsContext);
if (active !== index) return null;
return <div role="tabpanel">{children}</div>;
};
// 用法
<Tabs>
<Tabs.Tab index={0}>Profile</Tabs.Tab>
<Tabs.Tab index={1}>Settings</Tabs.Tab>
<Tabs.Panel index={0}><ProfileForm /></Tabs.Panel>
<Tabs.Panel index={1}><SettingsForm /></Tabs.Panel>
</Tabs>
性能规则
- 避免在 JSX 属性中创建对象/数组(导致重新渲染)
- 仅在性能分析确认有不必要的重新渲染后使用
React.memo - 对于昂贵的计算或传递给记忆化子组件的稳定引用,优先使用
useMemo/useCallback - 使用
key来有意重置组件状态 - 状态共置:将状态保持在尽可能接近使用它的地方
- 避免属性传递超过 2 级;使用 Context 或组合代替