epic-react-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Epic Stack: React Patterns and Guidelines

Epic Stack:React 模式与指南

When to use this skill

何时使用此技能

Use this skill when you need to:
  • Write efficient React components in Epic Stack applications
  • Optimize performance and bundle size
  • Follow React Router patterns and conventions
  • Avoid common React anti-patterns
  • Implement proper code splitting
  • Optimize re-renders and data fetching
  • Use React hooks correctly
当你需要以下操作时使用此技能:
  • 在 Epic Stack 应用中编写高效的 React 组件
  • 优化性能与包体积
  • 遵循 React Router 的模式与约定
  • 避免常见的 React 反模式
  • 实现合理的代码分割
  • 优化重渲染与数据获取
  • 正确使用 React Hooks

Philosophy

设计理念

Following Epic Web principles:
  • Make it work, make it right, make it fast - In that order. First make it functional, then refactor for clarity, then optimize for performance.
  • Pragmatism over purity - Choose practical solutions that work well in your context rather than theoretically perfect ones.
  • Optimize for sustainable velocity - Write code that's easy to maintain and extend, not just fast to write initially.
  • Do as little as possible - Only add complexity when it provides real value.
遵循 Epic Web 原则:
  • 先能用,再规范,再高效 - 按此顺序来。先实现功能,再重构以提升代码清晰度,最后优化性能。
  • 实用优先于纯粹 - 选择在你的场景中切实有效的方案,而非理论上完美的方案。
  • 优化可持续开发速度 - 编写易于维护和扩展的代码,而非仅仅是初始编写速度快的代码。
  • 尽可能精简 - 仅在能带来实际价值时才增加复杂度。

Patterns and conventions

模式与约定

Data Fetching in React Router

React Router 中的数据获取

Epic Stack uses React Router loaders for data fetching, not
useEffect
.
✅ Good - Use loaders:
typescript
// 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>
}
❌ Avoid - Don't fetch in useEffect:
typescript
// ❌ 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>
}
Epic Stack 使用 React Router loaders 进行数据获取,而非
useEffect
✅ 推荐写法 - 使用 loaders:
typescript
// 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 中获取数据:
typescript
// ❌ 不要这样做
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>
}

Avoid useEffect for Side Effects

避免使用 useEffect 处理副作用

Instead of using
useEffect
, use event handlers, CSS, ref callbacks, or
useSyncExternalStore
.
✅ Good - Use event handlers:
typescript
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>
	)
}
❌ Avoid - Side effects in useEffect:
typescript
// ❌ 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)
	}

	// ...
}
✅ Appropriate use of useEffect:
typescript
// ✅ 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()
	}
}, [])
替代 useEffect 的方案:使用事件处理函数、CSS、ref 回调或
useSyncExternalStore
✅ 推荐写法 - 使用事件处理函数:
typescript
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 中处理副作用:
typescript
// ❌ 不要这样做
function ProductPage({ product, addToCart }: Route.ComponentProps) {
	useEffect(() => {
		if (product.isInCart) {
			showNotification(`Added ${product.name} to cart!`)
		}
	}, [product])

	function handleBuyClick() {
		addToCart(product)
	}

	// ...
}
✅ useEffect 的合理使用场景:
typescript
// ✅ 合理用法 - 事件监听器场景适用
useEffect(() => {
	const controller = new AbortController()

	window.addEventListener(
		'keydown',
		(event: KeyboardEvent) => {
			if (event.key !== 'Escape') return
			// 处理ESC键逻辑
		},
		{ signal: controller.signal },
	)

	return () => {
		controller.abort()
	}
}, [])

Code Splitting with React Router

React Router 代码分割

React Router automatically code-splits by route. Use dynamic imports for heavy components.
✅ Good - Dynamic imports:
typescript
// 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>
	)
}
React Router 会自动按路由进行代码分割。对大型组件使用动态导入。
✅ 推荐写法 - 动态导入:
typescript
// 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>
	)
}

Optimizing Re-renders

优化重渲染

✅ Good - Memoize expensive computations:
typescript
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>
	)
}
✅ Good - Memoize callbacks:
typescript
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} />
}
❌ Avoid - Unnecessary memoization:
typescript
// ❌ 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
✅ 推荐写法 - 缓存昂贵的计算:
typescript
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>
	)
}
✅ 推荐写法 - 缓存回调函数:
typescript
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} />
}
❌ 避免写法 - 不必要的缓存:
typescript
// ❌ 不要缓存简单值
const count = useMemo(() => items.length, [items]) // 直接使用 items.length 即可

// ❌ 不要缓存简单回调函数
const handleClick = useCallback(() => {
	console.log('clicked')
}, []) // 如果不需要缓存,直接定义函数即可

Bundle Size Optimization

包体积优化

✅ Good - Import only what you need:
typescript
// ✅ Import specific functions
import { useSearchParams } from 'react-router'
import { parseWithZod } from '@conform-to/zod'
❌ Avoid - Barrel imports:
typescript
// ❌ Don't import entire libraries if you only need one thing
import * as ReactRouter from 'react-router'
import * as Conform from '@conform-to/zod'
✅ 推荐写法 - 按需导入:
typescript
// ✅ 导入特定函数
import { useSearchParams } from 'react-router'
import { parseWithZod } from '@conform-to/zod'
❌ 避免写法 - 桶导入:
typescript
// ❌ 不要在只需要单个功能时导入整个库
import * as ReactRouter from 'react-router'
import * as Conform from '@conform-to/zod'

Form Handling with Conform

使用 Conform 处理表单

✅ Good - Use Conform for forms:
typescript
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>
	)
}
✅ 推荐写法 - 使用 Conform 处理表单:
typescript
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>
	)
}

Component Composition

组件组合

✅ Good - Compose components:
typescript
function UserProfile({ user }: { user: User }) {
	return (
		<Card>
			<UserHeader user={user} />
			<UserDetails user={user} />
			<UserActions userId={user.id} />
		</Card>
	)
}
❌ Avoid - Large monolithic components:
typescript
// ❌ 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>
	)
}
✅ 推荐写法 - 组件组合:
typescript
function UserProfile({ user }: { user: User }) {
	return (
		<Card>
			<UserHeader user={user} />
			<UserDetails user={user} />
			<UserActions userId={user.id} />
		</Card>
	)
}
❌ 避免写法 - 大型单体组件:
typescript
// ❌ 不要把所有内容放在一个组件中
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>
	)
}

Error Boundaries

错误边界

✅ Good - Use error boundaries:
typescript
// app/routes/users/$username.tsx
export function ErrorBoundary() {
	return (
		<GeneralErrorBoundary
			statusHandlers={{
				404: ({ params }) => (
					<p>User "{params.username}" not found</p>
				),
			}}
		/>
	)
}
✅ 推荐写法 - 使用错误边界:
typescript
// app/routes/users/$username.tsx
export function ErrorBoundary() {
	return (
		<GeneralErrorBoundary
			statusHandlers={{
				404: ({ params }) => (
					<p>用户 "{params.username}" 未找到</p>
				),
			}}
		/>
	)
}

TypeScript Guidelines

TypeScript 指南

✅ Good - Type props explicitly:
typescript
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>
	)
}
✅ Good - Use Route types:
typescript
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>
}
✅ 推荐写法 - 显式定义属性类型:
typescript
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 类型:
typescript
import type { Route } from './+types/users.$username'

export async function loader({ params }: Route.LoaderArgs) {
	// params 是类型安全的!
	const user = await prisma.user.findUnique({
		where: { username: params.username },
	})
	return { user }
}

export default function UserRoute({ loaderData }: Route.ComponentProps) {
	// loaderData 是类型安全的!
	return <div>{loaderData.user.name}</div>
}

Loading States

加载状态处理

✅ Good - Use React Router's pending states:
typescript
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 的 pending 状态:
typescript
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 ? '保存中...' : '保存'}
			</button>
		</Form>
	)
}

Preventing Data Fetching Waterfalls

避免数据获取瀑布流

React Router loaders can prevent waterfalls by fetching data in parallel.
❌ Avoid - Sequential data fetching (waterfall):
typescript
// ❌ 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 }
}
✅ Good - Parallel data fetching:
typescript
// ✅ 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 }
}
✅ Good - Nested route parallel loading:
typescript
// 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 }
}
React Router loaders 可以通过并行获取数据来避免瀑布流。
❌ 避免写法 - 串行数据获取(瀑布流):
typescript
// ❌ 不要这样做 - 会产生瀑布流
export async function loader({ params }: Route.LoaderArgs) {
	const user = await prisma.user.findUnique({
		where: { username: params.username },
	})
	// 第二次请求需等待第一次完成
	const notes = await prisma.note.findMany({
		where: { ownerId: user.id },
	})
	return { user, notes }
}
✅ 推荐写法 - 并行数据获取:
typescript
// ✅ 并行获取数据
export async function loader({ params }: Route.LoaderArgs) {
	const user = await prisma.user.findUnique({
		where: { username: params.username },
		select: { id: true, username: true, name: true },
	})

	// 与用户数据并行获取笔记
	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 }
}
✅ 推荐写法 - 嵌套路由并行加载:
typescript
// 父路由 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 }
}

// 子路由 loader 并行运行
// 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 }
}

Server-Side Rendering (SSR) Performance

服务端渲染(SSR)性能

React Router provides SSR by default. Optimize by:
✅ Good - Selective data fetching:
typescript
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 }
}
✅ Good - Use caching for expensive operations:
typescript
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 Router 默认支持 SSR。可通过以下方式优化:
✅ 推荐写法 - 选择性数据获取:
typescript
export async function loader({ request }: Route.LoaderArgs) {
	// 仅获取初始渲染所需的数据
	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 }, // 仅获取所需字段
		}),
		prisma.item.count(),
	])

	return { items, total, page }
}
✅ 推荐写法 - 对昂贵操作使用缓存:
typescript
import { cachified, cache } from '#app/utils/cache.server.ts'

export async function loader({ request }: Route.LoaderArgs) {
	const timings: Timings = {}

	// 缓存昂贵的数据库查询
	const stats = await cachified({
		key: 'user-stats',
		cache,
		timings,
		getFreshValue: async () => {
			return await prisma.user.aggregate({
				_count: { id: true },
			})
		},
		ttl: 1000 * 60 * 5, // 5分钟
	})

	return { stats }
}

Rendering Performance

渲染性能

✅ Good - Use React.memo for expensive components:
typescript
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} />
}
✅ Good - Optimize list rendering:
typescript
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>
	)
}
❌ Avoid - Creating new objects/arrays in render:
typescript
// ❌ 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.memo:
typescript
import { memo } from 'react'

const ExpensiveChart = memo(function ExpensiveChart({ data }: { data: Data[] }) {
	// 昂贵的渲染逻辑
	return <Chart data={data} />
})

// 仅在 data 变化时重渲染
export default function Dashboard({ chartData }: { chartData: Data[] }) {
	return <ExpensiveChart data={chartData} />
}
✅ 推荐写法 - 优化列表渲染:
typescript
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>
	)
}
❌ 避免写法 - 在渲染时创建新对象/数组:
typescript
// ❌ 不要在每次渲染时创建新对象
function UserProfile({ user }: { user: User }) {
	return <Card user={{ ...user, fullName: `${user.firstName} ${user.lastName}` }} />
}

// ✅ 推荐写法 - 在 loader 中计算或缓存
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}`,
		},
	}
}

Bundle Size Optimization Strategies

包体积优化策略

✅ Good - Route-based code splitting: React Router automatically splits code by route. Leverage this:
typescript
// Heavy dependencies are automatically split by route
// app/routes/admin/dashboard.tsx
import { Chart } from 'chart.js' // Only loaded on /admin/dashboard route
✅ Good - Dynamic imports for heavy components:
typescript
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>
	)
}
✅ Good - Tree-shakeable imports:
typescript
// ✅ 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 Router 会自动按路由分割代码。充分利用这一特性:
typescript
// 大型依赖会自动按路由分割
// app/routes/admin/dashboard.tsx
import { Chart } from 'chart.js' // 仅在 /admin/dashboard 路由加载
✅ 推荐写法 - 对大型组件使用动态导入:
typescript
import { lazy, Suspense } from 'react'

const HeavyComponent = lazy(() => import('#app/components/heavy-component.tsx'))

export default function Route() {
	return (
		<Suspense fallback={<div>加载中...</div>}>
			<HeavyComponent />
		</Suspense>
	)
}
✅ 推荐写法 - 支持树摇的导入:
typescript
// ✅ 支持树摇 - 仅导入所需内容
import { format } from 'date-fns/format'
import { addDays } from 'date-fns/addDays'

// ❌ 避免写法 - 导入整个库
import * as dateFns from 'date-fns'

React 18+ Features for Performance

React 18+ 性能特性

✅ Good - Use transitions for non-urgent updates:
typescript
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'}
		/>
	)
}
✅ 推荐写法 - 对非紧急更新使用过渡:
typescript
import { useTransition } from 'react'
import { useNavigation } from 'react-router'

function SearchInput() {
	const [isPending, startTransition] = useTransition()
	const navigation = useNavigation()

	function handleSearch(query: string) {
		startTransition(() => {
			// 更新搜索结果(非紧急)
			navigation.navigate(`/search?q=${query}`)
		})
	}

	return (
		<input
			onChange={(e) => handleSearch(e.target.value)}
			placeholder={isPending ? '搜索中...' : '搜索'}
		/>
	)
}

Common mistakes to avoid

需避免的常见错误

  • Fetching data in useEffect: Use React Router loaders instead
  • Overusing useEffect: Prefer event handlers, CSS, or ref callbacks
  • Premature memoization: Only memoize when there's a measurable performance benefit
  • Barrel imports: Import only what you need
  • Ignoring TypeScript types: Use Route types for type safety
  • Not handling loading states: Use React Router's navigation states
  • Large monolithic components: Break components into smaller, focused pieces
  • Not using error boundaries: Always add error boundaries to routes
  • Client-side routing when server-side works: Prefer server-side data fetching
  • Data fetching waterfalls: Use
    Promise.all()
    to fetch data in parallel
  • Fetching unnecessary data: Only fetch what's needed for the initial render
  • Creating new objects in render: Compute derived data in loaders or memoize
  • Not using React.memo for expensive lists: Memoize list items for better performance
  • Not leveraging route-based code splitting: React Router splits by route automatically
  • 在 useEffect 中获取数据:改用 React Router loaders
  • 过度使用 useEffect:优先使用事件处理函数、CSS 或 ref 回调
  • 过早缓存:仅在有可衡量的性能收益时才进行缓存
  • 桶导入:按需导入
  • 忽略 TypeScript 类型:使用 Route 类型确保类型安全
  • 不处理加载状态:使用 React Router 的导航状态
  • 大型单体组件:将组件拆分为更小、职责单一的模块
  • 不使用错误边界:始终为路由添加错误边界
  • 在服务端方案可行时使用客户端路由:优先选择服务端数据获取
  • 数据获取瀑布流:使用
    Promise.all()
    并行获取数据
  • 获取不必要的数据:仅获取初始渲染所需的数据
  • 在渲染时创建新对象:在 loaders 中计算派生数据或进行缓存
  • 不为大型列表使用 React.memo:为列表项添加缓存以提升性能
  • 不利用基于路由的代码分割:React Router 会自动按路由分割代码

References

参考资料