Как стать автором
Обновить

Комментарии 70

Что, правда есть люди, которые так делают?
Жду статей "Если слишком долго держать в руках раскалённую докрасна кочергу, в конце концов обожжёшься", "Если поглубже полоснуть по пальцу ножом, из пальца обычно идёт кровь" и "Если разом осушить пузырёк с пометкой «Яд!», рано или поздно почти наверняка почувствуешь недомогание".

К сожалению да, много людей, кто совершал такую ошибку даже среди синьор разработчиков.

Для меня странна сама мысль ждать в данном случае магии от useCallback.


Наверное, дело в том, что я не JavaScript-разработчик (хотя приходится) и не знаю React. Поэтому для меня useCallback – это просто функция какая-то, чтобы передать ей аргумент – надо его вычислить (т.е. создать передаваемую функцию… если вначале аргумент присвоить какой-то переменной – всё становится совсем уж прозрачно).

Все верно, мы просто разрабатывая на классах привыкли к определенного рода магии реакта. Что есть какой то метод componentDidUpdate, что есть какой то setState, и по факту это все абстракции в виде черных ящиков. И когда ты привык к этой идее, тут тебе дали еще один ящик, который по синтаксису обманчив. И ты думаешь по привычке, ну реакт там сам все решит.

Фреймворкм должны быть просты, понятны и интуитивны.


Разработчики Реакт каждый раз создают новые хаки, чтоб этот шаткий домик не развалился — на это больно смотреть.
Еще как эти хуки/хаки покрывать юнит тестами?

Для тестирования хуков есть вроде как уже готовые решения, на подобии этого

React core разработчики говорили по их статистике разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить, а когда лучше создать метод как arrow function. Для людей, кто это уже освоил давно, кажется очень простой задачей работать с классами, но видимо это не так.
Я думаю этот текст в документации появился вместе с выходом хуков. Поэтому интересно, где они эту статистику взяли. Ну и интересно, с чем был опыт в прошлом у этих новичков, с функциональным программированием или с ООП.
разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить,
ну и я сомневаюсь, что такие новички на хорошем уровне понимают, как использовать хуки. Скорее всего, делают кучу ошибок, и не замечают их.
Я конечно не знаю точного ответа, но если предполагать, то возможно у них есть условно лаба и есть фидбек о том сколько сложностей вызывает освоение функций и сколько сложностей после этого вызывает освоение классов и работа с контекстом. А сейчас на хуках осваивать контекст в принципе не особо то и важно, да и сами классы нет смысла изучать. Поэтому можно сделать вывод, чтобы теперь начать писать на React, вам нужно меньше знать о js чем раньше.
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);
  };

Я новичок в вебе (но не в программировании) и хуки воспринимаю крайне негативно, а вот классы и декораторы в Nest — няшка, говорят, ангулар, похож на нест, значит нам туда дорога.
Вывод с Angular неправильный или за что минус? Непонятно же.

Angular не пользуется популярностью. Кто-то из вредности засадил.

Основная причина в том что в одном мажорном релизе полностью сломали обратную совместимость. Многие проекты не смогли переехать и застряли на старой версии, многие разработчики обиделись и завязали с ангуляром. С тех бытует небезосновательное мнение что все проекты на ангуляр это легаси.

Спасибо, значит я все-таки правильно выбрал React
Время покажет)
Из популярного еще Vue есть.
НЛО прилетело и опубликовало эту надпись здесь
на 100% в тему))
const Test = () => {
  const someFunction = useCallback((title) => {
    console.log(title);
  }, [title])

  return (
    <button onClick={someFunction}>
      click me!
    </button>
  )
}

В этом примере не должно быть либо параметра title, либо переменной title в списке зависимостей. Судя по всему, параметр лишний.

да, вы правы, редактировал несколько раз и ошибся, сейчас исправлю
Спасибо!

Но в компонент же он должен передаваться.
const Test = ({ 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-ы. Но это адский костыль.

Я прекрасно понимаю, что это не надо (хотя может кому-то и надо в крайне редких случаях. Все ситуации никто не способен предусмотреть). Но вот когда только знакомишься с хуками, воспринимаешь их как магию, ибо не понятно, как они устроены (где сохраняется состояние useState? откуда функция useState знает, состояние какого компонента она хранит, ведь ссылка на него не передается?) Примеры хуков с использованием других хуков на эти вопросы не дают ответов. Это если потом решить глубже поизучать, то столкнешься с React Fiber и связанным и тогда станет проясняться, как работают хуки.
Т.е. на проектах это не надо, а вот в документации бы не помешало объяснение этой магии.

Я так понимаю, не хотят усложнять документацию. А любознательные сами нагуглят.

Вызывает сомнение другое — что рендеринг компонента-функции с кучей не JSX кода, быстрее функции render в классе в уже созданном компоненте)

Тут непонятно что быстрее, но судя по их доке создание инстанса перевешивает все остальные операции, а инстансы создаются часто, вы перешли на новую страницу, создай тьму компонентов, нажали на кнопки, одни удали, другие создай. И еще одна проблема у инстанса, они не могут его оптимизировать, а если им отдать функцию у которой есть if который в текущем энвароменте возвращается всегда true, то условно, они могут выкинуть часть кода.

Я бы с удовольствием посмотрел на читабельные реализации основных хуков.

Через примерно 2-3 видео, планирую сделать экспериментальное видео, в котором покажу как это работает из нутри. Там под каждым хуком несколько функций, условно есть useCallbackMount, useCallbackUpdate и другие
Интересно, как сделать так, чтобы внутренние механизмы react-а сохраняли prevState, связав его с компонентом, в котором он используется, чтобы полноценный аналог useCallback написать

Почти все хуки можно сделать через useState :) И useRef, и useCallback, и useMemo, и useEffect (кроме финального деструктора). Но вот без useState привязку к компоненту уже не обеспечить, т.к. React не даёт таких ручек. Разве что рендерить какой-нибудь domElement и цеплять всё к нему (это шутка, не делайте так).

И конечно, доверяйте реакту, если они сказали лучше создавать функцию на каждый рендер, так и делайте, ведь они заинтересованы только в улучшении перформанса вашего проекта.
А в действительности, в чём преимущество каждый раз создавать функцию? Кроме отладки, какие проблемы с выносом функции в замыкание?
какие проблемы с выносом функции в замыкание?

Что вы подразумеваете под этой фрахой? приведите пример

Потому что это все пытается выглядеть декларативно. А в декларативно нет последовательности вызова, оно просто есть. Посмотрите любый фп язык вроде haskell.

Внимание вопрос
В каком из вариантов написания компонента функция присваемая переменной 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). К выделенному объекту добавлены различные скрипты с логикой. Также он состоит из других объектов, каждый из которых тоже может содержать любые скрипты и дочерние объекты.image

В компонентах на классах до этого не дошли.
Во-первых, в реакте за мельчайшие «строительные блоки» приняли компоненты. Не сделали, чтобы компонент мог состоять из других частей. Это не позволило нормально повторно использовать код между компонентами.
Во-вторых, объединили логику компонента и представление. Тем самым не дав возможность использовать логику без привязки к конкретному представлению и наоборот, не дав использовать одну 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.

Неудивительно. Там использовалось смешивание, но не композиция. В итоге получался god object с кучей методов и полей. Миксины смешивались друг с другом и с кодом компонента, из-за чего код получался запутанным и перекрывались методы и поля с одинаковыми именами.

А в моем примере не миксины. Похоже, на первый взгляд, но это разные вещи. В отличие от миксин, здесь используется композиция. Все изолировано друг от друга. Похоже на паттерн «стратегия» (точнее, на массив «стратегий»), если знакомы.
Миксины же ближе к наследованию (к множественному наследованию). И в наследовании и в миксинах в конечном итоге получается объект с кучей полей и методов. Поэтому эти подходы не работают в сложных случаях.

Всё так, да. Тут важно отметить что авторы React, судя по всему, что я вижу, тяготеют больше к ФП и процедурному программированию. А ООП направление им совершенно неинтересно.

Ну мало ли что там думают авторы, это как был чисто view слой, так и остался. Стейт менеджмент там как был убогий, так и остался. Поэтому ничто не мешает использовать React только как view слой, а управление состоянием компонентов и приложения в целом предоставить нормальным инструментам.

Например:
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 некоторыми объектами, но сами эти объекты не передаются, ибо футеру они без надобности, он только сообщает о выборе пользователя. Таким образом, футеру нет нужды перерисовываться, если объекты поменялись.
Я полагаю, этот кейс не выглядит каким-то совсем уж странным.

А после нажатия на кнопку, вы не меняете вообще ничего в футере? например, состояния isLoading для кнопки, пока выполняется обработка onClick, или же состояние активной выбранной кнопки? Я к тому, чтобы интерфейс был отзывчивым, чаще всего все равно нужно после взаимодействия с кнопкой рендериться, чтобы отреагировать визуально на нажатие.

После нажатия на кнопку, разумеется, многое меняется. Мой поинт немного о другом. Поясню кодом (упрощенно):


...
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 при наличии такового, если модели нужна очистка, допустим таймер прибить. Как бы двустороннее сотрудничество компонента и модели — модель рулит логику и уведомляет вьюху, а компонент через хук предоставляет жизненный цикл.

так вроде же mobx-react предоставляет хук useLocalObservable для таких целей
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 может решить «забыть» некоторые ранее мемоизированные значения и пересчитать их при следующем рендере, например, чтобы освободить память для компонентов вне области видимости экрана.

Ааааа! *звуки боли фронтэндной разработки*
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории