Дженерик-компоненты
Компоненты могут принимать параметр типа для переиспользования с разными типами данных.
Базовый пример
type ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
<List
items={users}
renderItem={(user) => <li key={user.id}>{user.name}</li>}
/>
TypeScript выводит T из items.
Select
type SelectProps<T> = {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getKey: (option: T) => string | number;
};
function Select<T>({ options, value, onChange, getLabel, getKey }: SelectProps<T>) {
return (
<select
value={value ? getKey(value) : ''}
onChange={(e) => {
const found = options.find((o) => String(getKey(o)) === e.target.value);
if (found) onChange(found);
}}
>
{options.map((option) => (
<option key={getKey(option)} value={getKey(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
Table
type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], item: T) => ReactNode;
};
type TableProps<T> = {
items: T[];
columns: Column<T>[];
getRowKey: (item: T) => string | number;
};
function Table<T>({ items, columns, getRowKey }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map((c) => <th key={String(c.key)}>{c.header}</th>)}</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={getRowKey(item)}>
{columns.map((c) => (
<td key={String(c.key)}>
{c.render ? c.render(item[c.key], item) : String(item[c.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Хук useArray
function useArray<T>(initial: T[] = []): {
items: T[];
add: (item: T) => void;
remove: (index: number) => void;
clear: () => void;
} {
const [items, setItems] = useState<T[]>(initial);
return {
items,
add: (item) => setItems((prev) => [...prev, item]),
remove: (index) => setItems((prev) => prev.filter((_, i) => i !== index)),
clear: () => setItems([]),
};
}
const { items, add } = useArray<User>();
useFetch
function useFetch<T>(url: string): {
data: T | null;
loading: boolean;
error: Error | null;
} {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
try {
const res = await fetch(url);
const json = (await res.json()) as T;
if (!cancelled) setData(json);
} catch (e) {
if (!cancelled) setError(e as Error);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
const { data } = useFetch<User>('/api/user');
Ограничения
type KeyedListProps<T extends { id: number }> = {
items: T[];
onSelect: (id: number) => void;
};
function KeyedList<T extends { id: number }>({ items, onSelect }: KeyedListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.id}
</li>
))}
</ul>
);
}
T обязан иметь id: number.
Стрелочные дженерик-компоненты
const List = <T,>({ items, renderItem }: ListProps<T>) => {
return <ul>{items.map(renderItem)}</ul>;
};
Запятая после <T> нужна в .tsx, чтобы TypeScript не путал с JSX.
Итоги
- Дженерик-компоненты переиспользуются с разными типами
<T>перед пропсами- Ограничения через
extends - Стрелочные компоненты —
<T,> - Работают с хуками и утилитами