
模式仍然重要(即使有 Hook)
React Hooks 改变了我们编写组件的方式,但组件设计的基本模式——关注点分离、组合优于继承、控制反转——仍然和以往一样重要。
不同之处在于,hooks 使某些模式更容易实现,而某些模式则变得不那么必要。理解哪些模式解决哪些问题,是区分可维护的 React 代码库和积累技术债务的代码库的关键。

自定义 Hook:通用模式
自定义 Hook 不仅仅是重用有状态逻辑——它们是将做什么与怎么做分离开来:
// ❌ 逻辑与展示纠缠在一起
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetchUser(userId)
.then(data => { if (!cancelled) setUser(data) })
.catch(err => { if (!cancelled) setError(err) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [userId])
if (loading) return <Spinner />
if (error) return <ErrorDisplay error={error} />
if (!user) return null
return <div>{user.name}</div>
}
// ✅ 分离到自定义 Hook 中
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetchUser(userId)
.then(data => { if (!cancelled) setUser(data) })
.catch(err => { if (!cancelled) setError(err) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [userId])
return { user, loading, error }
}
// 干净、可测试的组件
function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId)
if (loading) return <Spinner />
if (error) return <ErrorDisplay error={error} />
if (!user) return null
return <div>{user.name}</div>
}
自定义 Hook 模式
// 模式:封装复杂状态机
type Status = 'idle' | 'loading' | 'success' | 'error'
function useAsync<T>(asyncFn: () => Promise<T>) {
const [status, setStatus] = useState<Status>('idle')
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const execute = useCallback(async () => {
setStatus('loading')
setData(null)
setError(null)
try {
const result = await asyncFn()
setData(result)
setStatus('success')
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)))
setStatus('error')
}
}, [asyncFn])
return { status, data, error, execute, isLoading: status === 'loading' }
}
// 用法:
const { data, isLoading, execute } = useAsync(() => fetchUser(userId))
// 模式:外部存储同步
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = useCallback((value: T | ((val: T) => T)) => {
setStoredValue(prev => {
const valueToStore = value instanceof Function ? value(prev) : value
if (typeof window !== 'undefined') {
localStorage.setItem(key, JSON.stringify(valueToStore))
}
return valueToStore
})
}, [key])
return [storedValue, setValue] as const
}
// 用法:
const [theme, setTheme] = useLocalStorage('theme', 'light')
复合组件
复合组件创建灵活且可读的 API——它们让父组件和子组件共享隐式状态,而无需 prop 逐层传递:
// ❌ 单体组件,属性过多
<Tabs
tabs={[{ label: 'Tab 1', content: <Content1 /> }, { label: 'Tab 2', content: <Content2 /> }]}
activeTab={active}
onTabChange={setActive}
tabStyle="underline"
contentStyle="padded"
/>
// ✅ 复合组件——灵活、可读
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="tab1"><Content1 /></Tabs.Content>
<Tabs.Content value="tab2"><Content2 /></Tabs.Content>
</Tabs>
实现:
// 用于共享状态的 Context
const TabsContext = createContext<{
value: string
onChange: (value: string) => void
} | null>(null)
function useTabs() {
const ctx = useContext(TabsContext)
if (!ctx) throw new Error('useTabs must be used within <Tabs>')
return ctx
}
// 根组件管理状态
function Tabs({ defaultValue, children }: { defaultValue: string, children: React.ReactNode }) {
const [value, setValue] = useState(defaultValue)
return (
<TabsContext.Provider value={{ value, onChange: setValue }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
// 子组件消费 context
function TabsList({ children }: { children: React.ReactNode }) {
return <div role="tablist" className="tabs-list">{children}</div>
}
function Tab({ value, children }: { value: string, children: React.ReactNode }) {
const { value: activeValue, onChange } = useTabs()
const isActive = value === activeValue
return (
<button
role="tab"
aria-selected={isActive}
className={isActive ? 'tab active' : 'tab'}
onClick={() => onChange(value)}
>
{children}
</button>
)
}
function TabsContent({ value, children }: { value: string, children: React.ReactNode }) {
const { value: activeValue } = useTabs()
if (value !== activeValue) return null
return <div role="tabpanel">{children}</div>
}
// 将子组件附加到根组件
Tabs.List = TabsList
Tabs.Tab = Tab
Tabs.Content = TabsContent
渲染属性(仍然有用)
渲染属性随着 hooks 的出现而不再流行,但它们仍然是共享渲染逻辑同时让消费者完全控制输出的最佳解决方案:
// 虚拟列表——消费者控制每个项目的渲染方式
interface VirtualListProps<T> {
items: T[]
itemHeight: number
renderItem: (item: T, index: number) => React.ReactNode
containerHeight: number
}
function VirtualList<T>({ items, itemHeight, renderItem, containerHeight }: VirtualListProps<T>) {
const [scrollTop, setScrollTop] = useState(0)
const startIndex = Math.floor(scrollTop / itemHeight)
const visibleCount = Math.ceil(containerHeight / itemHeight)
const endIndex = Math.min(startIndex + visibleCount + 1, items.length)
const visibleItems = items.slice(startIndex, endIndex)
const totalHeight = items.length * itemHeight
const offsetY = startIndex * itemHeight
return (
<div
style={{ height: containerHeight, overflowY: 'auto' }}
onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, i) => renderItem(item, startIndex + i))}
</div>
</div>
</div>
)
}
// 用法——消费者决定渲染方式
<VirtualList
items={users}
itemHeight={60}
containerHeight={400}
renderItem={(user, index) => (
<UserRow key={user.id} user={user} highlighted={index % 2 === 0} />
)}
/>

受控与非受控组件
这是 React 中最常见的 bug 来源之一:
// 非受控——组件拥有自己的状态
// 使用场景:简单、独立、表单数据仅在提交时需要
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null)
function handleSubmit() {
console.log(inputRef.current?.value) // 按需读取
}
return <input ref={inputRef} defaultValue="initial" />
}
// 受控——父组件拥有状态
// 使用场景:实时验证、依赖字段、外部状态
function ControlledInput({ value, onChange }: { value: string, onChange: (v: string) => void }) {
return <input value={value} onChange={e => onChange(e.target.value)} />
}
灵活的“受控或非受控”模式
像 Radix UI 这样的库使用这种模式——组件可以双向工作:
interface AccordionProps {
// 受控 API
value?: string | null
onValueChange?: (value: string | null) => void
// 非受控 API
defaultValue?: string | null
}
function Accordion({ value: controlledValue, onValueChange, defaultValue }: AccordionProps) {
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? null)
// 如果提供了 value prop,则使用受控模式
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
function handleChange(newValue: string | null) {
if (!isControlled) {
setUncontrolledValue(newValue) // 内部管理状态
}
onValueChange?.(newValue) // 始终通知父组件
}
return (/* ... */)
}
// 非受控方式工作(简单用法):
<Accordion defaultValue="item-1">
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
// 受控方式工作(复杂用法):
<Accordion value={openItem} onValueChange={setOpenItem}>
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
复杂 UI 的状态机
对于具有非平凡状态逻辑的组件,useReducer + TypeScript 可辨识联合比多个 useState 调用更易于维护:
type UploadState =
| { status: 'idle' }
| { status: 'selecting' }
| { status: 'uploading'; progress: number; file: File }
| { status: 'success'; url: string; file: File }
| { status: 'error'; error: string; file: File }
type UploadAction =
| { type: 'SELECT' }
| { type: 'START'; file: File }
| { type: 'PROGRESS'; progress: number }
| { type: 'SUCCESS'; url: string }
| { type: 'ERROR'; error: string }
| { type: 'RESET' }
function uploadReducer(state: UploadState, action: UploadAction): UploadState {
switch (action.type) {
case 'SELECT': return { status: 'selecting' }
case 'START': return { status: 'uploading', progress: 0, file: action.file }
case 'PROGRESS':
if (state.status !== 'uploading') return state
return { ...state, progress: action.progress }
case 'SUCCESS':
if (state.status !== 'uploading') return state
return { status: 'success', url: action.url, file: state.file }
case 'ERROR':
if (state.status !== 'uploading') return state
return { status: 'error', error: action.error, file: state.file }
case 'RESET': return { status: 'idle' }
default: return state
}
}
function FileUploader() {
const [state, dispatch] = useReducer(uploadReducer, { status: 'idle' })
async function handleFileSelect(file: File) {
dispatch({ type: 'START', file })
try {
const url = await uploadWithProgress(file, (progress) => {
dispatch({ type: 'PROGRESS', progress })
})
dispatch({ type: 'SUCCESS', url })
} catch (e) {
dispatch({ type: 'ERROR', error: String(e) })
}
}
// TypeScript 在每个分支中缩小 state.status 的类型:
return (
<div>
{state.status === 'idle' && <DropZone onSelect={handleFileSelect} />}
{state.status === 'uploading' && <ProgressBar value={state.progress} />}
{state.status === 'success' && <SuccessView url={state.url} onReset={() => dispatch({ type: 'RESET' })} />}
{state.status === 'error' && <ErrorView error={state.error} onRetry={() => dispatch({ type: 'RESET' })} />}
</div>
)
}
组合优于配置
“一个组件带很多属性”的反模式会导致 API 膨胀。组合的扩展性更好:
// ❌ 配置繁重的组件——难以扩展
<Card
title="User Profile"
subtitle="Member since 2021"
avatar="/avatar.jpg"
badge="Pro"
action={<Button>Edit</Button>}
footer={<Stats />}
variant="horizontal"
elevated
/>
// ✅ 可组合的 Card——灵活、可扩展
<Card>
<Card.Header>
<Avatar src="/avatar.jpg" />
<div>
<Card.Title>User Profile</Card.Title>
<Card.Subtitle>Member since 2021</Card.Subtitle>
</div>
<Badge variant="pro">Pro</Badge>
</Card.Header>
<Card.Body>
<Stats />
</Card.Body>
<Card.Footer>
<Button>Edit</Button>
</Card.Footer>
</Card>

Provider 模式
对于不适合组件 props 的横切关注点,Provider 模式可以保持代码整洁:
// 主题 provider——影响整个子树
interface ThemeContextValue {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
return (localStorage.getItem('theme') as 'light' | 'dark') ?? 'light'
})
const toggleTheme = useCallback(() => {
setTheme(prev => {
const next = prev === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', next)
return next
})
}, [])
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div data-theme={theme}>{children}</div>
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}
Portal:在树外渲染
对于需要脱离 CSS overflow/z-index 约束的模态框、工具提示和下拉菜单:
import { createPortal } from 'react-dom'
import { useEffect, useRef } from 'react'
function Modal({ children, onClose }: { children: React.ReactNode, onClose: () => void }) {
const portalRoot = document.getElementById('modal-root') ?? document.body
// 聚焦捕获并处理 Escape 键
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [onClose])
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content"
role="dialog"
aria-modal="true"
onClick={e => e.stopPropagation()}
>
{children}
</div>
</div>,
portalRoot
)
}
错误边界
类组件仍然有一个独占的用例——错误边界:
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback: ReactNode | ((error: Error) => ReactNode)
}
interface State {
error: Error | null
}
class ErrorBoundary extends Component<Props, State> {
state: State = { error: null }
static getDerivedStateFromError(error: Error): State {
return { error }
}
componentDidCatch(error: Error, info: { componentStack: string }) {
console.error('Error boundary caught:', error, info)
}
render() {
if (this.state.error) {
const { fallback } = this.props
return typeof fallback === 'function'
? fallback(this.state.error)
: fallback
}
return this.props.children
}
}
// 用法
<ErrorBoundary
fallback={(error) => (
<div className="error-state">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
)}
>
<RiskyComponent />
</ErrorBoundary>
应避免的模式反模式
1. 超过 2 层的 prop 逐层传递:改用 context 或组合。
2. 上帝组件:如果组件文件超过约 300 行,说明它做得太多。将逻辑提取到 hooks 和子组件中。
3. 过度使用 useEffect:大多数数据转换应属于渲染逻辑或自定义 hooks,而不是 effects。
4. 到处使用 memo:React.memo、useMemo 和 useCallback 是优化手段。在性能分析显示问题后再应用,而不是预先使用。
// ❌ 过早优化——增加复杂性,很少需要
const value = useMemo(() => data.filter(x => x.active), [data])
// ✅ 仅当计算确实昂贵或引用相等性对子组件重新渲染重要时才使用 memo
React 模式归根结底是管理复杂性。从简单开始(useState、props),在出现重复时提取(自定义 hooks),仅在组合本身不够时使用高级模式(复合组件、context)。最好的模式是解决问题的最简单模式。
→ 使用 Hash Text 工具验证文本完整性。