Все ли вы знаете о useCallback

    Привет, Хабр!

    Начиная с версии ReactJS 16.8 в наш обиход вошли хуки.  Этот функционал вызвал много споров, и на это есть свои причины. В данной статье мы рассмотрим одно из самых популярных заблуждений использования хуков и заодно разберемся стоит ли писать компоненты на классах (данная статья является расшифровкой видео).

    Два пути

    Как вы знаете в реакте есть 2 вариант написания компонента, с помощью классов и с помощью функций. И каждый вариант по своему взаимодействует с методами. Давайте рассмотрим оба варианта:

    Метод в классе

    Первый вариант, это использовать классы:

    class Test extends Component {
      onClick = () => {
        console.log('onClick');
      }
    
      render() {
        return (
          <button onClick={this.onClick}>
            test
          </button>
        )
      }
    }

    В данном варианте мы добавили метод onClick классу Test и при создании инстанса класса, этот метод создается 1 раз и в рендере мы уже используем ссылку на этот метод onClick={this.onClick}, таким образом при каждом рендере мы обращаемся всегда к одной и той же ссылке и не пересоздаем метод класса. Эта конструкция всем, кто давно в профессии, привычна и понятна даже если вы недавно пришли в React с другого языка программирования.

    Метод в функции

    Второй способ создания компонента является использование функции:

    const Test = () => {
      const onClick = () => {
        console.log('onClick');
      }
      
      return (
        <button onClick={onClick}>
          test
        </button>
      )
    }

    В таком подходе, чтобы создать обработчик onClick, мы описываем тело функции прямо внутри render, потому что все тело функции и есть render, другого варианта в принципе не существует, если вы хотите использовать props.

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

    Классы лучше чем функции?

    Чтобы разобраться с этим вопросом я полез в React документацию в секцию вопросы и ответы и нашел там следующий вопрос:

    Судя по документации создание инстанса класса для реакта настолько дорогостоящая операция, что создавать функцию на каждый рендер на порядок дешевле. Да и тот факт, что дерево становится глубже при использовании компонента высшего порядка connect от redux или бесконечных observer от mobX совсем не радует.

    Кажется есть один "вариантик" сэкономить

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

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

    Викторина!

    Сейчас мы рассмотрим 2 примера и Вы попытаетесь ответить кто круче!

    В одном углу ринга находится уже изученный нами ранее вариант написания обработчика события someFunction:

    const Test = ({ title }) => {
      const someFunction = () => {
        console.log(title);
      }
      
      return (
        <button onClick={someFunction}>
          click me!
        </button>
      )
    }

    В другом углу ринга находится точно такой же компонент, но уже решили завернуть функцию в useCallback.

    const Test = () => {
      const someFunction = useCallback(() => {
        console.log(title);
      }, [title])
      
      return (
        <button onClick={someFunction}>
          click me!
        </button>
      )
    }

    Для пользователя ничего не изменилось, console.log(title), точно так же вызывается при нажатии на кнопку.

    Внимание вопрос

    В каком из вариантов написания компонента функция присваемая переменной someFunction создается реже?

     

    Даем минутку подумать...

     

     

     

     

    Аккуратно ответ!

     

     

    Ответ

    И правильный ответ ни в каком! Да именно, никакой оптимизации useCallback нам не дал, функция создается ровно столько же раз, как и до оптимизации. Более того, мы наоборот ухудшили перфоманс нашего компонента.

    Разбираем ответ

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

    Естественно мы получим результат function. По синтаксису это было очевидно. Чтобы понять как работает черный ящик, давайте сами напишем имплементацию useCallback.

    Пишем свой useCallback

    useCallback - это функция, которая принимает 2 параметра, callback и deps.

    function useCallback (callback, deps) {
    
    }

    Далее нам надо хранить где-то этот callback и deps, чтобы иметь возможность при очередном вызове вернуть ту же самую функцию callback.

    const prevState = {
      callback: null,
      deps: null,
    }
    
    function useCallback(callback, deps) {
    
    }

    Теперь рассмотрим разные случаи: если deps не существует либо в prevState, либо в новых данных, тогда нужно сохранить текущие параметры и вернуть callback.

    const prevState = {
      callback: null,
      deps: null,
    }
    
    function useCallback(callback, deps) {
      if (!prevState.deps || !deps) {
        prevState.callback = callback;
        prevState.deps = deps;
        
        return callback;
      }
    
    }

    Если же deps существуют. Тогда сравниваем какой-либо функцией массивы и если они совпадают, тогда возвращаем мемоизированную функцию.

    const prevState = {
      callback: null,
      deps: null,
    }
    
    function useCallback(callback, deps) {
      if (!prevState.deps || !deps) {
        prevState.callback = callback;
        prevState.deps = deps;
        
        return callback;
      }
      
      if (shallowEqual(deps, prevState.deps)) {
        return prevState.callback;
      }
      
    }

    Ну и если deps не совпадают, тогда снова сохраняем параметры и возвращаем текущий callback.

    const prevState = {
      callback: null,
      deps: null,
    }
    
    function useCallback(callback, deps) {
      if (!prevState.deps || !deps) {
        prevState.callback = callback;
        prevState.deps = deps;
      
        return callback;
      }
      
      if (shallowEqual(deps, prevState.deps)) {
        return prevState.callback;
      }
      
      prevState.callback = callback;
      prevState.deps = deps;
      
      return callback;
    }

    Вроде бы мы покрыли все кейсы

    Какие выводы из этого мы можем сделать?

    Функция useCallback как и любая другая функция вызывается на каждый рендер и в качестве параметра callback каждый рендер приходит новая функция и новый массив зависимостей. Которые мы либо выбрасываем, если зависимости до и после совпадают, либо сохраняем в хранилище, для будущего использования.

    Давайте теперь посмотрим на эту функцию со стороны компонента. Мы знаем, что useCallback это просто функция и мы можем извлечь передаваемые параметры в отдельные переменные.

    const Test = ({ title }) => {
      const callback = () => {
        console.log(title);
      }
      
      const deps = [title];
      
      const someFunction = useCallback(callback, deps);
      
      return (
        <button onClick={someFunction}>
          click me!
        </button>
      )
    }

    Тут становится совсем очевидно, что мы на каждый рендер создаем не то что функцию, а еще и массив с зависимостями, а потом еще и прокручиваем все это через useCallback.

    Если мы просто закомментируем создание зависимостей и вызов useCallback и передадим параметр callback напрямую в onClick, тогда кажется перфоманс компонента должен улучшиться, ведь мы убрали посредника, который не нес никакой пользы для перфоманса.

    const Test = ({ title }) => {
      const callback = () => {
        console.log(title);
      }
      
      // const deps = [title];
      
      // const someFunction = useCallback(callback, deps);
      
      return (
        <button onClick={callback}>
          click me!
        </button>
      )
    }

    По итогу мы вернулись к начальной ситуации. Когда просто создавали функцию на каждый рендер.

    Получается, в данном случае использовать хук useCallback - это не значит улучшить перфоманс, а скорее  совсем наоборот, ухудшить перфоманс

    А для чего тогда нужен useCallback ?

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

    Получается основная идея не в улучшении перформанса в конкретном компоненте, а скорее использование useCallback выгодно только в случае передачи функции как props. Давайте рассмотрим еще один пример.

    Допустим у нас есть список машин, который мы хотим отобразить:

    const Cars = ({ cars }) => {
      return cars.map((car) => {
        return (
          <Car key={car.id} car={car} />
        )
      });
    }

    Тут нам понадобилось добавить обработчик клика на машину. Мы создаем метод onCarClick и передаем его в компонент Car.

    const Cars = ({ cars }) => {
      const onCarClick = (car) => {
        console.log(car.model);
      }
      
      return cars.map((car) => {
        return (
          <Car key={car.id} car={car} onCarClick={onCarClick} />
        )
      });
    }

    В такой ситуации на каждый рендер компонента Cars у нас создается новая функция onCarClick, соответственно, не важно Car  это PureComponent или обернут в memo, все машины всегда будут заново рендерится, т.к. получают каждый раз новую ссылку на функцию.

    Для этого и нужен useCallback, если мы завернем функцию в хук, то у нас в переменной onCarClick будет уже возвращаться мемоизированая функция, хоть мы в нее на каждый рендер и передаем новую функцию

    const Cars = ({ cars }) => {
      const onCarClick = useCallback((car) => {
        console.log(car.model);
      }, []);
      
      return cars.map((car) => {
        return (
          <Car key={car.id} car={car} onCarClick={onCarClick} />
        )
      });
    }

    Таким образом все компоненты Car не будут рендериться лишний раз, т.к. ссылка на функцию останется прежней.

    А если заглянуть внутрь компонента Car. Там мы создадим еще одну функцию, которая свяжет onCarClick и объект car.

    const Car = ({ car, onCarClick }) => {
      const onClick = () => onCarClick(car);
      
      return (
        <button onClick={onClick}>{car.model}</button>
      )
    }

    В этом случае нет никакой пользы оборачивать метод в useCallback, т.к. нам не важно, ссылка это на функцию с прошлого рендера или с текущего рендера, а useCallback как мы уже знаем не бесплатный.

    Итоги

    Подытожить данную статью можно следующими словами. React хоть и поддерживает компоненты в виде классов, но имеет больше маневров над ускорением именно компонентов в виде функций. Да и сама экосистема все больше в качестве API предоставляет вам именно хуки, что невозможно использовать в классах:

    import { useLocation } from "react-router-dom";
    
    import { useSelector } from "react-redux";
    
    import { useLocalObservable } from "mobx-react-lite";
    
    import { useTranslation } from "react-i18next";

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

    А если вам понравилось данная статья, то здесь есть еще немного интересного.

    Чао

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

      –4

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

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

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


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

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

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


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

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

          React core разработчики говорили по их статистике разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить, а когда лучше создать метод как arrow function. Для людей, кто это уже освоил давно, кажется очень простой задачей работать с классами, но видимо это не так.
            0
            Я думаю этот текст в документации появился вместе с выходом хуков. Поэтому интересно, где они эту статистику взяли. Ну и интересно, с чем был опыт в прошлом у этих новичков, с функциональным программированием или с ООП.
            разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js, как работать внутри него с this и когда надо функцию биндить,
            ну и я сомневаюсь, что такие новички на хорошем уровне понимают, как использовать хуки. Скорее всего, делают кучу ошибок, и не замечают их.
              0
              Я конечно не знаю точного ответа, но если предполагать, то возможно у них есть условно лаба и есть фидбек о том сколько сложностей вызывает освоение функций и сколько сложностей после этого вызывает освоение классов и работа с контекстом. А сейчас на хуках осваивать контекст в принципе не особо то и важно, да и сами классы нет смысла изучать. Поэтому можно сделать вывод, чтобы теперь начать писать на React, вам нужно меньше знать о js чем раньше.
              +2
              React core разработчики говорили по их статистике разработчикам новичкам гораздо проще понять хуки, чем осознать как работает class в js

              Но практика показала что замыкания сами по себе и хуки в частности взрывают мозг разработчикам куда эффективнее, чем this keyword :)


              Судя по собеседованиям для большинства разработчиков хуки это "новая магия". Причём не важно сколько лет опыта у человека. Озвученный вопрос в статье про создание метода я частенько спрашивал на собеседованиях (у тех кто смог ответить на элементарные вопросы). Видел как тяжело идёт понимание, что если мы создали функцию… ты мы её создали. И да и она и dependencies создаются вообще всегда с нуля, просто при удачном раскладе отбрасывается результат. Вот прямо видно было как искра в глазах появляется. Эдакое "эээврика, ничего себе… А я и не думал об этом раньше с этого угла".


              С классами многие вещи было реализовать сложно и неудобно\некрасиво. Однако относительно прямолинейно и понятно. Пишем метод. Добавляем новое поле в state. Берём методы жизненного цикла. Разгребаем баги потому что не учли кучу нюансов (ну или забиваем на баги). А тут прямо приходится мышление перестраивать под нечто более декларативное. И пока мозг рассматривает всё как "маааагия" получается ерунда.

                0
                Видел как тяжело идёт понимание, что если мы создали функцию… ты мы её создали. И да и она и dependencies создаются вообще всегда с нуля, просто при удачном раскладе отбрасывается результат.

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

                Есть лишь один момент в котором я сомневаюсь. Все мы с класами работали еще на php или Java или на других языках, когда JS использовался очень минимально в проекте. А что если нам с классами все просто и понятно именно потому что у истории программирования много лет опыта в этой стезе, уже куча статей как решать любую проблему с классами и мы уже все это давно прошли. Возможно и с хуками решится эта проблема, когда пройдет через хабр 10-ки статей и это уже будет все просто и очевидно

                Ответа я конечно не знаю, но вопрос который я задаю себе: не являются ли все эти проблемы с хуками просто борьба с чем то новым? Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?

                Чтобы это понять, планирую общаться с новыми поколениями, как они вообще воспринимают хуки
                  0
                  Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?

                  Мне очень нравится писать на хуках. Я нахожу их гораздо более удобными чем ООП модель. Но всё же я не могу не отметить, что людям хуки даются с большим скрипом. И скажем многие "middle" разработчики требуют долгого времени адаптации. А как в серьёзный проект вливаться junior-у ума не приложу. Кажется React просто отрезает их как класс. Для них и без того очень много магии, а тут она сразу в квадрате. Давать им задачи на вёрстку? Так они закономерно взвоют. Да и не так много таких задач.

                    0
                    Ответа я конечно не знаю, но вопрос который я задаю себе: не являются ли все эти проблемы с хуками просто борьба с чем то новым? Не являемся ли мы просто консерваторами, потому что мы привыкли писать на классах?
                    У меня такой вопрос отпадает при сравнение, в каком из двух примеров код проще, читабельней, меньше вероятность допустить ошибку, в том числе ошибку, влияющую на производительность:
                      const someFunction = useCallback((title) => {
                        console.log(title);
                      }, []);
                    
                      someClassFunction = (title) => {
                        console.log(title);
                      };
                    

                0
                Я создал create-react-app template с примерами хуков и тестов для них — github.com/morewings/cra-template-quickstart-redux
                +1

                Картинка из прошлого поста в ленте как раз в тему...


                image

                  0
                  на 100% в тему))
                  0
                  const Test = () => {
                    const someFunction = useCallback((title) => {
                      console.log(title);
                    }, [title])
                  
                    return (
                      <button onClick={someFunction}>
                        click me!
                      </button>
                    )
                  }

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

                    0
                    да, вы правы, редактировал несколько раз и ошибся, сейчас исправлю
                    Спасибо!
                    0
                    Проект, полностью написанный на хуках, имеет менее глубокое дерево компонентов.
                    Смотря, какими техниками пользоваться. Если ограничиваться техниками, распространёнными в реакт-сообществе, то да. Но в других стеках есть и другие техники.

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

                    Пишем свой useCallback
                    Я бы с удовольствием посмотрел на читабельные реализации основных хуков. А то по их исходникам не понятно. Слишком все разбросано по разным файлам.
                    К сожалению, в этом примере prevState существует только в одном экземпляре и в нескольких компонентах этот useCallback использовать не получится. Интересно, как сделать так, чтобы внутренние механизмы react-а сохраняли prevState, связав его с компонентом, в котором он используется, чтобы полноценный аналог useCallback написать. Наибольшее преставление о связывании компонента с хуком мне дал этот ответ — stackoverflow.com/a/53730788
                      0
                      К сожалению, в этом примере 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 Все выше псевдокод

                        0
                        useRef
                        мне интересно увидеть аналогичный код реализации хуков без использования встроенных хуков react-а. В данном случае надо сначала реализовать свой useRef)

                        Мне как раз и не нравится, что эти глобальные переменные скрыли и не понять, как к ним из хуков обращаться, если надо.
                        Я сейчас обсуждаю, как правильно реализовать более простую и удобную альтернативу useReducer — github.com/reactjs/rfcs/issues/185. Мне сказали, что вариант с useEffect + useRef не будет работать в concurrent mode, а useMemo будет. Почему так, мне пока не понятно. Знал бы детали реализации, может быть бы и понял.
                          0
                          как к ним из хуков обращаться, если надо.

                          Всё просто. Скрыли как раз потому, что не надо к ним обращаться. Такой код может не пережить без полного переписывания даже 1 мажорного релиза. Плюс это уже грубое нарушение здравого смысла. Имхо, если настолько сильно приспичило пойти против шерсти, лучше поменять React на что-то другое, что решит задачу лучше, чем пытаться вот так вот лезть в глобалки. Так то если вот 100% надо — вы можете добраться до FiberNode через domNode-ы. Но это адский костыль.

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

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

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

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

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

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

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

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

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

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

                              0
                              Внимание вопрос
                              В каком из вариантов написания компонента функция присваемая переменной 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).

                                +1
                                В том то и дело что польза есть! Примеры, которые вы приводите, содержат фундаментальные ошибки.

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


                                Я использую VSCode с плагином ESLint на который установлено специальное дополнение по автоисправлению всех зависимостей хуков

                                Рекомендую отключить правило для useEffect (и уж точно не авто-исправлять его). Оно легко может сильно переломать вам поведение программы (например вызвать eternal loop загрузки какого-нибудь API метода). По мнению авторов хука в useEffect должны упоминаться все вещи, от которых он зависит. А это далеко не всегда так. Эффект совершенно не обязательно должен перезапускаться при изменении любой из зависимостей. Это ортогональные вещи.

                                0
                                Здесь функция onClick явно зависит от props. Чтобы React не делал лишнего рендера нужно чтобы ссылка на функцию onClick не менялась, при передачи одного и того же значения. А добиться этого как раз можно используя useCallback.

                                Из этих слов не очень понятно, как ссылка на функцию, каждый раз новая, которую мы передаем в button onClick приводит к «лишнему рендеру»
                                  0

                                  Вот то что было написано в вашем примере:
                                  const onClick = () => onCarClick(car);
                                  А вот, так как надо делать с точки зрения React:
                                  const onClick = useCallback(() => onCarClick(car), [car]);


                                  При первом рендеренге, в обоих случаях, будет создана ссылка на функцию onClick.
                                  При повторном рендеренге onClick, созданная без useCallback, будет содержать новую ссылку на анонимную функцию.
                                  А вот при повторном рендеренге onClick, созданная через useCallback, будет содержать туже ссылку на анонимную функцию. В этом и есть магии функции useCallback! Задача useCallback состоит в том чтобы ссылка на функцию внутри не менялась при отсутствии изменений в deps


                                  Если при повторном рендеренге реакт видит что ссылка на функцию не поменялась, то он не будет производите перерендер.

                                    +4
                                    А вот, так как надо делать с точки зрения 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 вещах, используемых по всему проекту
                                      +1
                                      Мои аплодисменты, ответ шикарен! Жаль у меня недостаточно кармы чтобы плюсануть.
                                      Единственное я бы добавил один нюанс по поводу следующих строк:
                                      Однако в чём заключается render этой <button/>? На самом деле в removeEventListener и addEventListener. Всё. Это практически нулевые затраты.

                                      Я изучал исходники хуков (про это тоже хочу написать статью и снять видео), и там useCallback включает в себя логику думаю не дешевле чем добавлять и удалять EventListener. Строится очердь хуков, запускает метод render hooks. Так же нужно сравнивать deps которые прислал через Object.is и другие сложности, так что думаю addEventListener будет если не дешевле, то как минимум столько же стоить
                                        0

                                        У меня сложилось впечатление, как будто вы иногда не используете useCallback и видимо вас очень задевают те люди, которые делают иначе.


                                        Ну не хотите вы использовать useCallback — ну так и не используйте! Это ваше право, ваш выбор! Я не заставляю вас делать этого в каждом компоненте. React рекомендует использовать useCallback но не заставляет. Дедлайны горят! Больше кода надо писать! Не видите в этом пользы! Ну так и не делайте! Я вас ругать не буду! Правда!


                                        А кому надо?

                                        Этот надо мне!


                                        И зачем?

                                        Чтобы перерендер работал быстрее!


                                        Чего вы добились этим кодом?

                                        Чтобы было легче переиспользовать код! Чаще всего, я сталкиваюсь с задачами, когда я не знаю что там происходить в компонентах ниже/выше по дереву (писал больше пол года назад, либо писал другой человек). А там может быть не только removeEventListener/addEventListener но другие вещи. Моя задача написать такой код, который бы делал минимальное число лишних перерендеров. Поэтому я использую useCallback.

                                          0
                                          У меня сложилось впечатление, как будто вы иногда не используете 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. В целом это похвально, что вы интересуетесь производительностью, и имеете представление о пользе мемоизации. Но всё же я рекомендую вам углубиться в эту тему поглубже. Она не так проста, как, судя по всему, вам кажется. Она переполнена компромиссами, оценками и подходами. Всё далеко не так просто.

                                            0
                                            У нас просто очень большой опыт работы с мемоизацией и React. Мы на ней собаку съели.

                                            Интересно у вас… Прямо свою предыдущую работу вспомнил (там вебсокет-пулеметчик, тоже приходилось упарываться в мемо и вообще включать голову).

                                              0

                                              Поправьте, если я вас не правильно понял, но вы предлагаете следующее: если "ниже" DOM нода, то не используйте useCallback. Если же там компонент, то лучше обернуть.


                                              Примеры автора статьи слишком упрощены, как и во многих других статьях. От этого и масса вопросов.


                                              Например пример со списками — лучше для перформанса наверное вообще не прокидывать в каждый элемент колбек. А ловить клик на родителе и уже там определять выбранный элемент.


                                              Кроме того, не понятны примеры, когда колбек принимает аргумент и что-то с ним делает. В таком случае в зависимостях у него ничего нет и тут действительно стоит задуматься, чтобы выкинуть такой колбек за пределы рендер-функции.

                                                +1
                                                если "ниже" DOM нода, то не используйте useCallback

                                                Ага. Особого смысла нет. Производительность останется на примерно том же уровне, а вот качество кода заметно снизится. Но в редких случаях может быть актуальным и тут useCallback применить. Скажем когда на него завязано несколько эффектов очень хитроумным способом. Но это уже большая специфика.


                                                Если же там компонент, то лучше обернуть

                                                Не факт, что лучше, но вполне может быть. Зависит от конкретных компонент, глубины древа и пр… К примеру если вы передаёте в дочерний компонент children, то почти 100% внутренний Memo идёт лесом. Ну просто потому что children всегда новый. Есть ли тогда смысл лишний раз приседать с useCallback? В общем тут нужно вникать в каждый отдельно взятый случай и в то как у вас потоки данных по приложению ходят.


                                                А ловить клик на родителе и уже там определять выбранный элемент.

                                                Там много решений. Один из популярных — передавать от родителя внутрь один callback для всех children. Просто в сигнатуре этого метода должен быть ещё и ID, чтобы действие можно было произвести над нужной сущностью.


                                                А вот готовить каждому элементу его собственный callback это обычно геморрой.
                                                Но в любом случае тут стоит исходить из здравого мысла.


                                                чтобы выкинуть такой колбек за пределы рендер-функции

                                                Почему бы и нет. Но стоит уточнить что это очень редкий случай. Ибо получается что если callback делает хоть что-то, то он оперирует глобальными переменными (ведь ни к чему ещё у него просто нет доступа).


                                                Но да — если зависимостей нет, то можно смело выносить метод из компонента наружу.

                                                  +1

                                                  Спасибо за подробный ответ.


                                                  Хотел ещё добавить. Вот есть статья. И конкретно раздел про мемоизацию. Ведь по сути, useCallback — это ведь useMemo.


                                                  Так вот, с одной стороны мысль о мемоизации всего — выглядит крайне страной. С другой стороны, автор утверждает что нет никакой заметной разницы при "лишней" мемоизации. Так не кажется ли вам, что вся эта тема с useCallback не стоит даже обсуждения? Особенно без каких то замеров.


                                                  Я, признаться, не очень понимаю всю эту шумиху вокруг хуков. Они мне с самого начала показались порочной практикой. Они слишком много делают "магически". Да, в какой-то мере удобно, но читать это всё потом такая боль

                                                    0
                                                    С другой стороны, автор утверждает что нет никакой заметной разницы при «лишней» мемоизации

                                                    Я утверждал, что в конкретной ситуации пользователь не заметит разницы, но всегда найдется 10 других ситуаций, в которых хочется выжать из инструмента максимум. И мы должны знать как это сделать.

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

                                                    Они мне с самого начала показались порочной практикой. Они слишком много делают «магически».

                                                    Тут дело субъективное, я, например, с первого дня втянулся в новые возможности хуков. Наконец решил проблемы с переиспользованием логики, наконец перестал биндить каждый метод во имя правильного контекста. Да и сама идея использования бесконечного замыкания для хуков максимально соответствует духу JavaScript, а не попытки имитировать классы из других языков, что вечно раздражало.

                                                    Поэтому сколько людей, столько и мнений :)
                                                      0
                                                      Я утверждал, что в конкретной ситуации пользователь не заметит разницы

                                                      Полагаю, что @epovidlov имел ввиду Anujit Nene, а не вас. Там по ссылке очень большая статья (почти мини-книга).

                                                        0
                                                        Я после прочтения статьи и комментариев задался следующими вопросами:
                                                        1. При использовании обычного метода (немемоизированного) react-dom будет вызывать add/removeEventListener на DOM-элементе, в то время как при использовании мемоизированной версии этого происходить не будет, верно? Что-то мне подсказывает, что операция с DOM зачастую будет более дорогостоящей, нежели сравнения зависимостей и создание лишней функции. Есть соображения, как это замерить или уже готовые метрики?
                                                        2. У нас на проекте очень часто мемоизированные функции вызываются в эффектах. Использование useCallback позволяет, по моему скромному мнению, писать эти самые эффекты сходу, не выстреливая себе в ногу тем, что какая-то из вызываемых функций внезапно окажется немемоизированной и будет триггерить эффект, и так же безопасно включать эти самые функции в список зависимостей эффектов дочерних компонентов. То есть, делаешь все функции по дефолту мемоизированными и больше не вспоминаешь о возможных сайд-эффектах. Что вы думаете по этому поводу?
                                                          0
                                                          то операция с DOM зачастую будет более дорогостоящей

                                                          Дорогие DOM-операции это те DOM-операции которые влияют на layout. Т.е. двигают\изменяют размеры визуальных блоков. Дорогие они по причине чрезмерной сложности и взаимосвязанности. Самый яркий пример — ячейка таблицы. Размер одной единственной ячейки таблицы может повлиять на размеры вообще всех других ячеек таблицы. И таблицы целиком. И страницы целиком. А это очень много вычислений. А вы всего-то, скажем, точку после слова добавили.


                                                          А addEventListener в идеале сводится к 1 записи в hashmap в недрах v8 с O(1). На практике может и сложнее устроен. Я не копал.


                                                          То есть, делаешь все функции по дефолту мемоизированными и больше не вспоминаешь о возможных сайд-эффектах.

                                                          Примерно так мы и пишем код. Но это не main-stream, т.к. это сложно. Во всяком случае в и без того сложных проектах — это сильно усложняет код. Мемоизация и иммутабельность вообще не сильные стороны JS. Где-нибудь в Haskell, с этим наверное всё хорошо… Но не у нас.

                                                        0
                                                        Вот есть статья

                                                        Всю статью не читал. Только приведённый вами отрывок. Что я могу сказать. Только ППКС. Он в точности выдал в нём то, что я думал. Я тоже не согласен с Деном Абрамовым по этому поводу. Я тоже думаю про net gain. Ну и примерно в таком ключе мы и пишем наши приложения. По-умолчанию используем memo, а там где это очевидно создаёт проблемы за зря, или точно не даст никаких преимуществ — там не используем.


                                                        Я полагаю, что если взять хорошо написанное immutable приложение, то сделав memo везде по-умолчанию, вы в худшем случаем проиграете 1-5% производительности. А, возможно, даже выиграете в производительности. Однако если в принципе изначально писать с уклоном в мемоизацию, то выгода очевидна. Но это создаёт сложности для всех членов команды, особенно для наименее опытных, т.к. мемоизация "по взрослому" это ни разу не детский сад. Когда начинаешь писать вложенные weakMapы или профилировать shallowComparison цепочки из-за нормализации данных (чтобы не вылезти из 60 кадров в секунду), то понимаешь, что у всего есть своя цена и очень легко пере-овер-инжинирить.


                                                        Они слишком много делают "магически".

                                                        Да ну. Просто немного нестандартно. Немного другая парадигма. Просто практика показывает (во всяком случае наша), что хуки решают типичные бизнес-задачи намного лучше, чем ООП. Возможно я слишком плох в ООП, но мне действительно, спустя 1.5 лет с хуками, куда проще с ними решать сложные задачи.


                                                        но читать это всё потом такая боль

                                                        Я полагаю, что читать код с хуками боль только тогда, когда вы или их плохо понимаете (это лечится), или код написан плохо (это тоже лечится практикой и clean code подходом). В идеале всё наоборот. Хуки позволяют вам совершенно элементарным образом делить сложные элементы на множество простых. Очень удобный инструмент декомпозиции. В случае классов очень непросто реализовать сложную задачу не создав Гордиева узла. Я вот, если честно, просто не умею. Мне надо раз 10 отрефакторить код на классах, чтобы от него перестало нести тухлыми яйцами.

                                                          0
                                                          Просто практика показывает (во всяком случае наша), что хуки решают типичные бизнес-задачи намного лучше, чем ООП.
                                                          Я некоторое время работал в геймдеве. Там бизнес задачи вполне хорошо решаются с помощью ООП. Там используется композиция, похожая на ту, которая в реакте на классах. Но, там не пишут 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 — тоже.

                                                          У меня есть своя рабочая реализация поверх реакт классов. Надеюсь, найду время написать статью про нее.
                                                            0
                                                            { logicalBlock: logicalBlockA },

                                                            Это было. Называлось mixins. У каждого миксина был свой набор методов жизненного цикла. Почти сразу же их перевели в deprecated.

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

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

                                                                0

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

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

                                                                  Например:
                                                                  codesandbox.io/s/relaxed-driscoll-r6wwm?file=/src/App.tsx
                                          +1

                                          У меня в использовании 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;
                                          }

                                          Как видим, это аналогично старому доброму подходу с классами, в самом начале этой статьи.

                                            0
                                            Оригинальное решение, впервые такое вижу) но как мы знаем редкие ситуации требуют экстравагантных решений) Интересно было бы посмотреть, реальную задачу, которую решили таким образом
                                              0

                                              Да ничего особенного. Последний пример — у компонента есть свой memo(Footer), в который уехали кнопки "да/нет/наверно". Соответственно в пропсах футера есть колбэк onUserSelect, в который передается функция, работающая c некоторыми объектами, но сами эти объекты не передаются, ибо футеру они без надобности, он только сообщает о выборе пользователя. Таким образом, футеру нет нужды перерисовываться, если объекты поменялись.
                                              Я полагаю, этот кейс не выглядит каким-то совсем уж странным.

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

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


                                                  ...
                                                  const handleUserSelect = useCallback(answer => {
                                                     // здесь некий код, использующий props.someProp
                                                  }, [props.someProp]);
                                                  
                                                  return (
                                                     ...
                                                     <Footer onUserSelect={handleUserSelect} loading={loading} />
                                                  );

                                                  someProp меняется часто, ещё ДО нажатия на кнопку. Footer не зависит от someProp, но в приведенном коде всё равно будет вынужден зря реконсилиться, потому что изменение someProp, видите ли, привело к замене handleUserSelect. Мой велосипед как раз для такого кейса.

                                                    0

                                                    По нажатию на кнопку вызывается метод, который может вызывать setState. Т.е. реактивное поведение остаётся каким и было. Реальной необходимости ещё и саму ссылку на метод при этом менять обычно нет. Достаточно чтобы метод не обращался к неактуальным данным.

                                                0

                                                У нас много кода который должен быть доступен по ссылке (не обязательно 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.


                                                Работает хорошо. Но более много словно чем хотелось бы. Ну и требует некоторого времени на то чтобы въехать как это работает. Однако используем такой механизм только там где это правда актуально.

                                                  0

                                                  Делал похожую штуку для viewModel к реактовским компонентам, тоже на useRef (модель была мобиксовым стором со своими экшенами, и жила, пока жил компонент). То есть специальный хук создавал и поддерживал постоянный экземпляр, да ещё и вызывал у него метод clean при наличии такового, если модели нужна очистка, допустим таймер прибить. Как бы двустороннее сотрудничество компонента и модели — модель рулит логику и уведомляет вьюху, а компонент через хук предоставляет жизненный цикл.

                                                    0
                                                    так вроде же mobx-react предоставляет хук useLocalObservable для таких целей
                                                  0
                                                  const { current: persistentCallback } = useRef(func)

                                                  Кажется, что приведенный выше фрагмент кода повторяет вашу логику. Вы инкапсулировали ее в хук для большей декларативности?

                                                  UPD: Прошу прощения, понял, что это разные вещи.

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                Самое читаемое