Меня зовут Артём Березин, я разработчик нескольких внутренних сервисов Яндекса. Последние полгода я активно работал с React Hooks. По ходу дела возникали некоторые сложности, с которыми приходилось бороться. Теперь хочу поделиться этим опытом с вами. В докладе я разобрал React Hook API с практической точки зрения — зачем нужны хуки, стоит ли переходить, что лучше учитывать при портировании. В процессе перехода легко наделать ошибок, но избежать их тоже не так сложно.



— Хуки — это просто еще один способ описывать логику ваших компонентов. Он позволяет добавить к функциональным компонентам некоторые возможности, ранее присущие только компонентам на классах.



Прежде всего это поддержка внутреннего состояния, затем — поддержка побочных эффектов. Например — сетевых запросов или запросов к WebSocket: подписка, отписка от каких-то каналов. Или, возможно, речь о запросах к каким-то другим асинхронным или синхронным API браузера. Также хуки дают нам доступ к жизненному циклу компонента, к его началу жизни, то есть монтированию, к обновлению его пропсов и к его смерти.



Наверное, проще всего проиллюстрировать в сравнении. Перед вами здесь самый простой код, который только может быть с компонентом на классах. Компонент что-то меняет. Это обычный счетчик, который можно увеличивать или уменьшать, всего одно поле в state. В общем, я думаю, что если вы знакомы с React, код для вас совершенно очевиден.



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

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



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

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



Хуки дают некоторые преимущества по сравнению с классами. Прежде всего, как следует из предыдущего, используя кастомные хуки, можно шарить логику гораздо проще. Раньше, используя подход с Higher Order Components, мы выкладывали туда какую-то шаренную логику, и она являлась оберткой над компонентом. Теперь мы эту логику закладываем внутрь хуков. Тем самым дерево компонентов уменьшается: уменьшается его вложенность, и для React становится проще отслеживать изменения компоненты, пересчитывать дерево, пересчитывать виртуальный DOM и т. д. Тем самым решается проблема так называемого wrapper-hell. Тем, кто работает с Redux, я думаю, это хорошо знакомо.

Код, написанный с использованием хуков, гораздо легче поддается минимизации современными минимизаторами типа Terser или старым UglifyJS. Дело в том, что нам не нужно сохранять имена методов, не нужно думать о прототипах. После транспиляции, если target стоит ES3 или ES5, у нас обычно появляется куча прототипов, которые патчатся. Здесь всего этого не нужно делать, поэтому проще минимизировать. И, как следствие неиспользования классов, нам не нужно думать про this. Для новичков это часто большая проблема и, наверное, одна из главных причин багов: мы забываем про то, что this может быть window, что надо забиндить метод, например, в конструкторе или каким-то еще способом.

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

Доклад будет неполным, если я не расскажу о каких-то базовых хуках. Прежде всего, это хуки управления состоянием.



В первую очередь — useState.



Пример похож на тот, что в начале доклада. useState — это функция, которая принимает начальное значение, и возвращает tuple из текущего значения и функции для изменения этого значения. Вся магия обслуживается React внутри. Мы же можем просто либо прочитать это значение, либо его изменить.

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



Есть такой useState на стероидах для любителей Redux. Он позволяет более консистентно изменять стейт с помощью редьюсера. Я думаю, что те, кто знаком с Redux, можно даже не объяснять, для тех, кто незнаком, расскажу.

Редьюсер — это функция, которая принимает стейт, и некий объект, называемый обычно action, который описывает то, как этот стейт должен поменяться. Точнее, он передает какие-то параметры, а внутри reducer уже решает, в зависимости от их параметров, как стейт поменяется, и в итоге, должен вернуться новый стейт, обновленный.



Примерно таким образом он используется в коде компонента. У нас хук useReducer, он принимает функцию reducer, и вторым параметром начальное значение стейта. Возвращает, так же, как и useState, текущий стейт, и функцию для его изменения — dispatch. Если передать в dispatch объект-action, мы вызовем изменение стейта.



Очень важный хук useEffect. Он позволяет добавлять побочные эффекты для компонента, давая альтернативу жизненному циклу. В данном примере мы используем простой способ с useEffect: это просто запрос каких-то данных с сервера, с API, например, и вывод этих данных на страничке.



У useEffect есть продвинутый режим, это когда функция, переданная в useEffect, возвращает какую-то другую функцию, то эта функция будет вызвана при следующем цикле, когда этот useEffect будет применен.

Забыл упомянуть, useEffect вызывается асинхронно, сразу после того, как применится изменение к DOM. То есть он гарантирует, что он будет выполнен после рендера компонента, и может привести к следующему рендеру, если какие-то значения изменятся.



Здесь мы с вами впервые встречаемся с таким понятием, как dependencies. Некоторые хуки — useEffect, useCallback, useMemo — вторым аргументом принимают массив значений, которые позволят сказать, что отслеживать. Изменения в этом массиве приводят к каким-то эффектам. Например, здесь гипотетически у нас какой-то компонент выбора автора из какого-то списка. И табличка с книжками этого автора. И при изменении автора, будет вызван useEffect. При изменении этого authorId будет вызван запрос, и загрузятся книжки.

Также вскользь упомяну такие хуки, как useRef, это альтернатива React.createRef, что-то похожее на useState, но изменения в ref не приводят к рендеру. Иногда удобно для каких-то хаков. useImperativeHandle позволяет нам объявить у компонента определенные «публичные методы». Если использовать useRef в родительском компоненте, то он может эти методы дернуть. Если честно, один раз пробовал в учебных целях, на практике не пригодилось. useContext — прямо хорошая штука, позволяет взять текущее значение из контекста, если провайдер где-то выше по уровню иерархии определил это значение.

Существует один способ оптимизации React-приложений на хуках, это мемоизация. Мемоизацию можно разделить на внутреннюю и внешнюю. Сначала о внешней.



Это React.memo, практически альтернатива классу React.PureComponent, который отслеживал изменение в пропсах и менял компоненты только тогда, когда изменились пропсы или стейт.

Здесь же похожая штука, правда, без стейта. Она тоже отслеживает изменения в пропсах, и если пропсы изменились, происходит ререндер. Если пропсы не менялись, компонент не обновляется, и мы на этом экономим.



Внутренние способы оптимизации. Прежде всего, это достаточно низкоуровневая штука — useMemo, редко используется. Она позволяет вычислять какое-то значение, и перевычислять его только, если указанные в зависимостях значения изменились.



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

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

Для любителей функциональщины, это, скажем, прямо must have, потому что хуки — это функции, и к ним применимы приемы функционального программирования. К примеру, их можно комбинировать или вообще делать что угодно, используя, например, библиотеки, типа Ramda, и тому подобных.



Так как мы избавились от классов, нам теперь нет необходимости привязывать контекст this к методам. Если использовать эти методы в качестве колбеков. Обычно, это была проблема, потому что нужно было не забыть забиндить их в конструкторе, либо использовать неофициальное расширение синтаксиса языка, такие arrow-functions в качестве property. Довольно распространенная практика. Я же использовал свой декоратор, что тоже, в принципе, экспериментально, на методы.



Есть разница в том, как устроен жизненный цикл, как им управлять. Хуки практически все действия с жизненным циклом связывают с хуком useEffect, который позволяет подписаться, как на рождение, так и на обновление компоненты, так и на его смерть. В классах же для этого приходилось переопределять несколько методов, типа componentDidMount, componentDidUpdate, и componentWillUnmount. Также метод shouldComponentUpdate теперь можно заменить на React.memo.



Есть достаточно небольшая разница в том, как обрабатывается состояние. Во-первых, у классов у нас один объект состояния. Мы должны были запихать туда все, что угодно. В хуках же мы можем разбить логическое состояние на какие-то куски, которыми нам удобно было бы оперировать раздельно.

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

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

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

В общем, вы вряд ли получите ответ, переезжать или нет. Но я советую хотя бы попробовать, хотя бы для нового кода, чтобы прочувствовать. Когда я только начал работать с хуками, то сразу же выделил несколько кастомных хуков, удобных для себя, для своего проекта. В принципе, я пытался заменить некоторые возможности, которые у меня были реализованы через Higher Order Components.



useDismounted — для тех, кто знаком с RxJS, там есть возможность массово отписаться от всех Observable в рамках одного компонента, или в рамках одной функции, передав подписку каждого Observable в специальный объект, Subject, и когда он закрывается, все подписки отменяются. Это очень удобно, если компонент сложный, если там внутри много каких-то асинхронных операций через Observable, удобно сразу разом от всех отписываться, а не от каждого отдельно.

useObservable возвращает значение из Observable, когда оно там появится новое. Похожий хук useBehaviourSubject возвращает из BehaviourSubject. Его отличие от Observable в том, что у него изначально есть какое-то значение.

Удобный кастомный хук useDebouncedValue позволяет нам организовать, например, саджест для поисковой строки, чтобы не на каждое нажатие клавиши что-то отправлять на сервер, а подождать, пока пользователь закончит вводить.

Два похожих хука. useWindowResize возвращает текущие актуальные значения для размеров окна. Следующий хук для позиции скролла — useWindowScroll. Я их использую для пересчета некоторых pop-up или модальных окон, если там какие-то сложные штуки, которые с CSS просто так не делаются.

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

В чем удобны вообще вот эти кастомные хуки? В том, что мы можем внутрь хука запихать отписку, и нам можно не думать о том, чтобы отписываться вручную где-то в том компоненте, где этот хук используют.

Не так давно мне кинули ссылочку на библиотеку react-use, и оказалось, что большая часть вот этих кастомных хуков там уже реализовано. И я писал велосипед. Это иногда полезно, но в дальнейшем, наверное, скорее всего, я их выкину, и использую react-use. И вам советую тоже посмотреть, если собираетесь использовать хуки.



Собственно, главная цель доклада — показать то, как неправильно писать, какие проблемы могут быть и как их избежать. Самое первое, наверное, с чем сталкивается любой, кто изучает эти хуки и пытается что-то написать, это использовать useEffect неправильно. Здесь тот код, подобный которому 100% каждый писал, если пробовал хуки. Он связан с тем, что useEffect поначалу воспринимается ментально, как альтернатива componentDidMount. Но, в отличие от componentDidMount, который вызывается единожды, useEffect вызывается при каждом рендере. И ошибка здесь в том, что он изменяет, допустим, перемену data, и при этом ее изменение приводит к ререндеру компонента, как следствие, эффект будет перезапрошен. Тем самым мы получаем бесконечную череду AJAX запросов на сервер, и компонент сам себя постоянно обновляет, обновляет, обновляет.



Исправить его очень просто. Нужно вот сюда добавить пустой массив тех зависимостей, от которых он зависит, и изменения в которых будут перезапускать эффект. Если у нас здесь указан пустой список зависимостей, то эффект, соответственно, не будет перезапускаться. Это не какой-то хак, это базовая особенность, что ли, использования useEffect.



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



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



Более сложный пример с useCallback. Здесь, на первый взгляд, все хорошо. Мы имеем некую страничку, у которой есть какой-то countdown timer, или, наоборот, timer просто, который тикает. И, например, список хостов, и сверху фильтры, которые позволяют этот список хостов фильтровать. Ну, maintenance здесь добавлен просто как для иллюстрации часто изменяющегося значения, который переводит к рирендеру.

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



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

Есть еще некоторые сложности, которые я выделил. Они, возможно, больше мои. То есть эти сложности отвергает Facebook, как создатель React. Они говорят, что, типа, все хорошо, все о'кей. Но все равно некое смущение, что ли, или confusing бывает.



Что сразу бросается в глаза? Так как у нас теперь функциональный компонент — это не просто кусок рендера, а есть какая-то логика и, возможно, внутри указана куча всяких функций, то все эти функции и объекты, скорее всего, будут использованы только один раз или редко когда меняться. Но при этом инициализироваться они будут на каждый рендер.

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

Еще есть такая проблема, она очень нервирует что ли, или вызывает фрустрацию. Когда у нас меняется какое-то значение, например, мы делаем setValue для какого-то значения, то мы не можем в хуках подписаться на изменение этого значения напрямую, как в setState можно было раньше вторым аргументом. Нам нужно как-то декларативно подписаться через изменения зависимости у useEffect.

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

И забавная такая, даже не претензия, а замечание. Иногда, когда мы выносим нашу логику в кастомные хуки, нам хочется эту логику переиспользовать. Но мы не можем это сделать в компонентах старых на классах, потому что они не поддерживают хуки. В моем случае это был довольно сложный компонент, куда можно было вбивать данные. Он искал, его можно было выбирать, типа, dropdown такой сложный. Он был большой, давно еще написанный на классах. Я для другого dropdown сделал pop-up, который автоматом использовал хуки useWindowScroll, useWindowResize и пересчитывал свой размерчик в зависимости от высоты экрана, очень удобно. Я хотел эту же логику использовать для этого компонента, который у меня сложный на классах, — не вышло, пришлось его переписывать.

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



Время еще есть, и есть небольшой раздел «Бонус», связанный с хуками. Я, как и многие в Яндексе, пишу на TypeScript и использую строгую типизацию. Есть определенная проблема в использовании редьюсеров. Она заключается в том, что reducer в Redux когда, он принимает объект action. Так вот, структура этого action может быть совершенно разной, в зависимости от типа action. И сделать так, чтобы работал автокомплит, и другие плюшки статической типизации, довольно сложно.

Я нашел следующий способ. Прежде всего, мы объявляем все перечисления со всеми типами action. Можем сделать вот так просто, тогда это будут числа просто, IncrementA это 0, потом 1, 2, и так далее. Можем для удобства отладки указать их в виде строк. Естественно, они не должны пересекаться, но, в принципе, вам компилятор не даст сделать одинаковые строки. Потом для каждого типа action мы описываем структуру этого action, расширяя его какими-то полями и обязательно указывая тип. Потом создаем UnionType “Action”, где мы перечисляем через вертикальную черту все типы данных, которые туда складываются, какими могут быть action. Это первый трюк.

Второй трюк — вычисление типа на основе значения. Например, здесь есть initialState, просто объект. И по идее, надо описать структуру этого объекта еще каким-нибудь одним интерфейсом. Но можно переложить эту задачу на TypeScript. Он умеет сам вычислять значения. Например, здесь я определяю typeState только для чтения, вычисляемый из initialState.



Дальше эти типы легко использовать в reducer. Мы передаем State, Action, и дальше происходит магия: мы используем switch по action.type. В TypeScript есть такая фича при использовании UnionType: он может вычислить допустимый тип внутри case, если эти типы отличаются значением какого-то поля, в нашем случае type. И мы как раз получаем возможность обращаться к полям этого action нужного нам типа.

Естественно, это удобно: прежде всего, это валидация по типам, подсказки автокомплита и все такое. И рефакторинг.



Как это применять? В принципе, при использовании особой разницы нет. Был предыдущий пример с небольшим калькулятором. Здесь мы делаем то же самое, только через reducer. Точнее, мы определяем action creator с типами, которые нам нужны, и потом просто отправляем их в dispatch.



Еще хочу пропагандировать использование extension официального реактовского Dev Tools. Они добавили поддержку хуков и позволяют очень классно показывать все дерево хуков. Особенно полезно для кастомных.

Справа пример, скриншот того, как он это показывает. Это маленький компонент, у него несложное дерево хуков, но бывают деревья и посложнее. Специальный необходимый для этого хук useDebugValue нужен, чтобы в кастомных хуках можно было добавить какую-нибудь подпись в этот Dev Tool. В данном случае у нас есть хук useConstants, и мы внутри него можем определить какую-то метку, loaded, которая определяет, загрузились ли с сервера константы, например словари.



В заключение — некоторые выводы. Прежде всего, хуки дают довольно большие возможности. С большими возможностями приходит ответственность, и я призываю вас использовать их с осторожностью. Нужно довольно хорошо понимать, как они работают, особенно то, как они мемоизируют значения. Потому что без мемоизации никуда, мы будем оптимизировать, и тут можно очень сильно себе выстрелить в ногу — мы неправильно что-то замемоизируем и оно будет хранить неактуальные значения, либо наоборот — ререндериться слишком часто.

Очень важная рекомендация. У Facebook есть специальный плагин для ESLint, который позволяет нам избежать большинства перечисленных проблем. Он отслеживает правильное использование хуков и, прежде всего, помогает. Он даже предлагает варианты, как прописать dependencies для хуков. Он их сам может прописать, и это очень удобно, всегда следует обращать внимание, на что там линтер ругается.

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

И самое последнее — в подходе к написанию компонентов с хуками, наверное, заложена какая-то философия. Там явно чувствуется, что люди продумывали парадигму, и ее надо постичь. А это приходит только с опытом, поэтому надо пробовать, делать какие-то выводы и постепенно к ним привыкать. Возможно, в дальнейшем можно познать дзен и писать уже суперэффективный классный код на хуках. Но пока не все так просто. Полезные ссылки:

  • «React hooks — победа или поражение?» Cтатья очень перекликается с моим докладом. Я про нее узнал уже после того, как доклад был готов, но добавил просто потому, что там мысли совпадают с моими, а это всегда приятно.
  • Полное руководство по useEffect. Переводная статья, тоже на Хабре, там прямо досконально по полочкам разложено, как это работает, как его использовать, как не ошибаться и т. д.
  • Статья «useReducer vs useState in React» объясняет, когда нужно использовать useReducer, а когда можно обойтись простым useState. Спойлер: если у вас сложный стейт, особенно поля, зависимые друг от друга, то лучше использовать useReducer. Если у вас два-три значения, то обычного useState хватит с головой.
  • Сайт React Hooks CheatSheets c множеством интерактивных примеров для изучения хуков.
  • Библиотеки кастомных хуков. Usehooks.com — это просто сайт, блог. Там люди выкладывают свои хуки. И я уже упоминал react-use — библиотечку, содержащую полезные для большинства проектов кастомные хуки, которых еще нет из коробки.

Комментарии (65)


  1. vintage
    18.08.2019 15:00

    image


    Простейший всё же выглядел бы так:


    @Observer
    export class Counter extends Component {
    
        @observable counter = 0
    
        render() { return <>
            <h2>Counter: {this.counter}</h2>
            <button onclick={ ()=> this.counter ++ }>+1</button>
            <button onclick={ ()=> this.counter -- }>-1</button>
        </>}
    
    }


    1. MaZaAa
      18.08.2019 20:41
      -2

      что за за «умники» заминусовали? Наверное те, которые застряли в своем мирке и даже не знают что такое MobX


      1. justboris
        18.08.2019 21:22
        +7

        Использование дополнительной библиотеки – это уже не простейший пример по определению (минусы не мои).


        1. vintage
          18.08.2019 22:41

          Вы, конечно же, говорите про React?


        1. Alexufo
          18.08.2019 23:54

          Только со временем бывает понятно- это доп библиотека или будущий дизайн языка, принятый сообществом. Js как вы понимаете, эксперимент-размышление о будущем.


    1. KhodeN Автор
      18.08.2019 21:29
      +5

      Декораторы еще даже не JS, это экспериментальный синтаксис, хоть и с большой перспективой войти в стандарт. И да, примеры все-таки на чистом React.


      1. vintage
        18.08.2019 22:43

        Вам шашечки или ехать?


        1. justboris
          19.08.2019 00:27

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


          1. vintage
            19.08.2019 07:47
            -1

            Вы, конечно же, говорите про переписывание компонент с нуля, используя на этот раз react-hooks, так как lifecycle-hooks идут в топку?


            1. justboris
              19.08.2019 08:40

              Ничего в топку не идет, не нужно перегибать. Документация так и говорит:


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

              Мы не планируем удалять классы из React. Вы можете прочитать больше о стратегии постепенного внедрения хуков в разделе ниже.

              А вот с декораторами такой номер не пройдет. Это часть языка, просто так переключаться между API не получится.


              В феврале этого года выпустили новую версию стандарта, совсем не совместимую с тем что было раньше и поддерживается в typescript или babel.


              Определение декоратора в новом стандарте:


              export decorator @bound {
                @initialize((instance, name) => instance[name] = instance[name].bind(instance))
              }

              раньше декоратор был просто функцией


              function bound(instance, name, descriptor) {
                instance[name] = instance[name].bind(instance);
              }

              Как вы собираетесь переиспользовать существующий код в такой ситуации?


              1. vintage
                19.08.2019 09:19

                Речь про хуки, а не про классы: https://ru.reactjs.org/docs/react-component.html#unsafe_componentwillmount
                Впрочем, всё идёт к тому, что и классы вскоре задепрекейтят.


                Ну и по секрету вам скажу, что typescript — это не javascript, а лишь немного похожий на него язык. Ему ничего не мешает поддерживать оба синтаксиса.


                1. justboris
                  19.08.2019 09:53

                  Из-за одного deprecated метода переписывать все целиком нерационально. Хуки здесь не причём


                  Ему ничего не мешает поддерживать оба синтаксиса

                  Я ваш оптимизм не разделяю. В Babel уже есть переключатель между двумя ныне устаревшими версиями, новая редакция добавит ещё один режим. Как писать в таких условиях переиспользуемые библиотеки — категорически непонятно


              1. MaZaAa
                19.08.2019 09:36

                Легко, Babel и TS будут поддерживать и тот и другой вариант. Это же очевидно.


                1. justboris
                  19.08.2019 09:58

                  А для меня вот совсем неочевидно. Есть конструкция:


                  @observer
                  class Example {}

                  Это в новый синтаксис транспилировать или в старый?


                  1. MaZaAa
                    19.08.2019 10:04

                    Без разницы, результат остается прежним, говоришь TS или Babel в какую версию JS код превратить и он это делает, а не вы собственноручно)


                    1. justboris
                      19.08.2019 10:16

                      Хорошо, спрошу по-другому. Результат транспиляции зависит от того, какой версии стандарта придерживается @observer. Typescript эту информацию из типов как-то достанет, а Babel как будет? Он работает с файлами поодиночке, в импорты не смотрит


                      1. MaZaAa
                        19.08.2019 10:20

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


                        1. justboris
                          19.08.2019 10:33

                          С 2015 года вы пользуетесь legacy реализацией, которую из стандарта выпилили. А для актуальной спецификации транспилятор пока никто не написал по причине указанных выше проблем.


                          1. MaZaAa
                            19.08.2019 11:04

                            Не вижу причин не использовать legacy декораторы по сей день, они ещё будут очень долго жить.


    1. Finesse
      19.08.2019 04:23

      Тут в комнату входит Svelte и говорит:


      <script>
        let counter = 0;
      </script>
      
      <h2>Counter: {counter}</h2>
      <button on:click={() => ++counter}>+1</button>
      <button on:click={() => --counter}>-1</button>

      Если использовать дополнительные библиотеки, то использовать по максимуму.


  1. vvadzim
    18.08.2019 17:33
    +1

    image
    Для рабочего кода тут ещё не хватает проверки, что эфект не размонтирован к моменту получения значения от сервера. Вроде такой:

    function Loader({entityId}) {
       const [data, setData] = useState(null);
    
       useEffect(() => {
          let mounted = true;
    
          fetch(`entities/${entiryId}`)
             .then(resp => resp.json())
             .then(entity => { if (mounted) setData(entity); });
    
          return () => { mounted = false };
       }, [entityId]);
    
       return <pre>{JSON.stringify(data)}</pre>;
    }

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

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

    В упомянутой коллекции хуков react-use этот момент учтён.


    1. KhodeN Автор
      18.08.2019 21:24

      Тут все-таки иллюстрируется другая проблема. Зачем усложнять код в примере?


      В боевом коде будет что-то типа:


      const dismounted = useDismounted();
      
      useEffect(() => {
        entitiesApi.getById(entityId)
          .pipe(takeUntil(dismounted))
          .subscribe(setData, toasts.handleApiError('Entity load'));
      }, [entityId])


      1. vvadzim
        18.08.2019 21:55
        +1

        Ну, возможно, я и слишком остро реагирую на код с гонками в примерах ;), но мне эта проблема видится достаточно серьёзной, чтоб пример усложнять.

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

        ПС. Хуки очень люблю несмотря на их проблемы.


      1. vvadzim
        18.08.2019 22:41

        Извините, невнимательно прочитал ваш пример.


        Ваш псевдокод вроде как про размонтирование компонента, а это совсем не то — нужно именно размонтирование эфекта.


        1. KhodeN Автор
          18.08.2019 22:57

          Хм, да, тогда у меня будет что-то типа (это, кстати, приводит к отмене запроса через AbortController):


          const dismounted = useDismounted();
          const loadRef = useRef();
          
          useEffect(() => {
             if (loadRef.current) {
                loadRef.current.unsubscribe();
             }
             loadRef.current = entitiesApi.getById(entityId)
                .pipe(takeUntil(dismounted))
                .subscribe(setData);
          }, [entityId])

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


          1. vvadzim
            18.08.2019 23:03

            Я всё-же не понимаю, как ваш код может работать.


            Задача в том, чтобы при изменении entityId прервать текущий запрос или проигнорировать его результат.


            Мой код эту задачу решает.


            useDismounted в вашем коде мне видится лишним.


            1. vvadzim
              18.08.2019 23:07

              А, dismounted это функция?
              Тогда согласен, работать будет.
              Хотя я не скажу, что ваш код проще. Он отдельно ловит перемонтирование эфекта и отдельно размонтирование компонента — две логики.

              В моем коде обрабатывается только размонтирование эфекта. Мой мне видится проще)


              1. KhodeN Автор
                19.08.2019 10:09

                dismounted это Subject из RxJs, при закрытии которого произойдет автоматическая отписка. А закрывается он внутри кастомного хука useDismounted автоматически при размонтировании компонента.


                Приемущество моего подхода с useDismounted в том, что он позволит отписаться от всех Observables разом, не отписываясь отдельно от каждого.


          1. vvadzim
            18.08.2019 23:14

            Я не силён в rx, но может вот так сработает?

            useEffect(() => {
               const load = entitiesApi.getById(entityId)
                  .subscribe(setData);
               return () => load.unsubscribe();
            }, [entityId])
            

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

            Так и есть, для большинства.
            Но сам бы я не написал и не одобрил такой код в ревью, поскольку он неявно зависит от реализации сервера, сети, частоты кликов юзера и т.д. Исключение — явный комментарий почему именно так.


            1. KhodeN Автор
              19.08.2019 10:10

              Я не силён в rx, но может вот так сработает?

              Хороший вариант.


      1. marshinov
        19.08.2019 08:17

        А вы используете react и rxjs в проде?


        1. KhodeN Автор
          19.08.2019 10:06

          Да, rxjs удобен, позволяет делать куда больше и гибче, чем промисы или async-await. По крайней мере на мой взгляд)
          Например — отменять запросы.


          1. marshinov
            19.08.2019 11:06

            Плюсы мне понятны. Я скорее в том плане, что в ng он идёт из коробки. Интересно, что и в react его используют


            1. KhodeN Автор
              19.08.2019 12:12

              Я перебежчик из мира ng, некоторые концепции крепко засели в голове.


            1. UdarEC
              19.08.2019 19:15
              +1

              Один из самых удобных вариантов использования в реакте — redux-obervable, для асинхорщины и бизнес логики, в частности.


      1. mayorovp
        19.08.2019 10:58

        Комментарий был удалён, потому что за меня уже всё сказали


  1. Druu
    19.08.2019 05:55

    И самое последнее — в подходе к написанию компонентов с хуками, наверное, заложена какая-то философия.

    useState — это di-контейнер, useEffect — реактивность. Но поскольку в фейсбуке проверенные, качественные решения не используют, то контейнер вышел обгрызанным, а реактивность — костыльной. Ну а всем, как водится, придется теперь жрать этот кактус. Вот и вся философия.


    1. vintage
      19.08.2019 07:59

      useState — это скорее аналог статических переменных (не полей класса) в си-подобных языках. К DI оно мало отношения имеет.


      useEffect — это скорее событие onRender, чем реактивность. Для реактивности его приспособить, конечно, можно, но смысла в этом мало.


      1. Druu
        19.08.2019 14:30

        useState — это скорее аналог статических переменных (не полей класса) в си-подобных языках.

        Не, это именно DI. Оно решает в точности те же задачи, что и инжект сервисов. Оно даже реализовано схожим образом — по факту, сейчас в реакте есть контейнер, который менеджит все стейты, и инжектит нужный инстанс стейта в нужный компонент. Он просто DI-контейнером не называется. Единственное, что вы можете сделать с полноценным DI и не можете со стейтом — это, с-но, настроить сам инжект (подменить реализацию или шарить инстансы), но, вангую, что еще через годик выкатят очередный костыль, в котором это будет можно (через ж, конечно, как это и водится у фейсбука).


        1. vintage
          19.08.2019 14:42

          С такой логикой любой стор является DI. Логически это одно и то же:


          const counter = useState(0)
          
          // object field
          counter = new Store(0)
          
          // object field
          counter = 0

          Собственно из-за невозможности настройки, это и никакой не DI.


          1. Druu
            19.08.2019 20:17

            С такой логикой любой стор является DI. Логически это одно и то же:

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


            1. vintage
              20.08.2019 04:42

              С такой логикой new тоже резолвит зависимость для каждого компонента, а Store — фабричный метод, какой объект он вернёт вы тоже знать не можете.


              1. Druu
                20.08.2019 10:31

                С такой логикой new тоже резолвит зависимость для каждого компонента

                Нет, не резолвит.


                а Store — фабричный метод, какой объект он вернёт вы тоже знать не можете.

                В смысле не знаю? Знаю. Но дело не в том, знаю или нет. В случае стора это один конкретный объект, который вы и создали и четко определили. Вы на стороне клиента говорите: "дай мне этот конкретный объект", и вам его дают. Это не DI, т.к. ответственность на клиенте, клиент полностью определяет результат.
                А вот когда вы на стороне клиента говорите: "дай-ка мне какой-нибудь подходящий объект", и уже какая-то внешняя логика решает, какой объект будет "подходящим" в данной ситуации для данного клиента — тогда у вас DI.


                1. mayorovp
                  20.08.2019 11:25

                  Вызов useState даёт вам именно что конкретный объект.


                1. vintage
                  20.08.2019 18:01

                  Ничего ты не знаешь, Джон Druu.


                  function Store() {
                      return { foo : 'bar' }
                  }
                  console.log( new Store )


        1. mayorovp
          20.08.2019 09:03

          Нет, оно решает совершенно другие задачи.


          DI — это про передачу внешних зависимостей, useState — про хранение внутреннего состояния компонента.


          1. Druu
            20.08.2019 11:32

            DI — это про передачу внешних зависимостей, useState — про хранение внутреннего состояния компонента.

            Не так. Хранение состояния — это задача, а DI — способ реализации. Такой задачи как "передача внешних зависимостей" никогда на практике не стоит — нам не надо передавать зависимости в принципе, нам надо какие-то конкретные. И передавать их надо за тем, чтобы решить, с-но, задачу. Т.е. передача зависимостей — не задача, это решение.


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


            Вызов useState даёт вам именно что конкретный объект.

            Какой "конкретный"? Вызывая useState, вы делегируете создание и дальнейший резолв объекта реакту, уже он определяет, какой объект вам выдать, вы на это не влияете никак.
            Мне кажется, вы не совсем понимаете как работает useState.


            1. mayorovp
              20.08.2019 11:38

              Такой задачи как "передача внешних зависимостей" никогда на практике не стоит

              С точки зрения архитектуры, такая задача вполне возможна. Можете считать её мета-задачей, если просто "задача" для вас слишком привязана к чему-то конкретному.


              В данном случае — мы храним состояние в некотором внешнем объекте, который инкапсулирует это состояние, а так же его геттеры/сеттеры.

              Если этот объект создаётся нами по нашему же запросу и нами же используется — это не внешний объект, а внутренний.


              1. Druu
                20.08.2019 15:12

                С точки зрения архитектуры, такая задача вполне возможна. Можете считать её мета-задачей, если просто "задача" для вас слишком привязана к чему-то конкретному.

                Ну так вот у вас "задача" — работа с внешним стейтом, а решение — приводит к "мета-задаче" внедрения этого стейта как зависимости.


                Если этот объект создаётся нами по нашему же запросу и нами же используется — это не внешний объект, а внутренний.

                С DI объекты всегда внедряются по запросу клиента и используются потом клиентом (ну, за тем клиент эти объекты и запрашивает, чтобы их использовать). При этом, заметьте — "внедряются", а не "создаются". И при вызове useState, вы стейт не создаете — вы его внедряете (т.е. просите у useState предоставить объект). А уже сам useState решает — что ему создавать, как создавать, и создавать ли вообще. useState решает, не вы.


                Внутренний объект — это объект, который клиентом создается и за пределы клиента не выходит. Если же объект создается не клиентом, а сторонним сервисом, существует за пределами клиента и менеджится тоже сторонним сервисом, к клиенту никак не относящимся — это объект, определенно, внешний.


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


                Но вы не можете создать этот объект руками, не можете повлиять на его конструкцию, и не можете вмешаться в процесс резолва (ну, опять же, не залезая куда-то в потроха).


                1. mayorovp
                  20.08.2019 15:26

                  Ну так вот у вас "задача" — работа с внешним стейтом, а решение — приводит к "мета-задаче" внедрения этого стейта как зависимости.

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


                  А useState — это внутренний стейт.


                  С DI объекты всегда создаются по запросу клиента

                  Нет


                  Внутренний объект — это объект, который клиентом создается и за пределы клиента не выходит.

                  А куда выходит результат setState, кроме инфраструктурного кода?


                  1. Druu
                    20.08.2019 16:41

                    А useState — это внутренний стейт.

                    Вы не понимаете, как работает useState.


                    Нет

                    Да. Исключений нет. У нас есть три общепринятых способа DI — через конструктор, через поле и через сервис-локатор/фабричный метод. Во всех трех случаях зависимость, очевидно, запрашивается именно клиентом. Вы знаете какие-то еще способы? Вообще, если клиент зависимость не запрашивает — значит, он ее не использует. Ну, т.е., вы можете, формально, что-то всунуть, что клиент не просил, но это что-то будет просто висеть и ничего не делать.


                    А куда выходит результат setState, кроме инфраструктурного кода?

                    Не понял, к чему вы это. Какая разница, куда выходит результат setState (useState, в смысле)?


                    1. mayorovp
                      20.08.2019 17:01

                      Вы не понимаете, как работает useState.

                      А пофигу как оно работает внутри, важно лишь наблюдаемое поведение.


                      Рассмотрим два компонента:


                      function Foo() {
                          const [ count, setCount ] = useState(0);
                      
                          return <div>
                              <span>count = {count}</span>
                              <button onClick={() => setCount(count+1)}>Increment</button>
                          </div>;
                      }
                      
                      function Bar({count, setCount}) {
                          return <div>
                              <span>count = {count}</span>
                              <button onClick={() => setCount(count+1)}>Increment</button>
                          </div>;
                      }

                      Так вот, у Foo внутреннее состояние, а у Bar — внешнее. Эти термины были придуманы специально для того, чтобы отличать Foo от Bar.


                      Во всех трех случаях зависимость, очевидно, запрашивается именно клиентом.

                      Во всех трёх случаях зависимость клиенту интересна независимо от её уникальности или времени жизни (важно лишь чтобы она не жила меньше чем сам клиент).


                      В случае с useState это не так.


                      1. justboris
                        20.08.2019 17:40
                        +1

                        Краткое содержание треда: Druu пытается натянуть концепции DI на useState, а потом удивляется, что ж все так неудобно работает


                      1. Druu
                        20.08.2019 18:04
                        -1

                        А пофигу как оно работает внутри, важно лишь наблюдаемое поведение.

                        Я про него и говорю. useState вам отдает внешнюю зависимость.


                        Так вот, у Foo внутреннее состояние, а у Bar — внешнее.

                        Что? Как раз нет. Вы просто продемонстрировали два разных способа DI. С-но, никаких различий, кроме удобства, в этих способах нет, они эквивалентны по тому самому наблюдаемому поведению. Просто внедрение по аргументу в реакте неудобно (придется этот аргумент везде таскать).


                        Вообще, не поделитесь своей логикой? Каким образом вы назвали внутренней зависимость, которую достали из внешнего глобального объекта?


                        Внутренней зависимость будет, если вы ее создадите внутри компонента. Если же зависимость создает неизвестная и неконтролируемая вами магия в произвольный (вам неизвестный) момент времени — чего тут внутреннего? За внутреннюю зависимость отвечает сам клиент. В данном же случае клиент за зависимость не отвечает и никак ей не управляет. Она существует полностью независимо от клиента и связана с ним, с-но, только через внешний сервис (useState). Вы же не станете утверждать, что сервис useState является тоже внутренним?


                        Во всех трёх случаях зависимость клиенту интересна независимо от её уникальности или времени жизни

                        В DI клиенту как раз очень часто важна и уникальность и время жизни.


                        В случае с useState это не так.

                        Не так что?


                        1. mayorovp
                          20.08.2019 18:46

                          Если же зависимость создает неизвестная и неконтролируемая вами магия в произвольный (вам неизвестный) момент времени

                          Не в произвольный, а в момент первого вызова useState каждым экземпляром компонента.


                          Она существует полностью независимо от клиента

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


                          1. Druu
                            21.08.2019 11:20

                            Нет, она создаётся при первом рендере экземпляра компонента (ведь раньше Реакт просто не знает про неё), и уничтожается вместе с ним

                            Это ваши домыслы о деталях реализации, которые вам никто не гарантирует. Точно так же он мог бы создаваться ДО первого рендера (при помощи статического анализа, например), создаваться во время каждого рендера и уничтожаться после его окончания и т.д..


                            И вы со стороны клиента никакие эти факторы никак не контролируете. Вы даже не можете быть уверены, что все на самом деле так, как вы говорите


                            hint: а оно в действительности и не так, вы с вашими домыслами ошиблись — стейт принадлежит файберу, а не компоненту, так что у вас на каждый компонент может быть несколько копий стейта, время жизни каждой из которых определяется временем жизни файбера.


                            Я вам больше скажу — перерасчет стейта, например, происходит не при вызове setSomething, а при вызове самого useState (которое на самом деле обертка над useReducer).


                            1. mayorovp
                              21.08.2019 12:12

                              Точно так же он мог бы создаваться ДО первого рендера (при помощи статического анализа, например)

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


                              создаваться во время каждого рендера и уничтожаться после его окончания

                              Нет, этот вариант нарушит работу компонента, а значит в корректной реализации он невозможен.


                              так что у вас на каждый компонент может быть несколько копий стейта, время жизни каждой из которых определяется временем жизни файбера

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


                              1. Druu
                                21.08.2019 12:30
                                -1

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

                                Ну как не может? Может.


                                Нет, этот вариант нарушит работу компонента, а значит в корректной реализации он невозможен.

                                Нет, не нарушит.


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

                                Это верно для всего. С точки зрения работы стейта вообще нет никаких компонент. И не важно как и куда они попали. Есть только файберы и стейт хранится только в файберах.


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


                                Т.е., еще раз — вы стейт не создаете, и у вас нет никакого контроля над тем, как он создается. АПИ которое вам предоставляет useState — это просто вернуть текущее значение значение стейта и ф-ю диспатч, которая все что делает — добавляет экшн в очередь файбера.


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


                                1. mayorovp
                                  21.08.2019 12:35

                                  Нет, не нарушит.

                                  Каким образом оно не нарушит?


                                  После нажатия на кнопку компонент Foo должен отобразить "count = 1". Если состояние будет удалено — он отобразит "count = 0". Это что, эквивалентное поведение?


                                  Это верно для всего. С точки зрения работы стейта вообще нет никаких компонент.

                                  А мне не важна точка зрения стейта, мне важна точка зрения компонента.


                                  1. Druu
                                    21.08.2019 13:55

                                    Каким образом оно не нарушит?

                                    Ни каким не нарушит.


                                    Если состояние будет удалено — он отобразит "count = 0".

                                    Он отобразит нужное состояние.


                                    А мне не важна точка зрения стейта, мне важна точка зрения компонента.

                                    Ну так с точки зрения компонента для вас не важна ни уникальность ни время жизни. Вам важно, чтобы useState вам зарезолвил правильный диспатч — это все, больше useState ничего вам не должен и ничего не гарантирует. А ваши домыслы о реализации (некорректные) — ни на что не влияют.


                                    1. mayorovp
                                      21.08.2019 14:15

                                      Он отобразит нужное состояние.

                                      Как, если оно было удалено после первого рендера?


                                      Вам важно, чтобы useState вам зарезолвил правильный диспатч

                                      Вот в понятие "правильности" тут как раз и входит, в том числе, правильное время жизни.


                                      1. Druu
                                        21.08.2019 18:18
                                        -1

                                        Как, если оно было удалено после первого рендера?

                                        Из замыкания возьмет.


                                        Вот в понятие "правильности" тут как раз и входит, в том числе, правильное время жизни.

                                        Так в том и дело что не входит. И в реальности время жизни стейта как раз не соответствует времени жизни компонента. Но вы этого не замечаете, т.к. это ни на что не влияет. Это детали реализации. Вам зависимость внедрили — вы с ней работаете, остальное вас не волнует.


                                        1. mayorovp
                                          21.08.2019 19:20

                                          Из замыкания возьмет.

                                          Если оно лежит в каком-то замыкании и косвенно доступно — оно не удалено.



  1. artemir
    20.08.2019 08:08

    Уже забыл, что классовые компоненты существуют :/