Урок 09

Дженерик-компоненты

Компоненты могут принимать параметр типа для переиспользования с разными типами данных.

Базовый пример

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,>
  • Работают с хуками и утилитами