
组件复用的关键不在“把代码复用起来”,而在于选择正确的抽象边界,让后续迭代的成本更低、协作更顺畅。本文从三条互补路径展开:抽离逻辑(跨 UI 复用)、封装组件(统一视觉与交互)与自定义 hooks(复用有状态逻辑),提供实战示例与选型指南。
适用场景
实践要点
示例:分页与排序的通用工具
// logic/paginate.ts
export function paginate<T>(list: T[], pageSize: number, page: number): T[] {
if (pageSize <= 0 || page < 1) return [];
const start = (page - 1) * pageSize;
return list.slice(start, start + pageSize);
}
// logic/sortBy.ts
export function sortBy<T>(list: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
const factor = order === 'asc' ? 1 : -1;
return [...list].sort((a, b) => {
const av = a[key] as unknown as number | string;
const bv = b[key] as unknown as number | string;
if (av === bv) return 0;
return (av > bv ? 1 : -1) * factor;
});
}在组件中调用
import { paginate } from './logic/paginate';
import { sortBy } from './logic/sortBy';
export function UserTable({ data }: { data: Array<{ name: string; age: number }> }) {
const sorted = sortBy(data, 'age', 'asc');
const page1 = paginate(sorted, 10, 1);
return (
<ul>
{page1.map((u) => (
<li key={u.name}>{u.name} - {u.age}</li>
))}
</ul>
);
}适用场景
实践要点
open + onOpenChange 与 defaultOpen。className、style、as)与 forwardRef。示例:可受控的 Modal 组件
import { useState, useEffect, forwardRef } from 'react';
type ModalProps = {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (next: boolean) => void;
title?: string;
children?: React.ReactNode;
footer?: React.ReactNode;
className?: string;
};
export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
{ open, defaultOpen, onOpenChange, title, children, footer, className },
ref
) {
const [innerOpen, setInnerOpen] = useState(Boolean(defaultOpen));
const isControlled = typeof open === 'boolean';
const visible = isControlled ? open! : innerOpen;
useEffect(() => {
if (!isControlled && typeof open === 'boolean') setInnerOpen(open);
}, [open, isControlled]);
const setOpen = (next: boolean) => {
if (!isControlled) setInnerOpen(next);
onOpenChange?.(next);
};
if (!visible) return null;
return (
<div ref={ref} role="dialog" aria-modal="true" className={className}>
{title && <div>{title}</div>}
<div>{children}</div>
<div>{footer ?? <button onClick={() => setOpen(false)}>关闭</button>}</div>
</div>
);
});使用示例
// 受控模式
function Page() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>打开</button>
<Modal open={open} onOpenChange={setOpen} title="标题">
内容……
</Modal>
</>
);
}
// 非受控模式
function PageUncontrolled() {
return (
<>
<Modal defaultOpen title="欢迎">
初次打开显示
</Modal>
</>
);
}适用场景
实践要点
useCallback 或 useRef 保持稳定。示例 A:useDebouncedValue(防抖值)
import { useEffect, useState } from 'react';
export function useDebouncedValue<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}示例 B:useRequest(带取消与重试)
import { useEffect, useRef, useState, useCallback } from 'react';
type RequestState<T> = { loading: boolean; data?: T; error?: unknown };
export function useRequest<T>(
fn: (signal: AbortSignal) => Promise<T>,
options?: { retries?: number; baseDelay?: number; immediate?: boolean }
) {
const [state, setState] = useState<RequestState<T>>({ loading: false });
const controllerRef = useRef<AbortController | null>(null);
const retries = options?.retries ?? 2;
const base = options?.baseDelay ?? 200;
const run = useCallback(async () => {
controllerRef.current?.abort();
controllerRef.current = new AbortController();
setState({ loading: true });
for (let i = 0; i <= retries; i++) {
try {
const data = await fn(controllerRef.current.signal);
setState({ loading: false, data });
return;
} catch (e) {
if (controllerRef.current.signal.aborted) return;
if (i === retries) {
setState({ loading: false, error: e });
return;
}
const jitter = Math.random() * 0.2 + 0.9;
const delay = Math.round(base * Math.pow(2, i) * jitter);
await new Promise((r) => setTimeout(r, delay));
}
}
}, [fn, retries, base]);
useEffect(() => {
if (options?.immediate) run();
return () => controllerRef.current?.abort();
}, [options?.immediate, run]);
return { ...state, run, cancel: () => controllerRef.current?.abort() };
}使用示例
function SearchBox() {
const [q, setQ] = useState('');
const dq = useDebouncedValue(q, 300);
const { data, loading, error } = useRequest<string[]>(
async (signal) => {
const res = await fetch(`/api/search?q=${encodeURIComponent(dq)}`, { signal });
if (!res.ok) throw new Error('failed');
return res.json();
},
{ immediate: true }
);
return (
<div>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="搜索…" />
{loading && <span>加载中…</span>}
{error && <span>出错</span>}
<ul>{data?.map((x) => <li key={x}>{x}</li>)}</ul>
</div>
);
}进一步判断
logic(纯函数) → hooks(状态逻辑) → components(UI)抽离逻辑、封装组件、自定义 hooks 是互补的三条路。正确的复用,是在合适层级建立稳固的抽象边界。先搞清“要复用的东西是什么”,再决定用哪种方案实现,才能让未来的你与同事受益。