mcpField
E
epic-react-patterns

epic-react-patterns

github.com/epicweb-dev开发5.5k45 次安装3 天前更新
代码仓库

关于 epic-react-patterns

为 Epic Stack 应用中的 React 开发提供模式与规范,面向编写和维护 React 界面的开发者。内容涵盖 React Router 数据加载、组件设计、性能优化和常见反模式。

主要功能

  1. 01使用 React Router loaders 获取数据
  2. 02减少不必要的 useEffect
  3. 03按路由拆分代码并延迟加载组件
  4. 04优化重渲染与资源包体积
  5. 05处理表单、加载状态和错误边界

适用场景

  1. 01为路由页面设计数据加载逻辑
  2. 02优化大型组件和列表的渲染性能
  3. 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

不要使用 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 会自动按路由拆分

参考资料

评论

支持 Markdown