- Published on
React核心设计模式实践指南
- Authors
- Name
- Yvan Yang
React核心设计模式完全指南
目录
- 引言
- Provider Pattern(提供者模式)
- Custom Hook Pattern(自定义Hook模式)
- HOC Pattern(高阶组件模式)
- Container/Presentational Pattern(容器/展示组件模式)
- Compound Components Pattern(复合组件模式)
- 模式选择与实践建议
引言
React设计模式是构建可维护、可扩展的React应用的关键。本文将详细介绍五种最常用且最实用的React设计模式,包括具体实现、最佳实践、适用场景以及性能优化建议。
Provider Pattern(提供者模式)
概述
Provider Pattern解决了跨组件层级数据共享的问题,是React上下文机制的标准实现方式。
实现示例
// types.ts
interface Theme {
primary: string;
secondary: string;
textColor: string;
}
interface AppContextType {
theme: Theme;
user: User | null;
setTheme: (theme: Theme) => void;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
// AppContext.tsx
const AppContext = React.createContext<AppContextType | undefined>(undefined);
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
try {
const user = await authService.login(credentials);
setUser(user);
} catch (error) {
throw new Error('Login failed');
}
};
const logout = () => {
setUser(null);
authService.logout();
};
const value = {
theme,
user,
setTheme,
login,
logout
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
// useApp.ts
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
};
// 使用示例
const Header = () => {
const { user, theme, logout } = useApp();
return (
<header style={{ backgroundColor: theme.primary }}>
{user ? (
<>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<LoginButton />
)}
</header>
);
};
性能优化
- 拆分Context
// 将主题和认证分开管理
const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<ThemeProvider>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
);
};
- 使用Context Selector
const useThemeSelector = <T,>(selector: (theme: Theme) => T) => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useThemeSelector must be used within ThemeProvider');
return selector(context);
};
// 使用示例
const PrimaryColorComponent = () => {
const primaryColor = useThemeSelector(theme => theme.primary);
return <div style={{ color: primaryColor }} />;
};
最佳实践
- 适当拆分Context避免不必要的重渲染
- 提供类型安全的Context使用方式
- 处理Context未定义的情况
- 将Provider逻辑封装在专门的组件中
Custom Hook Pattern(自定义Hook模式)
概述
Custom Hook是React中最灵活的逻辑复用机制,它让我们能够将组件逻辑提取到可重用的函数中。
实现示例
// useAsync.ts
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useAsync<T>(asyncFunction: () => Promise<T>, dependencies: any[] = []) {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
const fetchData = async () => {
setState(prev => ({ ...prev, loading: true }));
try {
const result = await asyncFunction();
setState({ data: result, loading: false, error: null });
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
}
};
fetchData();
}, dependencies);
return state;
}
// useForm.ts
interface FormConfig<T> {
initialValues: T;
validate?: (values: T) => Partial<Record<keyof T, string>>;
onSubmit: (values: T) => Promise<void> | void;
}
function useForm<T extends Record<string, any>>({
initialValues,
validate,
onSubmit
}: FormConfig<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (validate) {
const validationErrors = validate(values);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
}
setIsSubmitting(true);
try {
await onSubmit(values);
setErrors({});
} catch (error) {
setErrors({ submit: (error as Error).message });
} finally {
setIsSubmitting(false);
}
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit
};
}
// 使用示例
interface LoginForm {
email: string;
password: string;
}
const LoginPage = () => {
const {
values,
errors,
isSubmitting,
handleChange,
handleSubmit
} = useForm<LoginForm>({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors: Partial<Record<keyof LoginForm, string>> = {};
if (!values.email) errors.email = 'Required';
if (!values.password) errors.password = 'Required';
return errors;
},
onSubmit: async (values) => {
await loginAPI(values);
}
});
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <span>{errors.email}</span>}
{/* 其他表单字段 */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
};
最佳实践
- 保持Hook的单一职责
- 提供完善的TypeScript类型定义
- 处理所有可能的错误情况
- 提供清晰的接口和文档
- 适当使用泛型增加灵活性
HOC Pattern(高阶组件模式)
概述
HOC是一个函数,它接收一个组件作为参数并返回一个新的增强组件。适合处理横切关注点。
实现示例
// withAuth.tsx
interface WithAuthProps {
isAuthenticated?: boolean;
user?: User;
}
export function withAuth<P extends WithAuthProps>(
WrappedComponent: React.ComponentType<P>
) {
return function WithAuthComponent(props: Omit<P, keyof WithAuthProps>) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const checkAuth = async () => {
try {
const authResult = await authService.checkAuth();
setIsAuthenticated(authResult.isAuthenticated);
setUser(authResult.user);
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return (
<WrappedComponent
{...(props as P)}
isAuthenticated={isAuthenticated}
user={user}
/>
);
};
}
// withErrorBoundary.tsx
interface ErrorBoundaryProps {
error?: Error;
resetError?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export function withErrorBoundary<P extends ErrorBoundaryProps>(
WrappedComponent: React.ComponentType<P>,
ErrorComponent?: React.ComponentType<ErrorBoundaryProps>
) {
return class WithErrorBoundary extends React.Component<
Omit<P, keyof ErrorBoundaryProps>,
ErrorBoundaryState
> {
state: ErrorBoundaryState = {
hasError: false,
error: null
};
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// 可以在这里上报错误
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (ErrorComponent) {
return <ErrorComponent
error={this.state.error!}
resetError={this.resetError}
/>;
}
return <div>Something went wrong.</div>;
}
return (
<WrappedComponent
{...(this.props as P)}
error={this.state.error}
resetError={this.resetError}
/>
);
}
};
}
// 使用示例
interface DashboardProps extends WithAuthProps, ErrorBoundaryProps {
title: string;
}
const Dashboard: React.FC<DashboardProps> = ({ user, title }) => {
return (
<div>
<h1>{title}</h1>
<p>Welcome, {user?.name}</p>
</div>
);
};
// 组合多个HOC
const EnhancedDashboard = compose(
withAuth,
withErrorBoundary
)(Dashboard);
最佳实践
- 使用compose函数组合多个HOC
- 正确处理props的类型
- 转发refs
- 保持HOC的纯函数特性
- 注意displayName的设置
Container/Presentational Pattern(容器/展示组件模式)
概述
这种模式通过分离关注点来提高组件的可复用性和可测试性。
实现示例
// types.ts
interface User {
id: string;
name: string;
email: string;
}
interface UserListProps {
users: User[];
isLoading: boolean;
error?: Error;
onUserSelect: (user: User) => void;
}
// UserList.tsx (Presentational)
const UserList: React.FC<UserListProps> = ({
users,
isLoading,
error,
onUserSelect
}) => {
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error.message} />;
}
return (
<div className="user-list">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => onUserSelect(user)}
/>
))}
</div>
);
};
// UserListContainer.tsx (Container)
const UserListContainer: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error>();
const [selectedUser, setSelectedUser] = useState<User>();
useEffect(() => {
const fetchUsers = async () => {
try {
setIsLoading(true);
const data = await userService.getUsers();
setUsers(data);
} catch (err) {
setError(err as Error);
} finally {
setIsLoading(false);
}
};
fetchUsers();
}, []);
const handleUserSelect = (user: User) => {
setSelectedUser(user);
// 其他处理逻辑
};
return (
<>
<UserList
users={users}
isLoading={isLoading}
error={error}
onUserSelect={handleUserSelect}
/>
{selectedUser && <UserDetail user={selectedUser} />}
</>
);
};
最佳实践
- 保持展示组件的纯函数特性
- 使用TypeScript定义清晰的接口
- 处理所有可能的状态(加载、错误、空数据等)
- 使用适当的命名约定
Compound Components Pattern(复合组件模式)
概述
复合组件模式允许创建一组具有内在关联的组件,通过共享状态协同工作,提供灵活且声明式的API。
实现示例
// types.ts
interface TabContextType {
activeIndex: number;
setActiveIndex: (index: number) => void;
}
interface TabProps {
children: React.ReactNode;
index: number;
}
interface TabPanelProps {
children: React.ReactNode;
index: number;
}
// TabContext.tsx
const TabContext = React.createContext<TabContextType | undefined>(undefined);
const useTabs = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('Tabs compound components must be used within Tabs component');
}
return context;
};
// Tabs.tsx
const Tabs: React.FC<{
children: React.ReactNode;
defaultIndex?: number;
onChange?: (index: number) => void;
}> & {
List: typeof TabList;
Tab: typeof Tab;
Panels: typeof TabPanels;
Panel: typeof TabPanel;
} = ({ children, defaultIndex = 0, onChange }) => {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const handleChange = (index: number) => {
setActiveIndex(index);
onChange?.(index);
};
return (
<TabContext.Provider value={{ activeIndex, setActiveIndex: handleChange }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
};
// TabList.tsx
const TabList: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div role="tablist" className="tab-list">
{children}
</div>
);
};
// Tab.tsx
const Tab: React.FC<TabProps> = ({ children, index }) => {
const { activeIndex, setActiveIndex } = useTabs();
return (
<button
role="tab"
aria-selected={activeIndex === index}
className={`tab ${activeIndex === index ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
};
// TabPanels.tsx
const TabPanels: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="tab-panels">{children}</div>;
};
// TabPanel.tsx
const TabPanel: React.FC<TabPanelProps> = ({ children, index }) => {
const { activeIndex } = useTabs();
if (activeIndex !== index) return null;
return (
<div
role="tabpanel"
className="tab-panel"
>
{children}
</div>
);
};
// 组装Tabs组件
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
// 使用示例
const TabsExample: React.FC = () => {
return (
<Tabs defaultIndex={0} onChange={(index) => console.log(`Tab ${index} selected`)}>
<Tabs.List>
<Tabs.Tab index={0}>Account</Tabs.Tab>
<Tabs.Tab index={1}>Settings</Tabs.Tab>
<Tabs.Tab index={2}>Messages</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel index={0}>
<AccountSettings />
</Tabs.Panel>
<Tabs.Panel index={1}>
<UserSettings />
</Tabs.Panel>
<Tabs.Panel index={2}>
<Messages />
</Tabs.Panel>
</Tabs.Panels>
</Tabs>
);
};
最佳实践
- 提供清晰的TypeScript类型定义
- 实现合理的默认行为
- 添加必要的ARIA属性支持可访问性
- 提供样式定制能力
- 处理边界情况和错误状态
模式选择与实践建议
选择标准
Provider Pattern 适用于:
- 需要共享全局状态
- 需要跨多层组件传递数据
- 主题切换、用户认证等全局特性
Custom Hook Pattern 适用于:
- 复用状态逻辑
- 处理复杂的副作用
- 封装通用功能(如表单处理、数据获取)
HOC Pattern 适用于:
- 需要处理横切关注点
- 组件需要条件性的功能增强
- 需要复用与UI无关的功能
Container/Presentational Pattern 适用于:
- 需要分离数据逻辑和UI展示
- 提高组件的可测试性和可复用性
- 大型团队协作开发
Compound Components Pattern 适用于:
- 创建灵活的组件API
- 需要组件间紧密协作
- 构建复杂的表单控件或UI组件
性能优化建议
- 状态管理优化
// 使用useMemo缓存计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(dep), [dep]);
// 使用useCallback缓存函数
const memoizedCallback = useCallback((param) => {
doSomething(param);
}, [dep]);
- Context优化
// 拆分Context避免不必要的重渲染
const ThemeContext = React.createContext(defaultTheme);
const AuthContext = React.createContext(defaultAuth);
// 使用Context Selector
const useThemeColor = () => {
const theme = useContext(ThemeContext);
return theme.color; // 只订阅color的变化
};
- 组件优化
// 使用React.memo避免不必要的重渲染
const MemoizedComponent = React.memo(({ prop1, prop2 }) => {
return <div>{prop1} {prop2}</div>;
});
// 使用Key属性优化列表渲染
const List = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
测试策略
- 单元测试
// 测试Hook
const { result } = renderHook(() => useCustomHook());
act(() => {
result.current.someFunction();
});
expect(result.current.value).toBe(expectedValue);
// 测试展示组件
const { getByText } = render(<PresentationalComponent prop={value} />);
expect(getByText('Expected Text')).toBeInTheDocument();
- 集成测试
// 测试HOC
const WrappedComponent = withAuth(BaseComponent);
const { getByText } = render(<WrappedComponent />);
// 验证认证逻辑
// 测试Context
const { getByText } = render(
<ThemeProvider>
<ConsumerComponent />
</ThemeProvider>
);
文档规范
/**
* 组件描述
* @component
* @example
* ```tsx
* <MyComponent prop1="value" prop2={42} />
* ```
*/
interface MyComponentProps {
/** prop1的描述 */
prop1: string;
/** prop2的描述 */
prop2: number;
}
export const MyComponent: React.FC<MyComponentProps> = ({ prop1, prop2 }) => {
// ...
};
总结
React设计模式的选择应该基于具体需求和场景:
- 从最简单的解决方案开始,避免过度设计
- 根据团队规模和项目复杂度选择合适的模式
- 注重代码可维护性和可测试性
- 持续关注性能优化
- 保持良好的文档习惯
记住,设计模式是工具而非目标,选择合适的模式比使用特定模式更重要。随着项目的发展,也要及时调整和重构代码结构,使其更好地服务于业务需求。