Skip to content

泛型组件

概述

泛型组件是React TypeScript中的高级特性,允许创建可复用、类型安全的组件。泛型组件可以适应不同的数据类型,同时保持类型安全。本文将全面介绍泛型组件的设计和实现。

基础泛型组件

简单泛型组件

typescript
// List组件 - 最基本的泛型组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 使用
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
/>

带约束的泛型组件

typescript
// 要求T必须有id属性
interface WithId {
  id: string | number;
}

interface ListProps<T extends WithId> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  onItemClick?: (id: T['id']) => void;
}

function List<T extends WithId>({
  items,
  renderItem,
  onItemClick,
}: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li
          key={item.id}
          onClick={() => onItemClick?.(item.id)}
        >
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// 使用
interface Product extends WithId {
  name: string;
  price: number;
}

<List
  items={products}
  renderItem={(product) => `${product.name} - $${product.price}`}
  onItemClick={(id) => console.log(id)}
/>

复杂泛型组件

Table组件

typescript
interface Column<T> {
  key: keyof T;
  title: string;
  width?: string | number;
  render?: (value: T[keyof T], row: T, index: number) => React.ReactNode;
  sortable?: boolean;
  align?: 'left' | 'center' | 'right';
}

interface TableProps<T extends WithId> {
  data: T[];
  columns: Array<Column<T>>;
  rowKey?: keyof T;
  onRowClick?: (row: T) => void;
  loading?: boolean;
  emptyText?: string;
}

function Table<T extends WithId>({
  data,
  columns,
  rowKey = 'id',
  onRowClick,
  loading,
  emptyText = 'No data',
}: TableProps<T>) {
  if (loading) {
    return <div>Loading...</div>;
  }

  if (data.length === 0) {
    return <div>{emptyText}</div>;
  }

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th
              key={String(col.key)}
              style={{
                width: col.width,
                textAlign: col.align,
              }}
            >
              {col.title}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, rowIndex) => (
          <tr
            key={String(row[rowKey])}
            onClick={() => onRowClick?.(row)}
            style={{ cursor: onRowClick ? 'pointer' : 'default' }}
          >
            {columns.map((col) => (
              <td
                key={String(col.key)}
                style={{ textAlign: col.align }}
              >
                {col.render
                  ? col.render(row[col.key], row, rowIndex)
                  : String(row[col.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// 使用
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com', age: 30, isActive: true },
  { id: 2, name: 'Bob', email: 'bob@example.com', age: 25, isActive: false },
];

<Table
  data={users}
  columns={[
    {
      key: 'name',
      title: 'Name',
      width: 200,
    },
    {
      key: 'email',
      title: 'Email',
    },
    {
      key: 'age',
      title: 'Age',
      width: 80,
      align: 'center',
    },
    {
      key: 'isActive',
      title: 'Status',
      render: (value) => (
        <span style={{ color: value ? 'green' : 'red' }}>
          {value ? 'Active' : 'Inactive'}
        </span>
      ),
    },
  ]}
  onRowClick={(user) => console.log(user)}
/>

Select组件

typescript
interface SelectOption {
  label: string;
  value: string | number;
}

interface SelectProps<T extends SelectOption> {
  options: T[];
  value?: T['value'];
  onChange: (value: T['value'], option: T) => void;
  placeholder?: string;
  disabled?: boolean;
  multiple?: boolean;
  renderOption?: (option: T) => React.ReactNode;
}

function Select<T extends SelectOption>({
  options,
  value,
  onChange,
  placeholder = 'Select an option',
  disabled = false,
  renderOption,
}: SelectProps<T>) {
  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selectedValue = e.target.value;
    const selectedOption = options.find((opt) => String(opt.value) === selectedValue);
    
    if (selectedOption) {
      onChange(selectedOption.value, selectedOption);
    }
  };

  return (
    <select
      value={value}
      onChange={handleChange}
      disabled={disabled}
    >
      <option value="">{placeholder}</option>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {renderOption ? renderOption(option) : option.label}
        </option>
      ))}
    </select>
  );
}

// 使用
interface CountryOption extends SelectOption {
  code: string;
  flag: string;
}

const countries: CountryOption[] = [
  { label: 'United States', value: 'US', code: 'US', flag: '🇺🇸' },
  { label: 'China', value: 'CN', code: 'CN', flag: '🇨🇳' },
];

<Select
  options={countries}
  value={selectedCountry}
  onChange={(value, option) => {
    console.log('Selected:', value, option.code);
  }}
  renderOption={(option) => `${option.flag} ${option.label}`}
/>

多泛型参数组件

Form Field组件

typescript
interface FieldProps<TValue, TElement extends HTMLElement = HTMLInputElement> {
  name: string;
  value: TValue;
  onChange: (value: TValue) => void;
  render: (props: {
    value: TValue;
    onChange: (e: React.ChangeEvent<TElement>) => void;
  }) => React.ReactNode;
  validate?: (value: TValue) => string | undefined;
}

function Field<TValue, TElement extends HTMLElement = HTMLInputElement>({
  name,
  value,
  onChange,
  render,
  validate,
}: FieldProps<TValue, TElement>) {
  const [error, setError] = React.useState<string>();

  const handleChange = (e: React.ChangeEvent<TElement>) => {
    const newValue = (e.target as any).value as TValue;
    onChange(newValue);
    
    if (validate) {
      const validationError = validate(newValue);
      setError(validationError);
    }
  };

  return (
    <div>
      {render({ value, onChange: handleChange })}
      {error && <span style={{ color: 'red' }}>{error}</span>}
    </div>
  );
}

// 使用
<Field
  name="email"
  value={email}
  onChange={setEmail}
  render={({ value, onChange }) => (
    <input type="email" value={value} onChange={onChange} />
  )}
  validate={(value) => {
    if (!value.includes('@')) {
      return 'Invalid email';
    }
  }}
/>

Key-Value Mapper组件

typescript
interface MapperProps<TInput, TOutput> {
  items: TInput[];
  mapper: (item: TInput, index: number) => TOutput;
  render: (items: TOutput[]) => React.ReactNode;
}

function Mapper<TInput, TOutput>({
  items,
  mapper,
  render,
}: MapperProps<TInput, TOutput>) {
  const mappedItems = items.map(mapper);
  return <>{render(mappedItems)}</>;
}

// 使用
interface User {
  id: number;
  firstName: string;
  lastName: string;
}

interface UserDisplay {
  id: number;
  fullName: string;
}

<Mapper
  items={users}
  mapper={(user) => ({
    id: user.id,
    fullName: `${user.firstName} ${user.lastName}`,
  })}
  render={(displayUsers) => (
    <ul>
      {displayUsers.map((user) => (
        <li key={user.id}>{user.fullName}</li>
      ))}
    </ul>
  )}
/>

条件泛型组件

根据类型变化的组件

typescript
// 根据数据类型自动选择渲染方式
type DataDisplayProps<T> =
  | {
      type: 'list';
      data: T[];
      renderItem: (item: T) => React.ReactNode;
    }
  | {
      type: 'single';
      data: T;
      render: (item: T) => React.ReactNode;
    };

function DataDisplay<T>(props: DataDisplayProps<T>) {
  if (props.type === 'list') {
    return (
      <ul>
        {props.data.map((item, index) => (
          <li key={index}>{props.renderItem(item)}</li>
        ))}
      </ul>
    );
  }

  return <div>{props.render(props.data)}</div>;
}

// 使用
<DataDisplay
  type="list"
  data={users}
  renderItem={(user) => user.name}
/>

<DataDisplay
  type="single"
  data={user}
  render={(user) => <UserCard user={user} />}
/>

可选泛型参数

typescript
interface ApiResponse<T = any> {
  data?: T;
  error?: Error;
  loading: boolean;
}

interface ApiDisplayProps<T = any> {
  response: ApiResponse<T>;
  renderData: (data: T) => React.ReactNode;
  renderError?: (error: Error) => React.ReactNode;
  renderLoading?: () => React.ReactNode;
}

function ApiDisplay<T = any>({
  response,
  renderData,
  renderError = (error) => <div>Error: {error.message}</div>,
  renderLoading = () => <div>Loading...</div>,
}: ApiDisplayProps<T>) {
  if (response.loading) {
    return <>{renderLoading()}</>;
  }

  if (response.error) {
    return <>{renderError(response.error)}</>;
  }

  if (response.data) {
    return <>{renderData(response.data)}</>;
  }

  return null;
}

// 使用
interface UserData {
  id: number;
  name: string;
}

const response: ApiResponse<UserData> = {
  data: { id: 1, name: 'Alice' },
  loading: false,
};

<ApiDisplay
  response={response}
  renderData={(data) => <div>{data.name}</div>}
/>

泛型HOC

withData HOC

typescript
interface WithDataProps<T> {
  data: T;
}

function withData<T, P extends WithDataProps<T>>(
  Component: React.ComponentType<P>,
  fetchData: () => Promise<T>
): React.ComponentType<Omit<P, keyof WithDataProps<T>>> {
  return function WithDataComponent(props: Omit<P, keyof WithDataProps<T>>) {
    const [data, setData] = React.useState<T | null>(null);
    const [loading, setLoading] = React.useState(true);

    React.useEffect(() => {
      fetchData().then((result) => {
        setData(result);
        setLoading(false);
      });
    }, []);

    if (loading || !data) {
      return <div>Loading...</div>;
    }

    return <Component {...(props as P)} data={data} />;
  };
}

// 使用
interface UserData {
  id: number;
  name: string;
}

interface UserDisplayProps extends WithDataProps<UserData> {
  title: string;
}

const UserDisplay = ({ data, title }: UserDisplayProps) => (
  <div>
    <h1>{title}</h1>
    <p>{data.name}</p>
  </div>
);

const UserDisplayWithData = withData(
  UserDisplay,
  () => fetch('/api/user').then((r) => r.json())
);

<UserDisplayWithData title="User Profile" />

泛型Context

类型安全的Store

typescript
interface StoreState<T> {
  data: T;
  loading: boolean;
  error: Error | null;
}

interface StoreActions<T> {
  setData: (data: T) => void;
  setLoading: (loading: boolean) => void;
  setError: (error: Error | null) => void;
  reset: () => void;
}

type StoreContextType<T> = StoreState<T> & StoreActions<T>;

function createStore<T>(initialData: T) {
  const StoreContext = React.createContext<StoreContextType<T> | undefined>(
    undefined
  );

  const StoreProvider = ({ children }: { children: React.ReactNode }) => {
    const [state, setState] = React.useState<StoreState<T>>({
      data: initialData,
      loading: false,
      error: null,
    });

    const setData = React.useCallback((data: T) => {
      setState((prev) => ({ ...prev, data, error: null }));
    }, []);

    const setLoading = React.useCallback((loading: boolean) => {
      setState((prev) => ({ ...prev, loading }));
    }, []);

    const setError = React.useCallback((error: Error | null) => {
      setState((prev) => ({ ...prev, error }));
    }, []);

    const reset = React.useCallback(() => {
      setState({ data: initialData, loading: false, error: null });
    }, []);

    return (
      <StoreContext.Provider
        value={{
          ...state,
          setData,
          setLoading,
          setError,
          reset,
        }}
      >
        {children}
      </StoreContext.Provider>
    );
  };

  const useStore = () => {
    const context = React.useContext(StoreContext);
    if (!context) {
      throw new Error('useStore must be used within StoreProvider');
    }
    return context;
  };

  return { StoreProvider, useStore };
}

// 使用
interface UserStore {
  users: User[];
  selectedUser: User | null;
}

const { StoreProvider: UserStoreProvider, useStore: useUserStore } = createStore<UserStore>({
  users: [],
  selectedUser: null,
});

const App = () => (
  <UserStoreProvider>
    <UserList />
  </UserStoreProvider>
);

const UserList = () => {
  const { data, setData } = useUserStore();
  
  return (
    <div>
      {data.users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
};

泛型Hooks

useLocalStorage Hook

typescript
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  const [storedValue, setStoredValue] = React.useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// 使用
interface UserSettings {
  theme: 'light' | 'dark';
  language: string;
}

const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {
  theme: 'light',
  language: 'en',
});

useFetch Hook

typescript
interface UseFetchOptions {
  method?: string;
  headers?: Record<string, string>;
  body?: any;
}

interface UseFetchReturn<T> {
  data: T | null;
  error: Error | null;
  loading: boolean;
  refetch: () => Promise<void>;
}

function useFetch<T>(
  url: string,
  options?: UseFetchOptions
): UseFetchReturn<T> {
  const [data, setData] = React.useState<T | null>(null);
  const [error, setError] = React.useState<Error | null>(null);
  const [loading, setLoading] = React.useState(true);

  const fetchData = React.useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url, {
        method: options?.method || 'GET',
        headers: options?.headers,
        body: options?.body ? JSON.stringify(options.body) : undefined,
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result: T = await response.json();
      setData(result);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  }, [url, options?.method, options?.headers, options?.body]);

  React.useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, error, loading, refetch: fetchData };
}

// 使用
interface Post {
  id: number;
  title: string;
  body: string;
}

const PostList = () => {
  const { data, error, loading, refetch } = useFetch<Post[]>(
    'https://jsonplaceholder.typicode.com/posts'
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return null;

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      {data.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
};

最佳实践

1. 明确泛型约束

typescript
// ✅ 好 - 明确约束
interface ListProps<T extends WithId> {
  items: T[];
}

// ❌ 不好 - 过于宽泛
interface ListProps<T> {
  items: T[];
}

2. 提供默认泛型参数

typescript
// ✅ 好 - 提供默认值
interface ApiResponse<T = any> {
  data: T;
}

// 可以不指定泛型
const response: ApiResponse = { data: {} };

3. 使用有意义的泛型名称

typescript
// ✅ 好 - 描述性名称
interface Mapper<TInput, TOutput> {
  map: (input: TInput) => TOutput;
}

// ❌ 不好 - 无意义的名称
interface Mapper<A, B> {
  map: (input: A) => B;
}

4. 避免过度泛型化

typescript
// ✅ 好 - 简单直接
interface ButtonProps {
  label: string;
  onClick: () => void;
}

// ❌ 不好 - 不必要的泛型
interface ButtonProps<T> {
  label: T;
  onClick: () => void;
}

泛型组件是React TypeScript中的强大工具,合理使用可以创建高度可复用和类型安全的组件。