Зачем типизировать функции в useCallback
Мотивация здесь простая: с типами легче жить. Без грамотно описанных типов в масштабных приложениях вы рискуете столкнуться с огромным количеством багов, связанных с несовместимостью данных. TypeScript позволяет нам не только избежать большинства этих ошибок, но и наглядно описывать, что мы ожидаем на вход и хотим получить на выход.
useCallback принимает две вещи: колбэк-функцию и массив зависимостей. Колбэк — это ваш драгоценный кусочек логики, который должен быть правильно типизирован. Но вот беда: если забыть указать типы, можно пропустить тот момент, когда передали совершенно не то, что нужно. Начинаем разбираться, как этого избежать.
Типизация функций в useCallback
Основной синтаксис
Для начала посмотрим, как типизировать простейшую функцию в useCallback:
import React, { useCallback } from "react";
// Интерфейс для пропсов компонента
interface MyButtonProps {
onClick: (id: number) => void; // Функция принимает число и ничего не возвращает
}
// Компонент, который принимает обработчик клика
const MyButton: React.FC<MyButtonProps> = ({ onClick }) => {
return <button onClick={() => onClick(42)}>Click me</button>;
};
// Родительский компонент
const App: React.FC = () => {
// Функция с мемоизацией
const handleClick = useCallback((id: number): void => {
console.log(`Button clicked with ID: ${id}`);
}, []); // Зависимости указываются вот здесь — мы их обсудим чуть позже
return <MyButton onClick={handleClick} />;
};
export default App;
В данном примере мы:
- Определили интерфейс пропсов для компонента
MyButton, где чётко указали, чтоonClick— это функция, принимающаяid: numberи ничего не возвращающаяvoid. - В
Appтипизировали функцию вuseCallbackаналогично, чтобы всё было согласовано.
А если возвращаемое значение не void?
Давайте чуть усложним: предположим, что функция должна возвращать строку.
const useFormattedMessage = (): ((name: string) => string) => {
return useCallback((name: string): string => {
return `Hello, ${name}!`;
}, []);
};
const App: React.FC = () => {
const getMessage = useFormattedMessage();
console.log(getMessage("Alice")); // Hello, Alice!
return null;
};
Вот так просто: если вы возвращаете что-то полезное, тип возвращаемого значения всегда указывается после двоеточия как часть определения функции (: string в данном случае).
Типизация через интерфейсы
Иногда лучше вынести типы в интерфейсы, чтобы всё было более аккуратно (и пригодилось в других местах):
// Тип для функции
interface CalculateSum {
(a: number, b: number): number;
}
const App: React.FC = () => {
// Типизируем через интерфейс
const calculateSum: CalculateSum = useCallback((a, b) => a + b, []);
console.log(calculateSum(3, 4)); // 7
return null;
};
Тип в интерфейсе задаёт "контракт", что удобнее, если такую функцию нужно передавать в разные места.
Управление зависимостями в useCallback
Что это за зависимости?
Если вы не совсем уверены, почему у useCallback есть массив зависимостей, попробуем просто: зависимости — это всё, что может измениться и повлиять на результат функции. React следит за тем, чтобы ваша мемоизированная функция обновлялась только при изменении перечисленных значений.
Пример:
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
const increment = useCallback(() => {
setCount(count + 1); // Заметьте, мы используем count внутри функции
}, [count]); // Зависимость — count
return <button onClick={increment}>Increment</button>;
};
Если мы не укажем count в зависимости, React будет держать "старую" версию функции, даже если счётчик обновится. Ваш пользователь нажмёт кнопку, а ничего не произойдёт — так-то!
Ошибки при работе с зависимостями
Если вы забыли указать зависимость, получите баг: мемоизированная функция будет работать с устаревшими значениями.
Пример ошибки:
// ОШИБКА: count не добавлен в зависимости
const increment = useCallback(() => {
setCount(count + 1);
}, []); // Пустой массив зависимостей
React даже предупредит в консоли: "React Hook useCallback has a missing dependency".
Исправляем:
const increment = useCallback(() => {
setCount((prev) => prev + 1); // Используем callback-версию setState
}, []); // Теперь зависимости не нужны!
Использование prev — один из способов обойти необходимость зависимостей, если вам неважно текущее состояние.
Перенасыщенные зависимости
Добавлять всё подряд в массив тоже плохо. Разберём пример:
const App: React.FC = () => {
const [text, setText] = React.useState('');
const updateText = useCallback(() => {
console.log('Text changed');
}, [text]); // НЕ НУЖНО указывать text, если updateText не использует его!
return <input onChange={updateText} />;
};
Хотя updateText логически не зависит от text, ошибка программиста заставляет React пересоздавать функцию при каждом изменении text. Убираем эту зависимость.
Порядок управления зависимостями
Давайте соберём всё воедино:
- Указывайте только те зависимости, которые влияют на внутреннюю логику функции.
- Если функция зависит только от состояния, используйте callback-версию
setState. - Типизируйте массив зависимостей: если передаёте функции, они тоже должны быть соответствующим образом описаны.
Примеры типичных ошибок и их исправление
- Ошибка: использование неинициализированной переменной.
const App: React.FC = () => {
const myVar = 42;
const logVar = useCallback(() => {
console.log(myVar);
}, []);
return <button onClick={logVar}>Log</button>;
};
// myVar не указан в зависимостях -> БАГ!
Исправление:
const logVar = useCallback(() => {
console.log(myVar);
}, [myVar]);
- Ошибка: переопределённые значения вызывают постоянные обновления.
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return <button onClick={increment}>+</button>;
};
// React будет пересоздавать функцию при каждом рендере из-за count.
Исправление:
const increment = useCallback(() => {
setCount((prev) => prev + 1); // Контролируем через prev
}, []);
На этом мы завершаем разбор типизации в useCallback и управления зависимостями. Не забудьте потренироваться типизировать и оптимизировать свои компоненты!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ