Стратегии типизации компонентов
Теперь пора посмотреть, как правильная типизация компонентов может помочь нам сократить лишние рендеры и улучшить читаемость и поддержку кода.
Давайте начнем с практики. У нас есть три основные сущности для типизации в React:
- Props — данные, которые компонент принимает извне.
- State — внутреннее состояние компонента.
- Return type — результат работы компонента.
На практике, правильная типизация этих сущностей позволяет нам строить производительный и надёжный код.
Типизация props
Начнем с самого простого сценария. Допустим, у нас есть компонент, который отображает информацию о пользователе.
import React from "react";
// Определяем интерфейс для props
interface UserInfoProps {
name: string;
age: number;
}
// Компонент типизируется пропсами
const UserInfo: React.FC<UserInfoProps> = ({ name, age }) => {
return (
<div>
<h3>{name}</h3>
<p>Возраст: {age}</p>
</div>
);
};
// Пример использования
<UserInfo name="Иван" age={30} />
⚡ Обратите внимание: мы используем React.FC (функциональный компонент) для типизации. Он автоматически добавляет типы для children, что очень удобно.
Но бывает, что нам нужно указать пропсы опциональными. В таком случае просто добавляем ?:
interface UserInfoProps {
name: string;
age?: number; // Этот пропс теперь необязательный
}
const UserInfo: React.FC<UserInfoProps> = ({ name, age }) => {
return (
<div>
<h3>{name}</h3>
<p>Возраст: {age || "Не указан"}</p>
</div>
);
};
Типизация состояния (state)
Теперь представим, что у нас есть компонент с состоянием. Например, компонент счётчика.
import React, { useState } from "react";
// Определяем интерфейс для состояния
interface CounterState {
count: number;
}
const Counter: React.FC = () => {
// useState типизируется явно
const [state, setState] = useState<CounterState>({ count: 0 });
const increment = () => setState({ count: state.count + 1 });
return (
<div>
<p>Счётчик: {state.count}</p>
<button onClick={increment}>+</button>
</div>
);
};
Если наше состояние было бы более сложным (например, вложенные объекты), мы также могли бы вынести его описание в отдельный интерфейс.
Типизация return type
В большинстве случаев TypeScript сам способен определить тип возвращаемого значения функции. Однако иногда, например, при использовании HOC, явное указание типа может быть полезным.
const Button: React.FC<{ label: string }> = ({ label }) => {
return <button>{label}</button>;
};
const withLoading =
<P extends object>(Component: React.ComponentType<P>) =>
(props: P & { isLoading: boolean }) => {
if (props.isLoading) {
return <p>Загрузка...</p>;
}
return <Component {...props} />;
};
const WrappedButton = withLoading(Button);
<WrappedButton label="Клик!" isLoading={false} />;
Как типизация помогает оптимизировать рендеринг
Оптимизация с мемоизацией
Когда мы используем такие инструменты, как React.memo, типизация играет ключевую роль в проверке входных данных. Взгляните на пример:
import React from "react";
interface DataViewProps {
data: string[];
}
const DataView: React.FC<DataViewProps> = React.memo(({ data }) => {
console.log("Ререндер компонента");
return (
<ul>
{data.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
});
const ParentComponent: React.FC = () => {
const data = ["React", "TypeScript", "Optimization"];
return <DataView data={data} />;
};
Благодаря React.memo, компонент DataView не ререндерится, если data не изменяется. А типизация позволяет нам быть на 100% уверенными, что формат данных корректный!
Типизация зависимостей в хуках
Типизация становится особенно полезной, когда мы используем хуки вроде useCallback или useMemo.
import React, { useCallback } from "react";
interface ButtonProps {
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ onClick }) => {
return <button onClick={onClick}>Клик!</button>;
};
const ParentComponent: React.FC = () => {
const handleClick = useCallback(() => {
console.log("Кнопка нажата!");
}, []); // Типизация обеспечит правильные зависимости!
return <Button onClick={handleClick} />;
};
Типизация в ленивых компонентах (React.lazy)
Когда мы загружаем компоненты динамически, важно быть уверенными в их типизации. React и TypeScript заставляют нас быть немного аккуратнее:
import React, { lazy, Suspense } from "react";
// Динамически загружаемый компонент
const LazyComponent = lazy(() => import("./LazyComponent"));
const App: React.FC = () => {
return (
<Suspense fallback={<div>Загрузка...</div>}>
<LazyComponent />
</Suspense>
);
};
Для проверки типов импортируемого компонента мы можем использовать assertion:
const LazyComponent = lazy(() =>
import("./LazyComponent").then((module) => ({
default: module.LazyComponent as React.ComponentType<{ propA: string }>,
}))
);
Теперь любой неправильный пропс для LazyComponent будет выявлен на этапе компиляции.
Подводные камни типизации
Хотя типизация очень помогает, бывают ситуации, где нужно быть осторожным:
- Слишком сложные типы: если вы чувствуете, что пишете больше типов, чем кода, стоит упростить.
- Опциональные пропсы: не забывайте задавать значения по умолчанию через
defaultPropsили в функции. - Перекрестные зависимости: типы, завязанные друг на друга, могут вызывать головную боль.
Код, который мы построили сегодня, — не только способ избежать багов и головной боли, но и возможность заставить ваше приложение работать быстрее и надёжнее. Впереди нас ждут ещё более сложные механики оптимизации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ