Что выбрать: глобальные переменные или useThis?

    Привет Хабр!

    Как вы знаете при переходе с компонентов классов на функциональные, у нас отняли такую полезную вещь как this, которая указывает на текущий экземпляр компонента. И конечно у нас возник вопрос: “а где же тогда хранить timeoutId?”. И я видел как люди по разному выкручивались из этой проблемы (Данная статья, является расшифровкой видео)

    Например, если timeoutId используется только в рамках одного useEffect можно набросать следующий вариант:

    useEffect(() => {
      const timeout = setTimeout(() => {
        // do some action
      }, 3000);
      
      return () => {
        clearTimeout(timeout);
      }
    }, [...]);

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

    Поэтому многие решили, просто создавать переменную вне компонента и хранить в ней id этого тайм-аута, чтобы например по клику иметь возможность его отменить:

    let timeout;
    
    const Test = () => {
      const onClick = () => clearTimeout(timeout);
      
      useEffect(() => {
        timeout = setTimeout(() => {
          // do some action
        }, 3000);
      }, [...]);
      
      
      return (...);
    }

    И это работает в большинстве случаев без каких-либо проблем. Но как всегда есть “НО”.

    Проблема глобальных переменных

    Давайте рассмотрим пример. Допустим у нас есть компонент Counter, в котором есть локальный счетчик и глобальный счетчик определенный вне компонента:

    let globalCounter = 0;
    
    const Counter = () => {
      const [stateCounter, setStateCounter] = useState(0);
      
      const onClick = () => {
        globalCounter++;
        setStateCounter((stateCounter) => stateCounter + 1);
      };
      
      return (
        <div>
          <p>global counter - <b>{globalCounter}</b></p>
          <p>state counter - <b>{stateCounter}</b></p>
          <button onClick={onClick}>increment</button>
        </div>
      );
    }

    Компонент достаточно простой. Теперь добавим родительский компонент:

    const App = () => {
      const [countersNumber, setCountersNumber] = useState(0);
      
      return (
        <div>
          <button onClick={setCountersNumber((count) => count + 1)}>
            add
          </button>
          <button onClick={setCountersNumber((count) => count - 1)}>
            removed
          </button>
          {[...Array.from(countersNumber).keys()].map((index) => (
            <Counter key={index} />
          ))}
        </div>
      );
    };

    Здесь мы храним в state количество счетчиков, и ниже имеем 2 кнопки: для увеличения количества счетчиков и для уменьшения. И собственно вставляем сами счетчики в таком количество, как у нас указано в переменной countersNumber.

    Смотрим результат

    Перейдем в браузер и выполним следующие действия:

    • Добавим один счетчик;

    • Внутри появившегося счетчика, нажмем "increment" три раза;

    • Добавим второй счетчик.

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

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

    Рассмотрим альтернативу

    Решением данной проблемы является использование хука useRef(). Именно это и рекомендует React документация:

    Они прямо упомянули, что useRef() нужно использовать как аналог this. И более того, для удобства добавили в useRef() возможность передачи начального значения. Поэтому вариант с timeout может выглядеть следующим образом:

    const Test = () => {
      const timeout = useRef();
      
      const onClick = () => clearTimeout(timeout.current);
      
      useEffect(() => {
        timeout.current = setTimeout(() => {
          // do some action
        }, 3000);
      }, [...]);
      
      return (...);
    }

    Возможно в этом решении вас смущает, то что в timeout начинает храниться свойство current, это действительно выглядит немного странно, но у этого есть разумное объяснение, о котором мы рассказывали в предыдущей статье “createRef, setRef, useRef и зачем нужен current в ref”.

    prevProps не исчезли вместе с классами

    Использование useRef() для хранения timeout это конечно же очень полезно. Но есть и более интересные способы использования. Например, в компонентах в виде классов есть удобный метод жизненного цикла componentDidUpdate. В качестве первого параметра нам предоставляют prevProps, т.е. props из предыдущей итерации. Это давало нам возможность, сравнивать props из текущей итерации с props из предыдущей. На основании этого выполнять какие-то действия.

    componentDidUpdate(prevProps) {
      if (this.props.id !== prevProps.id) {
        // do some action
      }
    }

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

    Давайте напишем хук, который будет возвращать props из предыдущей итерации:

    const useGetPrevValue = (value) => {
      const prevValueRef = useRef();
    
      useEffect(() => {
        prevValueRef.current = value;
      });
      
      return prevValueRef.current;
    };

    Здесь мы получаем value из текущей итерации, после создадим ref для хранения данных между итерациями. И в рамках текущей итерации мы вернем текущее значение current равное null. Но перед началом следующей итерации мы обновим current значение, таким образом в следующей итерации в ref у нас будет хранится значение из предыдущей.

    И осталось только использовать этот хук:

    const CounterView = ({ counter }) => {
      const prevCount = useGetPrevValue(counter);
      
      const classes = classNames({
        [styles.greenCounter]: counter < prevCounter,
        [styles.redCounter] counter > prevCounter,
      });
      
      ...
    }

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

    Расширяйте сознание

    Мы упомянули лишь несколько базовых вариантов использования ref для решения нетипичных для ref задач. Но их гораздо больше. Например, если вы откроете телеграм десктоп версии, и будете скролить вверх, то у вас будет отображаться дата, а если вниз, она исчезнет. Мы можем хранить предыдущее значение скрола в ref, для того чтобы вычислить в какую же сторону скролит пользователь.

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

    И если вы знаете еще какие-то интересные варианты использования ref обязательно пишите в комментариях

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 23

      0

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

        0
        Интересный вариант использования) я о таком даже не думал)
        а что включает ваша viewModel?
        onClick обработчики включает? как значение там обновляете, если к примеру redux-store обновился и прочее) если выложите черновик где-нибудь, было бы круто!
          +2

          Вот примерный набросок для MobX


          быдлокод внутри
          // использование мобиксового стора в компоненте
          const MyComponent = observer(props => {
            const model = useMyViewModel();
          
            return (
              <div onClick={model.handleClick}>
                {model.text}
              </div>
            );
          })
          
          // вью-модель - мобиксовый стор
          class MyViewModel {
            constructor(...) { ... }
            @observable text = '123';
            @action handleClick = (e) => {
              // что-то делаем
            };
            clear = () => { 
               /* тут можно какую-нибудь очистку,
                   например убиение таймеров */
            };
          }
          
          // хук-поставщик для модели
          function useMyViewModel() {
            return useViewModel(() => new MyViewModel())
          }
          
          // вспомогательный хук для создания вью-моделей, с опцией очистки
          function useViewModel(creator) {
            const ref = useRef();
            if (!ref.current) {
              ref.current = creator();
            }
            useEffect(() => () => {
              if (ref.current && typeof ref.current.clear === 'function') {
                ref.current.clear();
              }
            }, []);
          }
            0

            забыл "return ref.current;" в последней функции

              0
              очень интересная имплементация)
              мы тоже пишем на mobX, но с хуками мучаемся во ViewModel)
              Возможно себе возьмем на вооружение какие то подходы)
              Спасибо!)
              –1
              Как бы вот так надо, зачем лишние манипуляции и лишний быдло-код? =)
              const MyComponent = observer(props => {
                const [model] = useState(() => new MyViewModel());
                useEffect(() => {
                  return model.clear; // cleanup
                });
              
                return (
                  <div onClick={model.handleClick}>
                    {model.text}
                  </div>
                );
              })
              
              // вью-модель - мобиксовый стор
              class MyViewModel {
                constructor(...) { ... }
                @observable text = '123';
                @action handleClick = (e) => {
                  // что-то делаем
                };
                clear = () => { 
                   /* тут можно какую-нибудь очистку,
                       например убиение таймеров */
                };
              }
              
                +1

                Вьюха засоряется лишним кодом, который к ней не относится (нарушение SRP)
                Если оную модель захочется поюзать ещё где, придется копипастить всю логику (нарушение DRY)
                Сильная зацепленность, вьюха должна знать, что модели требуется очистка, и что модель создается конструктором, и вообще что это вью-модель, а не обычная. Эти знания не нужны для функциональности вьюхи.
                Ещё до кучи нарушение DIP.
                В общем, одни плюсы :)

                  –1
                  Ну ладно
                  const MyComponent = observer(props => {
                    const model = useLocalState(() => new MyViewModel());
                  
                    return (
                      <div onClick={model.handleClick}>
                        {model.text}
                      </div>
                    );
                  })
                  
                  // вью-модель - мобиксовый стор
                  class MyViewModel {
                    constructor(...) { ... }
                    @observable text = '123';
                    @action handleClick = (e) => {
                      // что-то делаем
                    };
                    clear = () => { 
                       /* тут можно какую-нибудь очистку,
                           например убиение таймеров */
                    };
                  }
                  
                  function useLocalState(creatorFn) {
                    const [model] = useState(creatorFn);
                    useEffect(() => {
                      return typeof model.clear === 'function' && model.clear; // cleanup
                    });
                  
                    return model;
                  }
                  
                0
                Тоже подумывал об отделение логики от View в функциональных компонентах. В компонентах на классах с этим было проще.
                Спасибо за интересный пример! Он демонстрирует широкие возможности для написания функциональных компонентов с более понятным и предсказуемым потоком управления. Осталась дождаться, когда MobX переплюнет Redux, разделение логики и View в компоненте станет стандартом и затем компоненты снова переделают на классы или объекты)

                Не вижу необходимости в хуке useMyViewModel. ViewModel в нем заменить нельзя, а его нельзя заменить компоненте. Можно сразу класс передать в хук useViewModel.

                Подумал также, что подход из вашего примера по аналогии с custom hooks позволяет повторное использование ViewModel и их взаимодействие в одном компоненте при необходимости:
                const MyComponent = observer(props => {
                  const modelA = useMyViewModel(MyViewModelA);
                  const modelB = useMyViewModel(MyViewModelB, modelA); //передача экземпляра первой ViewModel во вторую, если это где-то понадобится. 
                
                  return (
                    <div onClick={modelA.handleClick}>
                      {modelA.text}
                    </div>
                  );
                })
                

                  0
                  и затем компоненты снова переделают на классы или объекты)

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


                  Не вижу необходимости в хуке useMyViewModel.

                  Да нет, это важный момент. На самом деле сейчас подумал, что лишнее тут только слово View. Надо useMyModel. По функциональности, в общем. То что это временная модель, компоненту-пользователю модели не интересно.
                  "Персональный" хук-поставщик (причем даже не для конкретного класса, а для интерфейса) позволяет уменьшить зацепленность. Можно менять жизненный цикл у модели, можно резолвить некоторые её зависимости, не меняя использующий код, можно создавать экземпляры подклассов при некоторых условиях. В общем, идеальное связующее звено.

                    0
                    эволюция показала, что функциональные компоненты
                    Ну вы же в итоге используете классы внутри функционального компонента.
                    Это разве сильно отличается от идеи сделать компонент объектом/классом?
                    Согласен, что функции идеальны для JSX кода.
                    Но и над компонентами-классами можно подшаманить и функцию render выносить отдельно и применять к любому компоненту, как и разбить компонент на составляющие, которые также можно применять к любому компоненту.
                    Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.

                    Насчет персонального хука-поставщика.
                    Если где-то нужно добавить некую логику между компонентом и моделью, то да, его стоит добавлять. Может местами даже цепочку хуков.
                    Не увидел смысла, когда он показан как в примере, где он ничего не делает и не отделим от класса модели.
                      0
                      Ну вы же в итоге используете классы внутри функционального компонента. Это разве сильно отличается от идеи сделать компонент объектом/классом?

                      Это совершенно разные вещи. Компонент остается функцией, а использует, повторюсь, не классы, а решателей определенных задач.


                      Но и над компонентами-классами можно подшаманить и функцию render выносить отдельно и применять к любому компоненту, как и разбить компонент на составляющие, которые также можно применять к любому компоненту.

                      Было уже. Итогом развития этой идеи стали HOC'и. Чтобы обеспечить представление необходимыми интерфейсами, приходилось обертывать несколько раз. Неудобно, негибко. Думаю, тут не надо подробно расписывать.

                        0
                        Вы же не считаете, что аналоги декораторов в виде HOC — это предел совершенства композиции в ООП и что авторы React первые, кто столкнулся с тем, что надо предоставить возможность разработчикам расширять что-то подобное компонентам React и в других областях не существует других решений, которые авторам React не известны? Есть, просто во фронтенде один юный разработчик умудрился переключить внимание почти всего сообщества с объектов на функции и перестали искать другие решения для компонентов-объектов.
                        Как я уже упоминал, скоро
                        Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.

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

                          Интересно будет взглянуть.

                            0
                            Выложил статьи. Вот только получились довольно большие и, наверное, мало кто прочитает.
                            habr.com/ru/post/545368
                            C 3-го пункта (c паттерна «Стратегия») и ниже описаны подходы, которые не распространены в React.

                            Во второй статье я описал применение одного из подходов в React компонентах (для компонентов-классов и компонентов-функций).
                            habr.com/ru/post/545064
                            Для реализации его в функциональных компонентах я использовал ваши идеи с userRef и пробросом событий из хуков. Еще раз благодарю за интересный и полезный пример!
            +1
            Обычно за код, приведенный в статье, джунов отправляют учить функции очистки.

            А где же тогда хранить timeoutId?

            В большинстве случаев он хранится в замыкании функции очистки.

            const Test = () => {
              const [isTimeoutActive, setIsTimeoutActive] = useState(true);
              
              const handleClick = () => setIsTimeoutActive(false);
              
              useEffect(() => {
                if (!isTimeoutActive) return;
            
                const timeoutId = setTimeout(() => {
                  // do some action
                }, 3000);
            
                return () => {
                  clearTimeout(timeoutId);
                };
              }, [isTimeoutActive]);
              
              return (...);
            }
            


            Приимущества:
            • Декларативность, ради которой хуки и создавались
            • Код менее размазан. Создание и удаление ресурса находятся рядом.
            • Меньше возможностей у компонента привратится спагетти-код (например начав присваивать в timeout.current какие-то другие таймауты)
            • Таймаут автоматически отменится, когда компонент перестанет существовать
              0
              Подход интересный :)
              Но я бы делать так не стал по нескольким причинам:
              — при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого
              — читабельность тоже как по мне не особо улучшилась, теперь в этом коде стало гораздо React чем JavaScript. Чтобы обнулить timeout мы вызываем не clearTimeout, а вызываем какую-то функцию абстракцию, на которую где-то в другом месте подписан useEffect, который в return возвращает функцию, которая обнулит timeout. По мне так, этот вариант и есть спагетти когда одно за другое зацепленно, как макарошки вокруг вилки
                +1
                То что вы описали и есть React программирование с использованием хуков. Оно подразумевает:

                — больше React, чем JavaScript
                — накладные расходы в пользу большей декларативности

                > при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого

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

                  Я не считаю вашу идеологию плохой, но мне ближе писать в первую очередь на JS чем на React, т.к. это лишь библиотека для отображения :)
              0
              Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.

              Они сейчас посмотрят на этот код, скопипастят, поменяют setTimeout на setInterval, и получат ай-яй-яй. И потом окажется, что useRef для управления ресурсами не очень то и подходит.

              React, т.к. это лишь библиотека для отображения


              А вы не путаете React и JSX? useState, useEffect, useMemo, useRef, useCallback, useReducer не имеют прямого отношения к отображению.

              Я пишу сейчас 3D редактор на React, где для отображения используется Babylon.js. React используется как декларативная state machine и для управления ресурсами. Куча React кода, а из отображения там только
              <canvas ref={setCanvasRef} />
                +1
                > Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.

                React написан на JS как тут можно их не мешать я не очень представляю)
                Да и контент который я публикую, он никак не направлен на новичков. Для новичков и так курсов / уроков / статей пруд пруди. А вот для senior разрабов контента порассуждать не хватает. Собственно поэтому я и начал свою деятельность.

                Спор дальнейший не вижу особого смысла продолжать :) очевидно что мы оба останемся при своем мнении)

              Only users with full accounts can post comments. Log in, please.