В React useCallback используется для мемоизации функций,
чтобы предотвратить их пересоздание при каждом рендере компонента. Это
особенно важно, когда функции передаются в дочерние компоненты или
используются в эффектах (useEffect, useMemo).
Тестирование таких функций требует понимания, что ключевой аспект здесь
— поведение компонентов в ответ на изменения
зависимостей, а не сама функция как объект.
useCallback возвращает мемоизированную версию функции,
которая пересоздаётся только при изменении указанных зависимостей. Прямо
протестировать внутреннюю ссылку функции обычно нет необходимости.
Важнее убедиться, что:
Пример:
import { render, screen, fireEvent } from '@testing-library/react';
import React, { useState, useCallback } from 'react';
function Counter({ onIncrement }) {
return <button onCl ick={onIncrement}>Increment</button>;
}
function App() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<>
<div>Count: {count}</div>
<Counter onIncrem ent={increment} />
</>
);
}
test('увеличивает счетчик при клике', () => {
render(<App />);
const button = screen.getByText('Increment');
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
В данном примере тест проверяет эффект действия функции, а не её идентичность. Это ключевая стратегия: проверка поведения вместо прямой проверки объекта функции.
Когда функция передается как пропс в дочерний компонент, важно
убедиться, что мемоизация предотвращает лишние рендеры дочернего
компонента. Для этого используют jest.fn() и отслеживают
вызовы рендера.
Пример:
import React, { memo, useCallback, useState } from 'react';
import { render, fireEvent } from '@testing-library/react';
const Child = memo(({ onClick }) => {
return <button onCl ick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<>
<div>Count: {count}</div>
<Child onCl ick={handleClick} />
</>
);
}
test('Child не ререндерится без изменения callback', () => {
const { rerender } = render(<Parent />);
const button = document.querySelector('button');
const initialRenderSpy = jest.fn();
initialRenderSpy();
fireEvent.click(button);
rerender(<Parent />);
expect(initialRenderSpy).toHaveBeenCalledTimes(1);
});
Использование memo позволяет проверить, что компонент
не перерисовывается без необходимости, что является
прямым следствием правильной работы useCallback.
Частой ошибкой является неверная настройка зависимостей в
useCallback. Тестирование должно покрывать сценарии,
когда:
Пример:
function Component({ value }) {
const handleClick = useCallback(() => {
console.log(value);
}, [value]);
return <button onCl ick={handleClick}>Click</button>;
}
test('callback пересоздается при изменении value', () => {
const { rerender } = render(<Component value={1} />);
const firstButton = document.querySelector('button');
const firstHandler = firstButton.onclick;
rerender(<Component value={2} />);
const secondButton = document.querySelector('button');
const secondHandler = secondButton.onclick;
expect(firstHandler).not.toBe(secondHandler);
});
Здесь проверяется смена функции при изменении зависимости, что критично для правильной работы мемоизации и предотвращения багов.
useCallback часто сочетается с useMemo,
когда функции используют сложные вычисления. Тестирование должно
охватывать:
Пример:
import React, { useState, useCallback, useMemo } from 'react';
import { render, fireEvent } from '@testing-library/react';
function ExpensiveComponent({ factor }) {
const [count, setCount] = useState(0);
const multiply = useCallback(
(num) => num * factor,
[factor]
);
const result = useMemo(() => multiply(count), [count, multiply]);
return (
<>
<div>Result: {result}</div>
<button onCl ick={() => setCount(prev => prev + 1)}>Increase</button>
</>
);
}
test('результат пересчитывается при изменении count', () => {
const { getByText } = render(<ExpensiveComponent factor={2} />);
const button = getByText('Increase');
fireEvent.click(button);
expect(getByText('Result: 2')).toBeInTheDocument();
});
Тест проверяет корректность вычислений, а мемоизация гарантирует, что
функция multiply не пересоздается без изменения
factor.
jest.fn() для контроля вызовов дочерних
компонентов.useCallback с memo для
оптимизации рендеров.useMemo совместно с
useCallback и покрывать тестами изменения
зависимостей.Эти методы позволяют покрывать ключевые сценарии мемоизации, обеспечивая надёжность компонентов и предотвращая лишние перерендеры в React-приложениях.