Appearance
泛型组件
概述
泛型组件是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中的强大工具,合理使用可以创建高度可复用和类型安全的组件。