y.y
Published on

React核心设计模式实践指南

Authors

React核心设计模式完全指南

目录

  1. 引言
  2. Provider Pattern(提供者模式)
  3. Custom Hook Pattern(自定义Hook模式)
  4. HOC Pattern(高阶组件模式)
  5. Container/Presentational Pattern(容器/展示组件模式)
  6. Compound Components Pattern(复合组件模式)
  7. 模式选择与实践建议

引言

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>
  );
};

性能优化

  1. 拆分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>
  );
};
  1. 使用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 }} />;
};

最佳实践

  1. 适当拆分Context避免不必要的重渲染
  2. 提供类型安全的Context使用方式
  3. 处理Context未定义的情况
  4. 将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>
  );
};

最佳实践

  1. 保持Hook的单一职责
  2. 提供完善的TypeScript类型定义
  3. 处理所有可能的错误情况
  4. 提供清晰的接口和文档
  5. 适当使用泛型增加灵活性

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);

最佳实践

  1. 使用compose函数组合多个HOC
  2. 正确处理props的类型
  3. 转发refs
  4. 保持HOC的纯函数特性
  5. 注意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} />}
    </>
  );
};

最佳实践

  1. 保持展示组件的纯函数特性
  2. 使用TypeScript定义清晰的接口
  3. 处理所有可能的状态(加载、错误、空数据等)
  4. 使用适当的命名约定

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>
  );
};

最佳实践

  1. 提供清晰的TypeScript类型定义
  2. 实现合理的默认行为
  3. 添加必要的ARIA属性支持可访问性
  4. 提供样式定制能力
  5. 处理边界情况和错误状态

模式选择与实践建议

选择标准

  1. Provider Pattern 适用于:

    • 需要共享全局状态
    • 需要跨多层组件传递数据
    • 主题切换、用户认证等全局特性
  2. Custom Hook Pattern 适用于:

    • 复用状态逻辑
    • 处理复杂的副作用
    • 封装通用功能(如表单处理、数据获取)
  3. HOC Pattern 适用于:

    • 需要处理横切关注点
    • 组件需要条件性的功能增强
    • 需要复用与UI无关的功能
  4. Container/Presentational Pattern 适用于:

    • 需要分离数据逻辑和UI展示
    • 提高组件的可测试性和可复用性
    • 大型团队协作开发
  5. Compound Components Pattern 适用于:

    • 创建灵活的组件API
    • 需要组件间紧密协作
    • 构建复杂的表单控件或UI组件

性能优化建议

  1. 状态管理优化
// 使用useMemo缓存计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(dep), [dep]);

// 使用useCallback缓存函数
const memoizedCallback = useCallback((param) => {
  doSomething(param);
}, [dep]);
  1. Context优化
// 拆分Context避免不必要的重渲染
const ThemeContext = React.createContext(defaultTheme);
const AuthContext = React.createContext(defaultAuth);

// 使用Context Selector
const useThemeColor = () => {
  const theme = useContext(ThemeContext);
  return theme.color; // 只订阅color的变化
};
  1. 组件优化
// 使用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>
);

测试策略

  1. 单元测试
// 测试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();
  1. 集成测试
// 测试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设计模式的选择应该基于具体需求和场景:

  1. 从最简单的解决方案开始,避免过度设计
  2. 根据团队规模和项目复杂度选择合适的模式
  3. 注重代码可维护性和可测试性
  4. 持续关注性能优化
  5. 保持良好的文档习惯

记住,设计模式是工具而非目标,选择合适的模式比使用特定模式更重要。随着项目的发展,也要及时调整和重构代码结构,使其更好地服务于业务需求。