关于 epic-react-patterns
为 Epic Stack 应用中的 React 开发提供模式与规范,面向编写和维护 React 界面的开发者。内容涵盖 React Router 数据加载、组件设计、性能优化和常见反模式。
主要功能
- 01使用 React Router loaders 获取数据
- 02减少不必要的 useEffect
- 03按路由拆分代码并延迟加载组件
- 04优化重渲染与资源包体积
- 05处理表单、加载状态和错误边界
适用场景
- 01为路由页面设计数据加载逻辑
- 02优化大型组件和列表的渲染性能
- 03检查 React 代码中的常见反模式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicweb-dev/ epic-stack --skill epic-react-patterns安装后在 Claude Code 中输入/epic-react-patterns确认已加载。
详细介绍
Epic Stack:React 模式与指南
何时使用此技能
在以下情况下使用此技能:
- 在 Epic Stack 应用中编写高效的 React 组件
- 优化性能和包体积
- 遵循 React Router 模式和约定
- 避免常见的 React 反模式
- 实现恰当的代码拆分
- 优化重复渲染和数据获取
- 正确使用 React Hooks
理念
遵循 Epic Web 原则:
- 先让它运行,再让它正确,最后让它快速 —— 按此顺序。先确保功能可用,再重构以提升清晰度,最后优化性能。
- 务实胜过纯粹 —— 选择适合当前情境且效果良好的实用方案,而不是理论上完美的方案。
- 以可持续的开发速度为目标 —— 编写易于维护和扩展的代码,而不只是追求最初编写速度快。
- 尽可能少做 —— 只有在确实能带来价值时才增加复杂性。
模式与约定
在 React Router 中获取数据
Epic Stack 使用 React Router loaders 获取数据,而不是 useEffect。
✅ 推荐 - 使用 loaders:
// app/routes/users/$username.tsx
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
})
return { user }
}
export default function UserRoute({ loaderData }: Route.ComponentProps) {
return <div>{loaderData.user.name}</div>
}
❌ 避免 - 不要在 useEffect 中获取数据:
// ❌ Don't do this
export default function UserRoute({ params }: Route.ComponentProps) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${params.username}`)
.then(res => res.json())
.then(setUser)
}, [params.username])
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
避免使用 useEffect 处理副作用
不要使用 useEffect,改用事件处理程序、CSS、ref 回调或
useSyncExternalStore。
✅ 推荐 - 使用事件处理程序:
function ProductPage({ product, addToCart }: Route.ComponentProps) {
function buyProduct() {
addToCart(product)
showNotification(`Added ${product.name} to cart!`)
}
function handleBuyClick() {
buyProduct()
}
function handleCheckoutClick() {
buyProduct()
navigate('/checkout')
}
return (
<div>
<button onClick={handleBuyClick}>Buy Now</button>
<button onClick={handleCheckoutClick}>Checkout</button>
</div>
)
}
❌ 避免 - 在 useEffect 中处理副作用:
// ❌ Don't do this
function ProductPage({ product, addToCart }: Route.ComponentProps) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart!`)
}
}, [product])
function handleBuyClick() {
addToCart(product)
}
// ...
}
✅ 适合使用 useEffect 的情况:
// ✅ Good - Event listeners are appropriate
useEffect(() => {
const controller = new AbortController()
window.addEventListener(
'keydown',
(event: KeyboardEvent) => {
if (event.key !== 'Escape') return
// handle escape key
},
{ signal: controller.signal },
)
return () => {
controller.abort()
}
}, [])
使用 React Router 拆分代码
React Router 会按路由自动拆分代码。对于较大的组件,请使用动态导入。
✅ 推荐 - 使用动态导入:
// app/routes/admin/dashboard.tsx
import { lazy } from 'react'
const AdminChart = lazy(() => import('#app/components/admin/chart.tsx'))
export default function AdminDashboard() {
return (
<Suspense fallback={<div>Loading chart...</div>}>
<AdminChart />
</Suspense>
)
}
优化重复渲染
✅ 推荐 - 对开销较大的计算使用记忆化:
import { useMemo } from 'react'
function UserList({ users }: { users: User[] }) {
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => a.name.localeCompare(b.name))
}, [users])
return (
<ul>
{sortedUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
✅ 推荐 - 对回调使用记忆化:
import { useCallback } from 'react'
function NoteEditor({ noteId, onSave }: { noteId: string; onSave: (note: Note) => void }) {
const handleSave = useCallback((note: Note) => {
onSave(note)
}, [onSave])
return <Editor onSave={handleSave} />
}
❌ 避免 - 不必要的记忆化:
// ❌ Don't memoize simple values
const count = useMemo(() => items.length, [items]) // Just use items.length directly
// ❌ Don't memoize simple callbacks
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // Just define the function normally if it doesn't need memoization
优化包体积
✅ 推荐 - 只导入所需内容:
// ✅ Import specific functions
import { useSearchParams } from 'react-router'
import { parseWithZod } from '@conform-to/zod'
❌ 避免 - 使用聚合导入:
// ❌ Don't import entire libraries if you only need one thing
import * as ReactRouter from 'react-router'
import * as Conform from '@conform-to/zod'
使用 Conform 处理表单
✅ 推荐 - 使用 Conform 处理表单:
import { useForm, getFormProps } from '@conform-to/react'
import { parseWithZod } from '@conform-to/zod'
import { Form } from 'react-router'
const SignupSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
export default function SignupRoute({ actionData }: Route.ComponentProps) {
const [form, fields] = useForm({
id: 'signup-form',
lastResult: actionData?.result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: SignupSchema })
},
})
return (
<Form method="POST" {...getFormProps(form)}>
{/* form fields */}
</Form>
)
}
组件组合
✅ 推荐 - 组合组件:
function UserProfile({ user }: { user: User }) {
return (
<Card>
<UserHeader user={user} />
<UserDetails user={user} />
<UserActions userId={user.id} />
</Card>
)
}
❌ 避免 - 编写庞大的单体组件:
// ❌ Don't put everything in one component
function UserProfile({ user }: { user: User }) {
return (
<div className="card">
<div className="header">
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
</div>
<div className="details">
<p>{user.email}</p>
<p>{user.bio}</p>
</div>
<div className="actions">
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
错误边界
✅ 推荐 - 使用错误边界:
// app/routes/users/$username.tsx
export function ErrorBoundary() {
return (
<GeneralErrorBoundary
statusHandlers={{
404: ({ params }) => (
<p>User "{params.username}" not found</p>
),
}}
/>
)
}
TypeScript 指南
✅ 推荐 - 显式指定 props 类型:
interface UserCardProps {
user: {
id: string
name: string
email: string
}
onEdit?: (userId: string) => void
}
function UserCard({ user, onEdit }: UserCardProps) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
{onEdit && <button onClick={() => onEdit(user.id)}>Edit</button>}
</div>
)
}
✅ 推荐 - 使用 Route 类型:
import type { Route } from './+types/users.$username'
export async function loader({ params }: Route.LoaderArgs) {
// params is type-safe!
const user = await prisma.user.findUnique({
where: { username: params.username },
})
return { user }
}
export default function UserRoute({ loaderData }: Route.ComponentProps) {
// loaderData is type-safe!
return <div>{loaderData.user.name}</div>
}
加载状态
✅ 推荐 - 使用 React Router 的待处理状态:
import { useNavigation } from 'react-router'
function NoteForm() {
const navigation = useNavigation()
const isSubmitting = navigation.state === 'submitting'
return (
<Form method="POST">
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</Form>
)
}
避免数据获取瀑布
React Router loaders 可以并行获取数据,避免瀑布式请求。
❌ 避免 - 顺序获取数据(瀑布式请求):
// ❌ Don't do this - creates a waterfall
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
})
// Second fetch waits for first to complete
const notes = await prisma.note.findMany({
where: { ownerId: user.id },
})
return { user, notes }
}
✅ 推荐 - 并行获取数据:
// ✅ Fetch data in parallel
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
select: { id: true, username: true, name: true },
})
// Fetch notes in parallel with user data
const [notes, stats] = await Promise.all([
user
? prisma.note.findMany({
where: { ownerId: user.id },
select: { id: true, title: true, updatedAt: true },
})
: Promise.resolve([]),
user
? prisma.note.count({ where: { ownerId: user.id } })
: Promise.resolve(0),
])
return { user, notes, stats }
}
✅ 推荐 - 嵌套路由并行加载:
// Parent route loader
// app/routes/users/$username.tsx
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
select: { id: true, username: true, name: true },
})
return { user }
}
// Child route loader runs in parallel
// app/routes/users/$username/notes.tsx
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
select: { id: true },
})
if (!user) {
throw new Response('Not Found', { status: 404 })
}
const notes = await prisma.note.findMany({
where: { ownerId: user.id },
select: { id: true, title: true, updatedAt: true },
})
return { notes }
}
服务端渲染(SSR)性能
React Router 默认提供 SSR。可通过以下方式进行优化:
✅ 推荐 - 有选择地获取数据:
export async function loader({ request }: Route.LoaderArgs) {
// Only fetch what's needed for initial render
const searchParams = new URL(request.url).searchParams
const page = Number(searchParams.get('page') || '1')
const [items, total] = await Promise.all([
prisma.item.findMany({
take: 20,
skip: (page - 1) * 20,
select: { id: true, title: true }, // Only needed fields
}),
prisma.item.count(),
])
return { items, total, page }
}
✅ 推荐 - 为开销较大的操作使用缓存:
import { cachified, cache } from '#app/utils/cache.server.ts'
export async function loader({ request }: Route.LoaderArgs) {
const timings: Timings = {}
// Cache expensive database queries
const stats = await cachified({
key: 'user-stats',
cache,
timings,
getFreshValue: async () => {
return await prisma.user.aggregate({
_count: { id: true },
})
},
ttl: 1000 * 60 * 5, // 5 minutes
})
return { stats }
}
渲染性能
✅ 推荐 - 对开销较大的组件使用 React.memo:
import { memo } from 'react'
const ExpensiveChart = memo(function ExpensiveChart({ data }: { data: Data[] }) {
// Expensive rendering logic
return <Chart data={data} />
})
// Only re-renders when data changes
export default function Dashboard({ chartData }: { chartData: Data[] }) {
return <ExpensiveChart data={chartData} />
}
✅ 推荐 - 优化列表渲染:
import { memo } from 'react'
const UserItem = memo(function UserItem({ user }: { user: User }) {
return (
<li>
<h3>{user.name}</h3>
<p>{user.email}</p>
</li>
)
}, (prev, next) => prev.user.id === next.user.id)
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(user => (
<UserItem key={user.id} user={user} />
))}
</ul>
)
}
❌ 避免 - 在渲染过程中创建新的对象/数组:
// ❌ Don't create new objects on every render
function UserProfile({ user }: { user: User }) {
return <Card user={{ ...user, fullName: `${user.firstName} ${user.lastName}` }} />
}
// ✅ Good - Compute in loader or memoize
export async function loader({ params }: Route.LoaderArgs) {
const user = await prisma.user.findUnique({
where: { username: params.username },
select: { firstName: true, lastName: true },
})
return {
user: {
...user,
fullName: `${user.firstName} ${user.lastName}`,
},
}
}
包体积优化策略
✅ 推荐 - 按路由拆分代码: React Router 会按路由自动拆分代码。 充分利用这一点:
// Heavy dependencies are automatically split by route
// app/routes/admin/dashboard.tsx
import { Chart } from 'chart.js' // Only loaded on /admin/dashboard route
✅ 推荐 - 对较大的组件使用动态导入:
import { lazy, Suspense } from 'react'
const HeavyComponent = lazy(() => import('#app/components/heavy-component.tsx'))
export default function Route() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)
}
✅ 推荐 - 使用支持 Tree Shaking 的导入:
// ✅ Tree-shakeable - only imports what you use
import { format } from 'date-fns/format'
import { addDays } from 'date-fns/addDays'
// ❌ Avoid - imports entire library
import * as dateFns from 'date-fns'
React 18+ 性能特性
✅ 推荐 - 对非紧急更新使用 transitions:
import { useTransition } from 'react'
import { useNavigation } from 'react-router'
function SearchInput() {
const [isPending, startTransition] = useTransition()
const navigation = useNavigation()
function handleSearch(query: string) {
startTransition(() => {
// Update search results (non-urgent)
navigation.navigate(`/search?q=${query}`)
})
}
return (
<input
onChange={(e) => handleSearch(e.target.value)}
placeholder={isPending ? 'Searching...' : 'Search'}
/>
)
}
应避免的常见错误
- ❌ 在 useEffect 中获取数据:改用 React Router 加载器
- ❌ 过度使用 useEffect:优先使用事件处理器、CSS 或 ref 回调
- ❌ 过早进行记忆化:只有在能带来可衡量的性能提升时才进行记忆化
- ❌ 使用汇总导入:只导入需要的内容
- ❌ 忽略 TypeScript 类型:使用 Route 类型确保类型安全
- ❌ 未处理加载状态:使用 React Router 的导航状态
- ❌ 组件过大且臃肿:将组件拆分为更小、更专注的部分
- ❌ 未使用错误边界:始终为路由添加错误边界
- ❌ 明明可在服务端处理却使用客户端路由:优先在服务端获取数据
- ❌ 数据获取形成瀑布:使用
Promise.all()并行获取数据 - ❌ 获取不必要的数据:只获取初始渲染所需的数据
- ❌ 在渲染时创建新对象:在加载器中计算派生数据,或对其进行记忆化
- ❌ 未对开销较大的列表使用 React.memo:对列表项进行记忆化,以提升性能
- ❌ 未充分利用基于路由的代码拆分:React Router 会自动按路由拆分
参考资料
- React Router 文档
- React 文档——你可能不需要 useEffect
- Conform 文档
- Epic Stack 文档
- Epic Web 原则
app/routes/- 使用这些模式的示例路由.cursor/rules/avoid-use-effect.mdc- Epic Stack 中用于避免使用 useEffect 的规则