Привет Хабр!
Как вы знаете при переходе с компонентов классов на функциональные, у нас отняли такую полезную вещь как this, которая указывает на текущий экземпляр компонента. И конечно у нас возник вопрос: “а где же тогда хранить timeoutId
?”. И я видел как люди по разному выкручивались из этой проблемы (Данная статья, является расшифровкой видео)
Например, если timeoutId
используется только в рамках одного useEffect
можно набросать следующий вариант:
useEffect(() => {
const timeout = setTimeout(() => {
// do some action
}, 3000);
return () => {
clearTimeout(timeout);
}
}, [...]);
Данный подход работает, но когда появляется нужда очищать timeout
по клику на кнопку, этот подход уже не работает.
Поэтому многие решили, просто создавать переменную вне компонента и хранить в ней id
этого тайм-аута, чтобы например по клику иметь возможность его отменить:
let timeout;
const Test = () => {
const onClick = () => clearTimeout(timeout);
useEffect(() => {
timeout = setTimeout(() => {
// do some action
}, 3000);
}, [...]);
return (...);
}
И это работает в большинстве случаев без каких-либо проблем. Но как всегда есть “НО”.
Проблема глобальных переменных
Давайте рассмотрим пример. Допустим у нас есть компонент Counter, в котором есть локальный счетчик и глобальный счетчик определенный вне компонента:
let globalCounter = 0;
const Counter = () => {
const [stateCounter, setStateCounter] = useState(0);
const onClick = () => {
globalCounter++;
setStateCounter((stateCounter) => stateCounter + 1);
};
return (
<div>
<p>global counter - <b>{globalCounter}</b></p>
<p>state counter - <b>{stateCounter}</b></p>
<button onClick={onClick}>increment</button>
</div>
);
}
Компонент достаточно простой. Теперь добавим родительский компонент:
const App = () => {
const [countersNumber, setCountersNumber] = useState(0);
return (
<div>
<button onClick={setCountersNumber((count) => count + 1)}>
add
</button>
<button onClick={setCountersNumber((count) => count - 1)}>
removed
</button>
{[...Array.from(countersNumber).keys()].map((index) => (
<Counter key={index} />
))}
</div>
);
};
Здесь мы храним в state
количество счетчиков, и ниже имеем 2 кнопки: для увеличения количества счетчиков и для уменьшения. И собственно вставляем сами счетчики в таком количество, как у нас указано в переменной countersNumber
.
Смотрим результат
Перейдем в браузер и выполним следующие действия:
Добавим один счетчик;
Внутри появившегося счетчика, нажмем "increment" три раза;
Добавим второй счетчик.
Как вы видите у второго глобального каунтера значение три. И не важно сколько счетчиков вы создадите, они все замыкаются на одну и ту же глобальную переменную. И даже если вы удалите все счетчики, а потом добавите новый счетчик, конечно же глобальный каунтер все равно будет равен трем, так как глобальная переменная создается при запуске сайта и очищается при полном закрытии табы в браузере с вашим сайтом.
Таким образом, хранение timeoutId
в такого рода глобальной переменной, может привести к странным багам. Да и если у вас в проекте огромное количество таких переменных, вы бесполезно засоряете память вашего компьютера, хоть это возможно и экономия на спичках.
Рассмотрим альтернативу
Решением данной проблемы является использование хука useRef()
. Именно это и рекомендует React документация:
Они прямо упомянули, что useRef()
нужно использовать как аналог this
. И более того, для удобства добавили в useRef()
возможность передачи начального значения. Поэтому вариант с timeout
может выглядеть следующим образом:
const Test = () => {
const timeout = useRef();
const onClick = () => clearTimeout(timeout.current);
useEffect(() => {
timeout.current = setTimeout(() => {
// do some action
}, 3000);
}, [...]);
return (...);
}
Возможно в этом решении вас смущает, то что в timeout
начинает храниться свойство current
, это действительно выглядит немного странно, но у этого есть разумное объяснение, о котором мы рассказывали в предыдущей статье “createRef, setRef, useRef и зачем нужен current в ref”.
prevProps не исчезли вместе с классами
Использование useRef()
для хранения timeout
это конечно же очень полезно. Но есть и более интересные способы использования. Например, в компонентах в виде классов есть удобный метод жизненного цикла componentDidUpdate
. В качестве первого параметра нам предоставляют prevProps
, т.е. props
из предыдущей итерации. Это давало нам возможность, сравнивать props
из текущей итерации с props
из предыдущей. На основании этого выполнять какие-то действия.
componentDidUpdate(prevProps) {
if (this.props.id !== prevProps.id) {
// do some action
}
}
Если вам кажется, что эту функциональность у вас отняли безвозвратно, тогда вы ошибаетесь.
Давайте напишем хук, который будет возвращать props
из предыдущей итерации:
const useGetPrevValue = (value) => {
const prevValueRef = useRef();
useEffect(() => {
prevValueRef.current = value;
});
return prevValueRef.current;
};
Здесь мы получаем value
из текущей итерации, после создадим ref
для хранения данных между итерациями. И в рамках текущей итерации мы вернем текущее значение current
равное null
. Но перед началом следующей итерации мы обновим current
значение, таким образом в следующей итерации в ref
у нас будет хранится значение из предыдущей.
И осталось только использовать этот хук:
const CounterView = ({ counter }) => {
const prevCount = useGetPrevValue(counter);
const classes = classNames({
[styles.greenCounter]: counter < prevCounter,
[styles.redCounter] counter > prevCounter,
});
...
}
В итоге, имея значение counter
из предыдущей итерации и из текущей итерации, мы можем покрасить в зеленый цвет, если counter
уменьшился, или в красный, если counter
увеличился.
Расширяйте сознание
Мы упомянули лишь несколько базовых вариантов использования ref
для решения нетипичных для ref
задач. Но их гораздо больше. Например, если вы откроете телеграм десктоп версии, и будете скролить вверх, то у вас будет отображаться дата, а если вниз, она исчезнет. Мы можем хранить предыдущее значение скрола в ref
, для того чтобы вычислить в какую же сторону скролит пользователь.
Как вы уже догадались, использование ref
ограничено лишь вашей фантазией и вы найдете ему гораздо больше вариантов применения, чем мы упомянули в этой статье.
И если вы знаете еще какие-то интересные варианты использования ref
обязательно пишите в комментариях
Alexandroppolus
В рефах хорошо хранить вью-модели, оборачивая их создание в пользовательский хук. Таким образом выносим логику из представления (не придется городить лапшу на хуках), а модель получает доступ к жизненному циклу компонента, в частности, очистку перед сносом.
Sin9k Автор
Интересный вариант использования) я о таком даже не думал)
а что включает ваша viewModel?
onClick обработчики включает? как значение там обновляете, если к примеру redux-store обновился и прочее) если выложите черновик где-нибудь, было бы круто!
Alexandroppolus
Вот примерный набросок для MobX
Alexandroppolus
забыл "return ref.current;" в последней функции
Sin9k Автор
очень интересная имплементация)
мы тоже пишем на mobX, но с хуками мучаемся во ViewModel)
Возможно себе возьмем на вооружение какие то подходы)
Спасибо!)
Alexandroppolus
В принципе, у мобикса из коробки есть что-то похожее: https://github.com/mobxjs/mobx-react#uselocalobservable-hook
Sin9k Автор
мы его и используем)
MaZaAa
Как бы вот так надо, зачем лишние манипуляции и лишний быдло-код? =)
Alexandroppolus
Вьюха засоряется лишним кодом, который к ней не относится (нарушение SRP)
Если оную модель захочется поюзать ещё где, придется копипастить всю логику (нарушение DRY)
Сильная зацепленность, вьюха должна знать, что модели требуется очистка, и что модель создается конструктором, и вообще что это вью-модель, а не обычная. Эти знания не нужны для функциональности вьюхи.
Ещё до кучи нарушение DIP.
В общем, одни плюсы :)
MaZaAa
Ну ладно
strannik_k
Тоже подумывал об отделение логики от View в функциональных компонентах. В компонентах на классах с этим было проще.
Спасибо за интересный пример! Он демонстрирует широкие возможности для написания функциональных компонентов с более понятным и предсказуемым потоком управления. Осталась дождаться, когда MobX переплюнет Redux, разделение логики и View в компоненте станет стандартом и затем компоненты снова переделают на классы или объекты)
Не вижу необходимости в хуке useMyViewModel. ViewModel в нем заменить нельзя, а его нельзя заменить компоненте. Можно сразу класс передать в хук useViewModel.
Подумал также, что подход из вашего примера по аналогии с custom hooks позволяет повторное использование ViewModel и их взаимодействие в одном компоненте при необходимости:
Alexandroppolus
Ну это вряд ли, эволюция показала, что функциональные компоненты всё-таки лучше. ООП идеально для логики, вот и будет отдельно. А компоненты-классы, это боль, боль, боль… Сейчас вот разгребаю по работе, увы. С каждым днём всё яснее видится мне величие принципа SRP..
Да нет, это важный момент. На самом деле сейчас подумал, что лишнее тут только слово View. Надо useMyModel. По функциональности, в общем. То что это временная модель, компоненту-пользователю модели не интересно.
"Персональный" хук-поставщик (причем даже не для конкретного класса, а для интерфейса) позволяет уменьшить зацепленность. Можно менять жизненный цикл у модели, можно резолвить некоторые её зависимости, не меняя использующий код, можно создавать экземпляры подклассов при некоторых условиях. В общем, идеальное связующее звено.
strannik_k
Это разве сильно отличается от идеи сделать компонент объектом/классом?
Согласен, что функции идеальны для JSX кода.
Но и над компонентами-классами можно подшаманить и функцию render выносить отдельно и применять к любому компоненту, как и разбить компонент на составляющие, которые также можно применять к любому компоненту.
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.
Насчет персонального хука-поставщика.
Если где-то нужно добавить некую логику между компонентом и моделью, то да, его стоит добавлять. Может местами даже цепочку хуков.
Не увидел смысла, когда он показан как в примере, где он ничего не делает и не отделим от класса модели.
Alexandroppolus
Это совершенно разные вещи. Компонент остается функцией, а использует, повторюсь, не классы, а решателей определенных задач.
Было уже. Итогом развития этой идеи стали HOC'и. Чтобы обеспечить представление необходимыми интерфейсами, приходилось обертывать несколько раз. Неудобно, негибко. Думаю, тут не надо подробно расписывать.
strannik_k
Вы же не считаете, что аналоги декораторов в виде HOC — это предел совершенства композиции в ООП и что авторы React первые, кто столкнулся с тем, что надо предоставить возможность разработчикам расширять что-то подобное компонентам React и в других областях не существует других решений, которые авторам React не известны? Есть, просто во фронтенде один юный разработчик умудрился переключить внимание почти всего сообщества с объектов на функции и перестали искать другие решения для компонентов-объектов.
Как я уже упоминал, скоро
Скину потом сюда ссылку.
mrlika
Обычно за код, приведенный в статье, джунов отправляют учить функции очистки.
В большинстве случаев он хранится в замыкании функции очистки.
Приимущества:
Sin9k Автор
Подход интересный :)
Но я бы делать так не стал по нескольким причинам:
— при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого
— читабельность тоже как по мне не особо улучшилась, теперь в этом коде стало гораздо React чем JavaScript. Чтобы обнулить timeout мы вызываем не clearTimeout, а вызываем какую-то функцию абстракцию, на которую где-то в другом месте подписан useEffect, который в return возвращает функцию, которая обнулит timeout. По мне так, этот вариант и есть спагетти когда одно за другое зацепленно, как макарошки вокруг вилки
mrlika
То что вы описали и есть React программирование с использованием хуков. Оно подразумевает:
— больше React, чем JavaScript
— накладные расходы в пользу большей декларативности
> при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого
Если для вашего проекта это дорого, то вы либо не должны использовать React в принципе, либо занимаетесь преждевременной оптимизацией.
Sin9k Автор
Я думаю здесь конфликтуют две идеологии, в основе которых мы принимаем решения, писать код так или иначе.
Я не считаю вашу идеологию плохой, но мне ближе писать в первую очередь на JS чем на React, т.к. это лишь библиотека для отображения :)
mrlika
Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.
Они сейчас посмотрят на этот код, скопипастят, поменяют setTimeout на setInterval, и получат ай-яй-яй. И потом окажется, что useRef для управления ресурсами не очень то и подходит.
А вы не путаете React и JSX? useState, useEffect, useMemo, useRef, useCallback, useReducer не имеют прямого отношения к отображению.
Я пишу сейчас 3D редактор на React, где для отображения используется Babylon.js. React используется как декларативная state machine и для управления ресурсами. Куча React кода, а из отображения там только
Sin9k Автор
> Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.
React написан на JS как тут можно их не мешать я не очень представляю)
Да и контент который я публикую, он никак не направлен на новичков. Для новичков и так курсов / уроков / статей пруд пруди. А вот для senior разрабов контента порассуждать не хватает. Собственно поэтому я и начал свою деятельность.
Спор дальнейший не вижу особого смысла продолжать :) очевидно что мы оба останемся при своем мнении)