关于 react-patterns
面向 React 开发者的组件模式技能,汇总复合组件、Render Props、高阶组件、自定义 Hooks 和组合等模式,并说明适用场景与注意事项。
主要功能
- 01复合组件模式
- 02Render Props 与高阶组件
- 03自定义 Hooks
- 04组件组合实践
- 05模式选择与问题排查
适用场景
- 01构建灵活的相关组件
- 02复用有状态逻辑
- 03分离组件逻辑与界面
- 04排查 Context 或渲染问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-patterns安装后在 Claude Code 中输入/react-patterns确认已加载。
附带文件advanced.md
详细介绍
React 模式
完整参考:有关容器/展示、状态机、插槽、多态和 Provider 模式,请参阅 advanced.md。
复合组件
协同工作以构成连贯 UI 的组件:
import { createContext, useContext, useState, ReactNode } from 'react';
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within Accordion');
}
return context;
}
function Accordion({ children, multiple = false, defaultOpen = [] }: AccordionProps) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));
const toggle = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!multiple) next.clear();
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
// Attach sub-components
Accordion.Item = Item;
Accordion.Trigger = Trigger;
Accordion.Content = Content;
Render Props
将函数作为子项或属性传递,以控制渲染:
interface DataFetcherProps<T> {
url: string;
children: (state: {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}) => ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
setData(await res.json());
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { fetchData(); }, [fetchData]);
return <>{children({ data, loading, error, refetch: fetchData })}</>;
}
// Usage
<DataFetcher<User[]> url="/api/users">
{({ data, loading, error, refetch }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
return <ul>{data?.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}}
</DataFetcher>
高阶组件(HOC)
包装组件以添加功能:
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>,
LoadingComponent: React.ComponentType = DefaultSpinner
) {
return function WithLoadingComponent(props: P & { loading: boolean }) {
const { loading, ...restProps } = props;
if (loading) return <LoadingComponent />;
return <WrappedComponent {...(restProps as P)} />;
};
}
// HOC with Refs
function withLogger<P extends object>(WrappedComponent: React.ComponentType<P>) {
const WithLogger = React.forwardRef<unknown, P>((props, ref) => {
useEffect(() => { console.log('Props changed:', props); }, [props]);
return <WrappedComponent {...props} ref={ref} />;
});
WithLogger.displayName = `WithLogger(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithLogger;
}
自定义 Hook 模式
将可复用逻辑提取到 Hook 中:
// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch { return initialValue; }
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// useOnClickOutside
function useOnClickOutside(
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) return;
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
组合模式
优先使用组合,而非继承:
function Card({ children, className }: CardProps) {
return <div className={`card ${className || ''}`}>{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return <div className="card-header">{children}</div>;
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="card-body">{children}</div>;
}
// Specialized cards through composition
function UserCard({ user }: { user: User }) {
return (
<Card>
<CardHeader>
<Avatar src={user.avatar} />
<h3>{user.name}</h3>
</CardHeader>
<CardBody><p>{user.bio}</p></CardBody>
</Card>
);
}
最佳实践总结
| 模式 | 适用场景 |
|---|---|
| 复合组件 | 灵活的相关组件 |
| Render Props | 共享行为、控制渲染 |
| HOC | 横切关注点、遗留代码 |
| 自定义 Hook | 可复用的有状态逻辑 |
| 组合 | 由简单构建复杂结构 |
| 容器/展示 | 将逻辑与 UI 分离 |
| 状态机 | 复杂的状态转换 |
| 多态 | 灵活的元素类型 |
- 使用自定义 Hook 实现可复用逻辑
- 保持组件小巧且专注
- 使用 TypeScript 改善开发体验
- 不要过度使用模式——保持简单(KISS)
- 不要嵌套过多 HOC
反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 对简单组件过度设计 | 不必要的复杂性 | 使用简单的属性 |
| 嵌套过多 HOC | 属性冲突,难以调试 | 使用组合或 Hook |
| 未进行记忆化的 Render Props | 性能问题 | 对渲染函数进行记忆化 |
| 属性层层传递过深 | 难以维护 | 使用 Context 或复合组件 |
| 使用 Context 管理频繁变化的值 | 性能问题 | 使用状态库 |
快速故障排除
| 问题 | 可能原因 | 修复方法 |
|---|---|---|
| Context 不可用 | 缺少 Provider | 使用 Provider 组件进行包装 |
| HOC 丢失 ref | 未转发 ref | 使用 React.forwardRef |
| Render Props 导致重新渲染 | 函数标识发生变化 | 对渲染函数进行记忆化 |
参考文档
评论
支持 Markdown