Привет, Хабр!
Начиная с версии ReactJS 16.8 в наш обиход вошли хуки. Этот функционал вызвал много споров, и на это есть свои причины. В данной статье мы рассмотрим одно из самых популярных заблуждений использования хуков и заодно разберемся стоит ли писать компоненты на классах (данная статья является расшифровкой видео).
Два пути
Как вы знаете в реакте есть 2 вариант написания компонента, с помощью классов и с помощью функций. И каждый вариант по своему взаимодействует с методами. Давайте рассмотрим оба варианта:
Метод в классе
Первый вариант, это использовать классы:
class Test extends Component {
onClick = () => {
console.log('onClick');
}
render() {
return (
<button onClick={this.onClick}>
test
</button>
)
}
}В данном варианте мы добавили метод onClick классу Test и при создании инстанса класса, этот метод создается 1 раз и в рендере мы уже используем ссылку на этот метод onClick={this.onClick}, таким образом при каждом рендере мы обращаемся всегда к одной и той же ссылке и не пересоздаем метод класса. Эта конструкция всем, кто давно в профессии, привычна и понятна даже если вы недавно пришли в React с другого языка программирования.
Метод в функции
Второй способ создания компонента является использование функции:
const Test = () => {
const onClick = () => {
console.log('onClick');
}
return (
<button onClick={onClick}>
test
</button>
)
}В таком подходе, чтобы создать обработчик onClick, мы описываем тело функции прямо внутри render, потому что все тело функции и есть render, другого варианта в принципе не существует, если вы хотите использовать props.
И тут у нас начинает зудеть в боку, да как это так, мы же теперь заново создаем функцию, абсолютно на каждый рендер. По сравнению с классами это как будто шаг назад.
Классы лучше чем функции?
Чтобы разобраться с этим вопросом я полез в React документацию в секцию вопросы и ответы и нашел там следующий вопрос:

Судя по документации создание инстанса класса для реакта настолько дорогостоящая операция, что создавать функцию на каждый рендер на порядок дешевле. Да и тот факт, что дерево становится глубже при использовании компонента высшего порядка connect от redux или бесконечных observer от mobX совсем не радует.
Кажется есть один "вариантик" сэкономить
Идею, создавать ��а каждый рендер новую функцию и думать что это дешевле, чем один раз создать инстанс класса, немного сложно принять разработчикам, потому что с нашей стороны мы должны писать код хуже, и верить что приложение ускорится. Звучит крайне противоречиво, а мы привыкли все оптимизировать.
Как результат, мы начинаем искать пути, как выйти на прежний уровень оптимизации с нашей стороны. И первое что гуглится, это начать использовать хук useCallback. И многие особо не вникая в суть происходящего начинают его активно использовать. Чтобы разобраться во всем этом давайте устроим небольшую викторину
Викторина!
Сейчас мы рассмотрим 2 примера и Вы попытаетесь ответить кто круче!
В одном углу ринга находится уже изученный нами ранее вариант написания обработчика события someFunction:
const Test = ({ title }) => {
const someFunction = () => {
console.log(title);
}
return (
<button onClick={someFunction}>
click me!
</button>
)
}В другом углу ринга находится точно такой же компонент, но уже решили завернуть функцию в useCallback.
const Test = () => {
const someFunction = useCallback(() => {
console.log(title);
}, [title])
return (
<button onClick={someFunction}>
click me!
</button>
)
}Для пользователя ничего не изменилось, console.log(title), точно так же вызывается при нажатии на кнопку.
Внимание вопрос
В каком из вариантов написания компонента функция присваемая переменной someFunction создается реже?

Даем минутку подумать...
Аккуратно ответ!
Ответ
И правильный ответ ни в каком! Да именно, никакой оптимизации useCallback нам не дал, функция создается ровно столько же раз, как и до оптимизации. Более того, мы наоборот ухудшили перфоманс нашего компонента.
Разбираем ответ
То что многих вводит в заблуждение - это представление что useCallback, это какой-то черный ящик, в который ты отдаешь функцию, с ней что-то происходит и после будет тебе счастье. Но давайте рассмотри как это работает на самом деле. Для начала сделаем typeof этого черного ящика:

Естественно мы получим результат function. По синтаксису это было очевидно. Чтобы понять как работает черный ящик, давайте сами напишем имплементацию useCallback.
Пишем свой useCallback
useCallback - это функция, которая принимает 2 параметра, callback и deps.
function useCallback (callback, deps) {
}Далее нам надо хранить где-то этот callback и deps, чтобы иметь возможность при очередном вызове вернуть ту же самую функцию callback.
const prevState = {
callback: null,
deps: null,
}
function useCallback(callback, deps) {
}Теперь рассмотрим разные случаи: если deps не существует либо в prevState, либо в новых данных, тогда нужно сохранить текущие параметры и вернуть callback.
const prevState = {
callback: null,
deps: null,
}
function useCallback(callback, deps) {
if (!prevState.deps || !deps) {
prevState.callback = callback;
prevState.deps = deps;
return callback;
}
}Если же deps ��уществуют. Тогда сравниваем какой-либо функцией массивы и если они совпадают, тогда возвращаем мемоизированную функцию.
const prevState = {
callback: null,
deps: null,
}
function useCallback(callback, deps) {
if (!prevState.deps || !deps) {
prevState.callback = callback;
prevState.deps = deps;
return callback;
}
if (shallowEqual(deps, prevState.deps)) {
return prevState.callback;
}
}Ну и если deps не совпадают, тогда снова сохраняем параметры и возвращаем текущий callback.
const prevState = {
callback: null,
deps: null,
}
function useCallback(callback, deps) {
if (!prevState.deps || !deps) {
prevState.callback = callback;
prevState.deps = deps;
return callback;
}
if (shallowEqual(deps, prevState.deps)) {
return prevState.callback;
}
prevState.callback = callback;
prevState.deps = deps;
return callback;
}Вроде бы мы покрыли все кейсы
Какие выводы из этого мы можем сделать?
Функция useCallback как и любая другая функция вызывается на каждый рендер и в качестве параметра callback каждый рендер приходит новая функция и новый массив зависимостей. Которые мы либо выбрасываем, если зависимости до и после совпадают, либо сохраняем в хранилище, для будущего использования.
Давайте теперь посмотрим на эту функцию со стороны компонента. Мы знаем, что useCallback это просто функция и мы можем извлечь передаваемые параметры в отдельные переменные.
const Test = ({ title }) => {
const callback = () => {
console.log(title);
}
const deps = [title];
const someFunction = useCallback(callback, deps);
return (
<button onClick={someFunction}>
click me!
</button>
)
}Тут становится совсем очевидно, что мы на каждый рендер создаем не то что функцию, а еще и массив с зависимостями, а потом еще и прокручиваем все это через useCallback.
Если мы просто закомментируем создание зависимостей и вызов useCallback и передадим параметр callback напрямую в onClick, тогда кажется перфоманс компонента должен улучшиться, ведь мы убрали посредника, который не нес никакой пользы для перфоманса.
const Test = ({ title }) => {
const callback = () => {
console.log(title);
}
// const deps = [title];
// const someFunction = useCallback(callback, deps);
return (
<button onClick={callback}>
click me!
</button>
)
}По итогу мы вернулись к начальной ситуации. Когда просто создавали функцию на каждый рендер.
Получается, в данном случае использовать хук useCallback - это не значит улучшить перфоманс, а скорее совсем наоборот, ухудшить перфоманс.

А для чего тогда нужен useCallback ?
Получается мы как то не так используем useCallback. Чтобы разобраться в этом, давайте обратимся к документации:

Получается основная идея не в улучшении перформанса в конкретном компоненте, а скорее использование useCallback выгодно только в случае передачи функции как props. Давайте рассмотрим еще один пример.
Допустим у нас есть список машин, который мы хотим отобразить:
const Cars = ({ cars }) => {
return cars.map((car) => {
return (
<Car key={car.id} car={car} />
)
});
}Тут нам понадобилось добавить обработчик клика на машину. Мы создаем метод onCarClick и передаем его в компонент Car.
const Cars = ({ cars }) => {
const onCarClick = (car) => {
console.log(car.model);
}
return cars.map((car) => {
return (
<Car key={car.id} car={car} onCarClick={onCarClick} />
)
});
}В такой ситуации на каждый рендер компонента Cars у нас создается новая функция onCarClick, соответственно, не важно Car это PureComponent или обернут в memo, все машины всегда будут заново рендерится, т.к. получают каждый раз новую ссылку на функцию.
Для этого и нужен useCallback, если мы завернем функцию в хук, то у нас в переменной onCarClick будет уже возвращаться мемоизированая функция, хоть мы в нее на каждый рендер и передаем новую функцию
const Cars = ({ cars }) => {
const onCarClick = useCallback((car) => {
console.log(car.model);
}, []);
return cars.map((car) => {
return (
<Car key={car.id} car={car} onCarClick={onCarClick} />
)
});
}Таким образом все компоненты Car не будут рендериться лишний раз, т.к. ссылка на функцию останется прежней.
А если заглянуть внутрь компонента Car. Там мы создадим еще одну функцию, которая свяжет onCarClick и объект car.
const Car = ({ car, onCarClick }) => {
const onClick = () => onCarClick(car);
return (
<button onClick={onClick}>{car.model}</button>
)
}В этом случае нет никакой пользы оборачивать метод в useCallback, т.к. нам не важно, ссылка это на функцию с прошлого рендера или с текущего рендера, а useCallback как мы уже знаем не бесплатный.
Итоги
Подытожить данную статью можно следующими словами. React хоть и поддерживает компоненты в виде классов, но имеет больше маневров над ускорением именно компонентов в виде функций. Да и сама экосистема все больше в качестве API предоставляет вам именно хуки, что невозможно использовать в классах:
import { useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
import { useLocalObservable } from "mobx-react-lite";
import { useTranslation } from "react-i18next";И конечно, доверяйте реакту, если они сказали лучше создавать функцию на каждый рендер, так и делайте, ведь они заинтересованы только в улучшении перформанса вашего проекта.
А если вам понравилось данная статья, то здесь есть еще немного интересного.
Чао