Комментарии 70
Что, правда есть люди, которые так делают?
Жду статей "Если слишком долго держать в руках раскалённую докрасна кочергу, в конце концов обожжёшься", "Если поглубже полоснуть по пальцу ножом, из пальца обычно идёт кровь" и "Если разом осушить пузырёк с пометкой «Яд!», рано или поздно почти наверняка почувствуешь недомогание".
Для меня странна сама мысль ждать в данном случае магии от useCallback.
Наверное, дело в том, что я не JavaScript-разработчик (хотя приходится) и не знаю React. Поэтому для меня useCallback – это просто функция какая-то, чтобы передать ей аргумент – надо его вычислить (т.е. создать передаваемую функцию… если вначале аргумент присвоить какой-то переменной – всё становится совсем уж прозрачно).
Фреймворкм должны быть просты, понятны и интуитивны.
Разработчики Реакт каждый раз создают новые хаки, чтоб этот шаткий домик не развалился — на это больно смотреть.
Еще как эти хуки/хаки покрывать юнит тестами?
React core разработчики говорили по их статистике разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить, а когда лучше создать метод как arrow function. Для людей, кто это уже освоил давно, кажется очень простой задачей работать с классами, но видимо это не так.
разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить,ну и я сомневаюсь, что такие новички на хорошем уровне понимают, как использовать хуки. Скорее всего, делают кучу ошибок, и не замечают их.
React core разработчики говорили по их статистике разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js
Но практика показала что замыкания сами по себе и хуки в частности взрывают мозг разработчикам куда эффективнее, чем this
keyword :)
Судя по собеседованиям для большинства разработчиков хуки это "новая магия". Причём не важно сколько лет опыта у человека. Озвученный вопрос в статье про создание метода я частенько спрашивал на собеседованиях (у тех кто смог ответить на элементарные вопросы). Видел как тяжело идёт понимание, что если мы создали функцию… ты мы её создали. И да и она и dependencies создаются вообще всегда с нуля, просто при удачном раскладе отбрасывается результат. Вот прямо видно было как искра в глазах появляется. Эдакое "эээврика, ничего себе… А я и не думал об этом раньше с этого угла".
С классами многие вещи было реализовать сложно и неудобно\некрасиво. Однако относительно прямолинейно и понятно. Пишем метод. Добавляем новое поле в state. Берём методы жизненного цикла. Разгребаем баги потому что не учли кучу нюансов (ну или забиваем на баги). А тут прямо приходится мышление перестраивать под нечто более декларативное. И пока мозг рассматривает всё как "маааагия" получается ерунда.
Видел как тяжело идёт понимание, что если мы создали функцию… ты мы её создали. И да и она и dependencies создаются вообще всегда с нуля, просто при удачном раскладе отбрасывается результат.
Я точно соглашусь с со словами про собеседование, точно такая же картина и у меня, поэтому и создал это видео и написал статью.
Есть лишь один момент в котором я сомневаюсь. Все мы с класами работали еще на php или Java или на других языках, когда JS использовался очень минимально в проекте. А что если нам с классами все просто и понятно именно потому что у истории программирования много лет опыта в этой стезе, уже куча статей как решать любую проблему с классами и мы уже все это давно прошли. Возможно и с хуками решится эта проблема, когда пройдет через хабр 10-ки статей и это уже будет все просто и очевидно
Ответа я конечно не знаю, но вопрос который я задаю себе: не являются ли все эти проблемы с хуками просто борьба с чем то новым? Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?
Чтобы это понять, планирую общаться с новыми поколениями, как они вообще воспринимают хуки
Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?
Мне очень нравится писать на хуках. Я нахожу их гораздо более удобными чем ООП модель. Но всё же я не могу не отметить, что людям хуки даются с большим скрипом. И скажем многие "middle" разработчики требуют долгого времени адаптации. А как в серьёзный проект вливаться junior-у ума не приложу. Кажется React просто отрезает их как класс. Для них и без того очень много магии, а тут она сразу в квадрате. Давать им задачи на вёрстку? Так они закономерно взвоют. Да и не так много таких задач.
Ответа я конечно не знаю, но вопрос который я задаю себе: не являются ли все эти проблемы с хуками просто борьба с чем то новым? Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?У меня такой вопрос отпадает при сравнение, в каком из двух примеров код проще, читабельней, меньше вероятность допустить ошибку, в том числе ошибку, влияющую на производительность:
const someFunction = useCallback((title) => {
console.log(title);
}, []);
someClassFunction = (title) => {
console.log(title);
};
Angular не пользуется популярностью. Кто-то из вредности засадил.
На фрилансе он как-бы половина от реакта (за вычетом react native)
Основная причина в том что в одном мажорном релизе полностью сломали обратную совместимость. Многие проекты не смогли переехать и застряли на старой версии, многие разработчики обиделись и завязали с ангуляром. С тех бытует небезосновательное мнение что все проекты на ангуляр это легаси.
Картинка из прошлого поста в ленте как раз в тему...
const Test = () => {
const someFunction = useCallback((title) => {
console.log(title);
}, [title])
return (
<button onClick={someFunction}>
click me!
</button>
)
}
В этом примере не должно быть либо параметра title
, либо переменной title
в списке зависимостей. Судя по всему, параметр лишний.
Проект, полностью написанный на хуках, имеет менее глубокое дерево компонентов.Смотря, какими техниками пользоваться. Если ограничиваться техниками, распространёнными в реакт-сообществе, то да. Но в других стеках есть и другие техники.
У меня не вызывает сомнений, что создании функции быстрее создания класса. Вызывает сомнение другое — что рендеринг компонента-функции с кучей не JSX кода, быстрее функции render в классе в уже созданном компоненте) Понимаю, что механизм жизненного цикла в классах в целом медленнее аналога на хуках, но думаю, что его можно было оптимизировать по аналогии с хуками.
Пишем свой useCallbackЯ бы с удовольствием посмотрел на читабельные реализации основных хуков. А то по их исходникам не понятно. Слишком все разбросано по разным файлам.
К сожалению, в этом примере prevState существует только в одном экземпляре и в нескольких компонентах этот useCallback использовать не получится. Интересно, как сделать так, чтобы внутренние механизмы react-а сохраняли prevState, связав его с компонентом, в котором он используется, чтобы полноценный аналог useCallback написать. Наибольшее преставление о связывании компонента с хуком мне дал этот ответ — stackoverflow.com/a/53730788
К сожалению, в этом примере prevState существует только в одном экземпляре
useRef
читабельные реализации основных хуков
На vdom ноду крепится доп инфа и все, только это скрыто. Если это понять, то с хуками все просто.
const MyComponent = (props, currentNode) => {
// const useRef = (initValue, cn) => cn.refs[cn.refIndex++] || initValue
const ref = useRef(null, currentNode);
const [value, setValue] = useState(false, currentNode);
};
Вот в такой форме понятней думаю было бы, но зачем-то они скрыли и сделали магию. Т.е сейчас внутри либы есть глобальная переменная currentNode и на неё опираются хуки.
PS Все выше псевдокод
useRefмне интересно увидеть аналогичный код реализации хуков без использования встроенных хуков react-а. В данном случае надо сначала реализовать свой useRef)
Мне как раз и не нравится, что эти глобальные переменные скрыли и не понять, как к ним из хуков обращаться, если надо.
Я сейчас обсуждаю, как правильно реализовать более простую и удобную альтернативу useReducer — github.com/reactjs/rfcs/issues/185. Мне сказали, что вариант с useEffect + useRef не будет работать в concurrent mode, а useMemo будет. Почему так, мне пока не понятно. Знал бы детали реализации, может быть бы и понял.
как к ним из хуков обращаться, если надо.
Всё просто. Скрыли как раз потому, что не надо к ним обращаться. Такой код может не пережить без полного переписывания даже 1 мажорного релиза. Плюс это уже грубое нарушение здравого смысла. Имхо, если настолько сильно приспичило пойти против шерсти, лучше поменять React на что-то другое, что решит задачу лучше, чем пытаться вот так вот лезть в глобалки. Так то если вот 100% надо — вы можете добраться до FiberNode через domNode-ы. Но это адский костыль.
Т.е. на проектах это не надо, а вот в документации бы не помешало объяснение этой магии.
Вызывает сомнение другое — что рендеринг компонента-функции с кучей не JSX кода, быстрее функции render в классе в уже созданном компоненте)
Тут непонятно что быстрее, но судя по их доке создание инстанса перевешивает все остальные операции, а инстансы создаются часто, вы перешли на новую страницу, создай тьму компонентов, нажали на кнопки, одни удали, другие создай. И еще одна проблема у инстанса, они не могут его оптимизировать, а если им отдать функцию у которой есть if который в текущем энвароменте возвращается всегда true, то условно, они могут выкинуть часть кода.
Я бы с удовольствием посмотрел на читабельные реализации основных хуков.
Через примерно 2-3 видео, планирую сделать экспериментальное видео, в котором покажу как это работает из нутри. Там под каждым хуком несколько функций, условно есть useCallbackMount, useCallbackUpdate и другие
Интересно, как сделать так, чтобы внутренние механизмы react-а сохраняли prevState, связав его с компонентом, в котором он используется, чтобы полноценный аналог useCallback написать
Почти все хуки можно сделать через useState
:) И useRef
, и useCallback
, и useMemo
, и useEffect
(кроме финального деструктора). Но вот без useState
привязку к компоненту уже не обеспечить, т.к. React не даёт таких ручек. Разве что рендерить какой-нибудь domElement и цеплять всё к нему (это шутка, не делайте так).
И конечно, доверяйте реакту, если они сказали лучше создавать функцию на каждый рендер, так и делайте, ведь они заинтересованы только в улучшении перформанса вашего проекта.А в действительности, в чём преимущество каждый раз создавать функцию? Кроме отладки, какие проблемы с выносом функции в замыкание?
Внимание вопрос
В каком из вариантов написания компонента функция присваемая переменной someFunction создается реже?
У меня тоже вопрос: зачем все это? Для чего вам уменьшать кол-во присвоений функции?
Может вы хотите сказать о производительности? Ну так сделайте бенчмарки, можно будет увидеть реальное сравнение в скорости и сделать какие то выводы.
Примеры, которые вы приводите, оставляют желать лучшего. Вместо вот этого:
const Cars = ({ cars }) => {
const onCarClick = (car) => {
console.log(car.model);
}
return cars.map((car) => {
return (
<Car key={car.id} car={car} onCarClick={onCarClick} />
)
});
}
можно вообще переписать так:
const onCarClick = (car) => {
console.log(car.model);
}
const Cars = ({ cars }) => {
return cars.map((car) => {
return (
<Car key={car.id} car={car} onCarClick={onCarClick} />
)
});
}
Зачем класть функцию onCarClick
, внутрь Cars
, если ссылка на нее не зависит от props
? onCarClick — это чистая функция.
А ниже вы тоже приводите странный листинг кода:
const Car = ({ car, onCarClick }) => {
const onClick = () => onCarClick(car);
return (
<button onClick={onClick}>{car.model}</button>
)
}
Этот код можно переписать так:
const Car = ({ car, onCarClick }) => {
const onClick = useCallback(() => onCarClick(car), [car]);
return (
<button onClick={onClick}>{car.model}</button>
)
}
Здесь функция onClick
явно зависит от props
. Чтобы React не делал лишнего рендера нужно чтобы ссылка на функцию onClick
не менялась, при передачи одного и того же значения. А добиться этого как раз можно используя useCallback
.
Я переписал окончательный вариант на колбеках:
const Cars = ({ cars }) => {
const onCarClick = useCallback((car) => {
console.log(car.model);
}, []);
return cars.map((car) => {
return (
<Car key={car.id} car={car} onCarClick={onCarClick} />
)
});
}
const Car = ({ car, onCarClick }) => {
const onClick = useCallback(() => onCarClick(car), [car]);
return (
<button onClick={onClick}>{car.model}</button>
)
}
В этом случае нет никакой пользы оборачивать метод в useCallback, т.к. нам не важно, ссылка это на функцию с прошлого рендера или с текущего рендера, а useCallback как мы уже знаем не бесплатный.
В том то и дело что польза есть! Примеры, которые вы приводите, содержат фундаментальные ошибки.
Я использую VSCode с плагином ESLint на который установлено специальное дополнение по автоисправлению всех зависимостей хуков (link).
В том то и дело что польза есть! Примеры, которые вы приводите, содержат фундаментальные ошибки.
Автор просто немного переупростил пример. А вы до этого примера докопались :) Понятное дело что в реальной жизни будет зависимость от props.
Я использую VSCode с плагином ESLint на который установлено специальное дополнение по автоисправлению всех зависимостей хуков
Рекомендую отключить правило для useEffect (и уж точно не авто-исправлять его). Оно легко может сильно переломать вам поведение программы (например вызвать eternal loop загрузки какого-нибудь API метода). По мнению авторов хука в useEffect должны упоминаться все вещи, от которых он зависит. А это далеко не всегда так. Эффект совершенно не обязательно должен перезапускаться при изменении любой из зависимостей. Это ортогональные вещи.
Здесь функция onClick явно зависит от props. Чтобы React не делал лишнего рендера нужно чтобы ссылка на функцию onClick не менялась, при передачи одного и того же значения. А добиться этого как раз можно используя useCallback.
Из этих слов не очень понятно, как ссылка на функцию, каждый раз новая, которую мы передаем в button onClick приводит к «лишнему рендеру»
Вот то что было написано в вашем примере:
const onClick = () => onCarClick(car);
А вот, так как надо делать с точки зрения React:
const onClick = useCallback(() => onCarClick(car), [car]);
При первом рендеренге, в обоих случаях, будет создана ссылка на функцию onClick
.
При повторном рендеренге onClick
, созданная без useCallback
, будет содержать новую ссылку на анонимную функцию.
А вот при повторном рендеренге onClick
, созданная через useCallback
, будет содержать туже ссылку на анонимную функцию. В этом и есть магии функции useCallback
! Задача useCallback
состоит в том чтобы ссылка на функцию внутри не менялась при отсутствии изменений в deps
Если при повторном рендеренге реакт видит что ссылка на функцию не поменялась, то он не будет производите перерендер.
А вот, так как надо делать с точки зрения React:
const onClick = useCallback(() => onCarClick(car), [car]);
А кому надо? И зачем? Чего вы добились этим кодом?
Давайте разбираться.
- Вы избежали лишнего ререндера. Эмм… Нет. Не избежали. Сам факт что этот метод исполнился уже говорит о том, что компонент рендерится
- Ок, но вы избежали лишнего под-ререндера. Для дочерних компонент. В какой-то степени это правда. Но…
- Вы не избежали реконсиляции. Ваше vDOM древо всё так же будет проверено на предмет новых ссылок и значений. Просто в результате изменения не будут найдены
- Это нас приводит к тому, что единственная разница тут в позитивном ключе в коде с useCallback и без него заключается в том, что, в случае useCallback, React не перейдёт глубже — на render
<button/>
- Однако в чём заключается render этой
<button/>
? На самом деле вremoveEventListener
иaddEventListener
. Всё. Это практически нулевые затраты. - А что мы проиграли? Ну судите сами: мы сделали код сложнее. Это всегда недостаток. Сложный код сложнее читать, поддерживать, рефакторить. Код с usecallback хуком и рядом не стоял с
onClick={() => onClick(car)}
по простоте понимания и поддержки. - У нас больше точек где мы могли совершить ошибку. Мы могли некорректно указать список зависимостей к примеру. Тут может помочь линтер или hook.macro
- Мы могли это всё сделать за зря, т.к. одна из зависимостей может быть всегда новой
Если рассуждать в таком ключе то быстро приходишь к выводу:
- Если горят дедлайны — useCallback может подождать до рефакторинга. Чтобы от него был толк нужно очень постараться, ведь:
- Если вы не можете выше по древу обеспечить сохранность ссылок на callback-и и object-ы, которые передаются по props каналу, вам ваш useCallback ничего не даст. А обеспечить это может быть весьма не тривиальной идеей. Например children в 99 случаях из 100 ломает memo, т.к. почти никто не мемоизирует children-prop :)
- Если у вас в принципе всё мутабельное — до свиданье мемоизация
- Если речь идёт о листьях vDOM дерева, как в примере в статье с кнопкой (
<button/>
не компонент а тег), то нет особого смысла что-то мемоизировать. Вы если и выиграете что-то, то сущие копейки.
Но когда тогда имеет смысл использовать мемоизацию?
- У вас по проекту всё или почти всё иммутабельно
- У вас высокая культура кода и вы действительно гарантируете сохранность ссылок (не пересоздаёте callback-и object-ы без необходимости)
- У вас есть высокие требования к производительности
- У вас хорошо настроены линтеры и\или code review (в мемоизации легко отстрелить ногу)
- Вы съели собаку на всяких
weakMap
, умеете в считанные секунды писать сложные селекторы с многоуровневым кешем, глубоко разбираетесь в языке
И куда их в таком случае ставить:
- Во все крупные звенья. Всё что под собой имеет много-много подкомпонент
- В сложных хуках (это как-минимум упрощает дебаг)
- В core вещах, используемых по всему проекту
Единственное я бы добавил один нюанс по поводу следующих строк:
Однако в чём заключается render этой <button/>? На самом деле в removeEventListener и addEventListener. Всё. Это практически нулевые затраты.
Я изучал исходники хуков (про это тоже хочу написать статью и снять видео), и там useCallback включает в себя логику думаю не дешевле чем добавлять и удалять EventListener. Строится очердь хуков, запускает метод render hooks. Так же нужно сравнивать deps которые прислал через Object.is и другие сложности, так что думаю addEventListener будет если не дешевле, то как минимум столько же стоить
У меня сложилось впечатление, как будто вы иногда не используете useCallback
и видимо вас очень задевают те люди, которые делают иначе.
Ну не хотите вы использовать useCallback
— ну так и не используйте! Это ваше право, ваш выбор! Я не заставляю вас делать этого в каждом компоненте. React рекомендует использовать useCallback
но не заставляет. Дедлайны горят! Больше кода надо писать! Не видите в этом пользы! Ну так и не делайте! Я вас ругать не буду! Правда!
А кому надо?
Этот надо мне!
И зачем?
Чтобы перерендер работал быстрее!
Чего вы добились этим кодом?
Чтобы было легче переиспользовать код! Чаще всего, я сталкиваюсь с задачами, когда я не знаю что там происходить в компонентах ниже/выше по дереву (писал больше пол года назад, либо писал другой человек). А там может быть не только removeEventListener/addEventListener но другие вещи. Моя задача написать такой код, который бы делал минимальное число лишних перерендеров. Поэтому я использую useCallback
.
У меня сложилось впечатление, как будто вы иногда не используете useCallback и видимо вас очень задевают те люди, которые делают иначе.
У нас просто очень большой опыт работы с мемоизацией и React. Мы на ней собаку съели. И я вижу типовые ошибки за километр. Я думаю с вашей любовью к useCallback — вам бы у нас понравилось. Правда вам пришлось бы переосмыслить некоторые догмы, которое засели в вашей голове.
А теперь давайте выдохнем и по пунктам:
Чтобы было легче переиспользовать код!
Причём тут переиспользование кода? Да, хуки в целом про это. Но в вашем примере этого нет.
когда я не знаю что там происходить в компонентах ниже/выше по дереву
Это никак не относится к обсуждаемой задаче. У вас ведь нет древа ниже. Это уже лист (звено без подзвеньев). Я думал, я сделал на этом очевидный акцент в своём сообщении выше. Пожалуйста перечитайте его внимательнее.
А там может быть не только removeEventListener/addEventListener но другие вещи
Это тег <button/>
— с точки зрения React ничего там ниже быть не может. Всё. Точка. Конечная остановка.
Моя задача написать такой код, который бы делал минимальное число лишних перерендеров.
Странная у вас задача. Полагаю что количество ререндеров вас интересует ввиду интереса к производительности? Если да, то пусть вас лучше интересует сама производительность, а не всего лишь один из её образующих критериев. Смотрите шире. Количество ререндеров далеко не всегда являются единственным и вообще важным фактором. Тема производительности куда сложнее чем useCallback.
Но важнее тут другое. Одна из самых важных задач для программиста это умение мыслить в рамках продукта и его задач (во всяком случае это так если вы планируете быть TeamLead-ом или архитектором). И в таком случае вы должны уметь расставлять приоритеты. Уметь оценивать их в деньгах и в получаемой пользе. После такого количество лишних ререндеров отходит на второй план (иначе бы вы писали на vanila, она гарантировано быстрее). А код вида:
const onClick = useCallback((car: Car) => onCarClick(car), [onCarClick, car]);
return <a {...{ onClick }}/>
перестаёт проходить код ревью т.к. имеет низкий performance, высокую сложность, повышенную багоопасность, и отдалён от бизнес-логики.
В то время как точно такой же код, но при <CarItem {...{ car, onClick }}/>
, где <CarItem/>
это тяжёлый вложенный компонент, был бы очень даже уместен. Так как тут useCallback заиграет новыми красками. Это и performance (будет особенно актуально если большие списки этих <CarItem/>
). Это и простота дебага (куда проще дебажить когда лишняя работа не выполняется). Это и дополнительные фишки (теперь onClick
можно при желании запихать в useEffect
).
P.S. В целом это похвально, что вы интересуетесь производительностью, и имеете представление о пользе мемоизации. Но всё же я рекомендую вам углубиться в эту тему поглубже. Она не так проста, как, судя по всему, вам кажется. Она переполнена компромиссами, оценками и подходами. Всё далеко не так просто.
У нас просто очень большой опыт работы с мемоизацией и React. Мы на ней собаку съели.
Интересно у вас… Прямо свою предыдущую работу вспомнил (там вебсокет-пулеметчик, тоже приходилось упарываться в мемо и вообще включать голову).
Поправьте, если я вас не правильно понял, но вы предлагаете следующее: если "ниже" DOM нода, то не используйте useCallback. Если же там компонент, то лучше обернуть.
Примеры автора статьи слишком упрощены, как и во многих других статьях. От этого и масса вопросов.
Например пример со списками — лучше для перформанса наверное вообще не прокидывать в каждый элемент колбек. А ловить клик на родителе и уже там определять выбранный элемент.
Кроме того, не понятны примеры, когда колбек принимает аргумент и что-то с ним делает. В таком случае в зависимостях у него ничего нет и тут действительно стоит задуматься, чтобы выкинуть такой колбек за пределы рендер-функции.
если "ниже" DOM нода, то не используйте useCallback
Ага. Особого смысла нет. Производительность останется на примерно том же уровне, а вот качество кода заметно снизится. Но в редких случаях может быть актуальным и тут useCallback применить. Скажем когда на него завязано несколько эффектов очень хитроумным способом. Но это уже большая специфика.
Если же там компонент, то лучше обернуть
Не факт, что лучше, но вполне может быть. Зависит от конкретных компонент, глубины древа и пр… К примеру если вы передаёте в дочерний компонент children, то почти 100% внутренний Memo идёт лесом. Ну просто потому что children всегда новый. Есть ли тогда смысл лишний раз приседать с useCallback? В общем тут нужно вникать в каждый отдельно взятый случай и в то как у вас потоки данных по приложению ходят.
А ловить клик на родителе и уже там определять выбранный элемент.
Там много решений. Один из популярных — передавать от родителя внутрь один callback для всех children. Просто в сигнатуре этого метода должен быть ещё и ID, чтобы действие можно было произвести над нужной сущностью.
А вот готовить каждому элементу его собственный callback это обычно геморрой.
Но в любом случае тут стоит исходить из здравого мысла.
чтобы выкинуть такой колбек за пределы рендер-функции
Почему бы и нет. Но стоит уточнить что это очень редкий случай. Ибо получается что если callback делает хоть что-то, то он оперирует глобальными переменными (ведь ни к чему ещё у него просто нет доступа).
Но да — если зависимостей нет, то можно смело выносить метод из компонента наружу.
Спасибо за подробный ответ.
Хотел ещё добавить. Вот есть статья. И конкретно раздел про мемоизацию. Ведь по сути, useCallback — это ведь useMemo.
Так вот, с одной стороны мысль о мемоизации всего — выглядит крайне страной. С другой стороны, автор утверждает что нет никакой заметной разницы при "лишней" мемоизации. Так не кажется ли вам, что вся эта тема с useCallback не стоит даже обсуждения? Особенно без каких то замеров.
Я, признаться, не очень понимаю всю эту шумиху вокруг хуков. Они мне с самого начала показались порочной практикой. Они слишком много делают "магически". Да, в какой-то мере удобно, но читать это всё потом такая боль
С другой стороны, автор утверждает что нет никакой заметной разницы при «лишней» мемоизации
Я утверждал, что в конкретной ситуации пользователь не заметит разницы, но всегда найдется 10 других ситуаций, в которых хочется выжать из инструмента максимум. И мы должны знать как это сделать.
Да и по поводу читабельности, догружать систему дополнительными хуками, это ухудшает читабельность кода.
Они мне с самого начала показались порочной практикой. Они слишком много делают «магически».
Тут дело субъективное, я, например, с первого дня втянулся в новые возможности хуков. Наконец решил проблемы с переиспользованием логики, наконец перестал биндить каждый метод во имя правильного контекста. Да и сама идея использования бесконечного замыкания для хуков максимально соответствует духу JavaScript, а не попытки имитировать классы из других языков, что вечно раздражало.
Поэтому сколько людей, столько и мнений :)
Я утверждал, что в конкретной ситуации пользователь не заметит разницы
Полагаю, что @epovidlov имел ввиду Anujit Nene, а не вас. Там по ссылке очень большая статья (почти мини-книга).
1. При использовании обычного метода (немемоизированного) react-dom будет вызывать add/removeEventListener на DOM-элементе, в то время как при использовании мемоизированной версии этого происходить не будет, верно? Что-то мне подсказывает, что операция с DOM зачастую будет более дорогостоящей, нежели сравнения зависимостей и создание лишней функции. Есть соображения, как это замерить или уже готовые метрики?
2. У нас на проекте очень часто мемоизированные функции вызываются в эффектах. Использование useCallback позволяет, по моему скромному мнению, писать эти самые эффекты сходу, не выстреливая себе в ногу тем, что какая-то из вызываемых функций внезапно окажется немемоизированной и будет триггерить эффект, и так же безопасно включать эти самые функции в список зависимостей эффектов дочерних компонентов. То есть, делаешь все функции по дефолту мемоизированными и больше не вспоминаешь о возможных сайд-эффектах. Что вы думаете по этому поводу?
то операция с DOM зачастую будет более дорогостоящей
Дорогие DOM-операции это те DOM-операции которые влияют на layout. Т.е. двигают\изменяют размеры визуальных блоков. Дорогие они по причине чрезмерной сложности и взаимосвязанности. Самый яркий пример — ячейка таблицы. Размер одной единственной ячейки таблицы может повлиять на размеры вообще всех других ячеек таблицы. И таблицы целиком. И страницы целиком. А это очень много вычислений. А вы всего-то, скажем, точку после слова добавили.
А addEventListener в идеале сводится к 1 записи в hashmap в недрах v8 с O(1). На практике может и сложнее устроен. Я не копал.
То есть, делаешь все функции по дефолту мемоизированными и больше не вспоминаешь о возможных сайд-эффектах.
Примерно так мы и пишем код. Но это не main-stream, т.к. это сложно. Во всяком случае в и без того сложных проектах — это сильно усложняет код. Мемоизация и иммутабельность вообще не сильные стороны JS. Где-нибудь в Haskell, с этим наверное всё хорошо… Но не у нас.
Вот есть статья
Всю статью не читал. Только приведённый вами отрывок. Что я могу сказать. Только ППКС. Он в точности выдал в нём то, что я думал. Я тоже не согласен с Деном Абрамовым по этому поводу. Я тоже думаю про net gain. Ну и примерно в таком ключе мы и пишем наши приложения. По-умолчанию используем memo, а там где это очевидно создаёт проблемы за зря, или точно не даст никаких преимуществ — там не используем.
Я полагаю, что если взять хорошо написанное immutable приложение, то сделав memo везде по-умолчанию, вы в худшем случаем проиграете 1-5% производительности. А, возможно, даже выиграете в производительности. Однако если в принципе изначально писать с уклоном в мемоизацию, то выгода очевидна. Но это создаёт сложности для всех членов команды, особенно для наименее опытных, т.к. мемоизация "по взрослому" это ни разу не детский сад. Когда начинаешь писать вложенные weakMapы или профилировать shallowComparison цепочки из-за нормализации данных (чтобы не вылезти из 60 кадров в секунду), то понимаешь, что у всего есть своя цена и очень легко пере-овер-инжинирить.
Они слишком много делают "магически".
Да ну. Просто немного нестандартно. Немного другая парадигма. Просто практика показывает (во всяком случае наша), что хуки решают типичные бизнес-задачи намного лучше, чем ООП. Возможно я слишком плох в ООП, но мне действительно, спустя 1.5 лет с хуками, куда проще с ними решать сложные задачи.
но читать это всё потом такая боль
Я полагаю, что читать код с хуками боль только тогда, когда вы или их плохо понимаете (это лечится), или код написан плохо (это тоже лечится практикой и clean code подходом). В идеале всё наоборот. Хуки позволяют вам совершенно элементарным образом делить сложные элементы на множество простых. Очень удобный инструмент декомпозиции. В случае классов очень непросто реализовать сложную задачу не создав Гордиева узла. Я вот, если честно, просто не умею. Мне надо раз 10 отрефакторить код на классах, чтобы от него перестало нести тухлыми яйцами.
Просто практика показывает (во всяком случае наша), что хуки решают типичные бизнес-задачи намного лучше, чем ООП.Я некоторое время работал в геймдеве. Там бизнес задачи вполне хорошо решаются с помощью ООП. Там используется композиция, похожая на ту, которая в реакте на классах. Но, там не пишут custom логику в «компонентах», а выносят ее в более маленькие «строительные блоки» из набора которых создается готовый «компонент».
Вот пример иерархии объектов сцены с конкретным выделенным игровым объектом (игровой объект — аналог компонента в react). К выделенному объекту добавлены различные скрипты с логикой. Также он состоит из других объектов, каждый из которых тоже может содержать любые скрипты и дочерние объекты.
В компонентах на классах до этого не дошли.
Во-первых, в реакте за мельчайшие «строительные блоки» приняли компоненты. Не сделали, чтобы компонент мог состоять из других частей. Это не позволило нормально повторно использовать код между компонентами.
Во-вторых, объединили логику компонента и представление. Тем самым не дав возможность использовать логику без привязки к конкретному представлению и наоборот, не дав использовать одну render функцию в компонентах с разной логикой.
Один из альтернативных вариантов, как можно было бы сделать react компоненты в ООП стиле с повторным использованием логики:
const MyComponent = {
name: "MyComponent",
logicalBlocks: [
{ logicalBlock: logicalBlockA },
{ logicalBlock: logicalBlockB }
/* Каждый logicalBlock получает props, имеет свой стейт, а также life cycle методы компонента (или можно сделать аналоги хуков useState, useEffect ... вместо life cycle методов);
возвращает данные и функции (подобно custom hooks) которые используются в render функции компонента. */
],
render: myRenderFunction
};
const myRenderFunction =
({dataFromLogicalBlockA, dataFromLogicalBlockB, ...props}) =>
(<div> ... </div>);
Т.е. для логики используется композиция, а не наследование. Логика компонентов не наследуются, а собирается из разных составляющих. Один и тот же логический блок можно использовать в любых компонентах. Функцию render — тоже.
У меня есть своя рабочая реализация поверх реакт классов. Надеюсь, найду время написать статью про нее.
{ logicalBlock: logicalBlockA },
Это было. Называлось mixins. У каждого миксина был свой набор методов жизненного цикла. Почти сразу же их перевели в deprecated.
А в моем примере не миксины. Похоже, на первый взгляд, но это разные вещи. В отличие от миксин, здесь используется композиция. Все изолировано друг от друга. Похоже на паттерн «стратегия» (точнее, на массив «стратегий»), если знакомы.
Миксины же ближе к наследованию (к множественному наследованию). И в наследовании и в миксинах в конечном итоге получается объект с кучей полей и методов. Поэтому эти подходы не работают в сложных случаях.
Всё так, да. Тут важно отметить что авторы React, судя по всему, что я вижу, тяготеют больше к ФП и процедурному программированию. А ООП направление им совершенно неинтересно.
Например:
codesandbox.io/s/relaxed-driscoll-r6wwm?file=/src/App.tsx
У меня в использовании useCallback иногда всплывал такой кейс: функция зависит от пропа, который часто меняется, из-за этого useCallback часто отдает новую функцию, и имеем лишние рендеры компонента, который получает на вход этот колбэк, но напрямую не зависит от того меняющегося пропа. Написал свой довольно простой аналог useCallback с блэкджеком и без депенденсов, который всегда возвращает одну и ту же функцию:
export function usePersistentCallback(func) {
const ref = useRef(null);
if (!ref.current) {
ref.current = {
callee: func,
caller: function() {
return ref.current.callee.apply(this, arguments);
}
};
}
ref.current.callee = func;
return ref.current.caller;
}
Как видим, это аналогично старому доброму подходу с классами, в самом начале этой статьи.
Да ничего особенного. Последний пример — у компонента есть свой memo(Footer), в который уехали кнопки "да/нет/наверно". Соответственно в пропсах футера есть колбэк onUserSelect, в который передается функция, работающая c некоторыми объектами, но сами эти объекты не передаются, ибо футеру они без надобности, он только сообщает о выборе пользователя. Таким образом, футеру нет нужды перерисовываться, если объекты поменялись.
Я полагаю, этот кейс не выглядит каким-то совсем уж странным.
После нажатия на кнопку, разумеется, многое меняется. Мой поинт немного о другом. Поясню кодом (упрощенно):
...
const handleUserSelect = useCallback(answer => {
// здесь некий код, использующий props.someProp
}, [props.someProp]);
return (
...
<Footer onUserSelect={handleUserSelect} loading={loading} />
);
someProp меняется часто, ещё ДО нажатия на кнопку. Footer не зависит от someProp, но в приведенном коде всё равно будет вынужден зря реконсилиться, потому что изменение someProp, видите ли, привело к замене handleUserSelect. Мой велосипед как раз для такого кейса.
По нажатию на кнопку вызывается метод, который может вызывать setState. Т.е. реактивное поведение остаётся каким и было. Реальной необходимости ещё и саму ссылку на метод при этом менять обычно нет. Достаточно чтобы метод не обращался к неактуальным данным.
У нас много кода который должен быть доступен по ссылке (не обязательно 1 метод), не меняться с течением жизни приложения, но при этом иметь доступ к актуальным значениям переменных. Пришли примерно к такой схеме:
const storage = useRefStorage({ var1, var2, var3, whatever });
const myMethod = useCallback(() => { ... storage.whatever ... }, []);
Где useRefStorage
это по сути обёртка вокруг useRef
, которая при каждом рендере делает Object.assign(ref.current, values /* var1, var2... */)
. Получается этакий аналог class-components поведения.
Тут главное разделять логически: то от чего зависит получаемый рендер (скажем итоговый HTML), должно оставаться реактивным (useState). А то от чего зависит поведение (всевозможно event-handler-ы) может быть и не реактивным. Достаточно только обеспечить однозначный доступ к самым актуальным данным. А для этого есть useRef
.
Работает хорошо. Но более много словно чем хотелось бы. Ну и требует некоторого времени на то чтобы въехать как это работает. Однако используем такой механизм только там где это правда актуально.
Делал похожую штуку для viewModel к реактовским компонентам, тоже на useRef (модель была мобиксовым стором со своими экшенами, и жила, пока жил компонент). То есть специальный хук создавал и поддерживал постоянный экземпляр, да ещё и вызывал у него метод clean при наличии такового, если модели нужна очистка, допустим таймер прибить. Как бы двустороннее сотрудничество компонента и модели — модель рулит логику и уведомляет вьюху, а компонент через хук предоставляет жизненный цикл.
const { current: persistentCallback } = useRef(func)
Кажется, что приведенный выше фрагмент кода повторяет вашу логику. Вы инкапсулировали ее в хук для большей декларативности?
UPD: Прошу прощения, понял, что это разные вещи.
Документация большинства опенсорсных JS-проектов — отвратительна. Документация Реакта — тоже отвратительна, и это вдвойне плохо, потому что очевидно, создатели других JS-библиотек вдохновляются отвратительностью реактовской документации, и думают, что им так тоже можно. Самоподдерживающаяся система отвратительных доков с положительной обратной связью.
Приведу пример. Чтобы далеко не ходить, из документации по сабжу (на английском там то же самое):
Возвращает мемоизированный колбэк.
Передайте встроенный колбэк и массив зависимостей. Хук useCallback вернёт мемоизированную версию колбэка, который изменяется только, если изменяются значения одной из зависимостей. Это полезно при передаче колбэков оптимизированным дочерним компонентам, которые полагаются на равенство ссылок для предотвращения ненужных рендеров (например, shouldComponentUpdate).
useCallback(fn, deps) — это эквивалент useMemo(() => fn, deps).
Как же это всё плохо.
Что мемоизированный? Куда мемоизированный? (Мемоизированный во внутреннем диспатчере Реакта, который будет помнить про useCallback и другие ваши хуки до тех пор, пока… ну, пока ему не надоест, короче, лайфтаймы компонентов в Реакте это отдельная песня.) В каких конкретно случаях происходит обновление переданной функции? (При рендере, и только при нём — магии, которая обновляла бы функцию при любом обновлении переменных из второго аргумента useCallback в принципе, нарушая законы JS, там нет, магии хватило только на JSX.) Я могу использовать функцию из useCallback как, гм, коллбэк в third-party код и ожидать, что этот коллбэк будет знать про изменения переменных из второго аргумента useCallback, которые произошли после передачи коллбека? (Нет, потому что предыдущий ответ и потому что замыкания в JS так не работают.) Я могу засунуть useCallback вот так: useRef(_.throttle(useCallback(() => {}, [A, B]), 1000)) и ожидать, что моя затротленная функция будет знать об изменениях A и B? (Нет, потому что хотя _.throttle будет вызывать новые замыкания с новыми значениями A и B, useRef это всё похерит.)
Но вместо ответов на все эти вопросы там язвительная ссылочка на статью «мемоизация» из Википедии, а эти ответы добываются экспериментально. Почему тогда JS-лиды так сильно удивляются, что их подопечные документацию не читают, если она настолько рахитичная?
Хм, ну ладно, там же ещё ссылка на описание похожего хука useMemo есть, может она что-то прояснит?
Вы можете использовать useMemo как оптимизацию производительности, а не как семантическую гарантию. В будущем React может решить «забыть» некоторые ранее мемоизированные значения и пересчитать их при следующем рендере, например, чтобы освободить память для компонентов вне области видимости экрана.
Ааааа! *звуки боли фронтэндной разработки*
Все ли вы знаете о useCallback