Хуки: ответы на вопросы¶
Хуки — нововведение в React 16.8, которое позволяет использовать состояние и другие возможности React без написания классов.
На этой странице вы найдёте ответы на популярные вопросы о хуках.
- Внедрение хуков
- В какой версии React появились хуки?
- Нужно ли переписать все мои классовые компоненты?
- Что можно сделать с помощью хуков, чего невозможно добиться, используя классы?
- Какая часть моих знаний о React всё ещё актуальна?
- Что мне использовать: хуки, классы или оба подхода?
- Дают ли хуки все возможности классов?
- Являются ли хуки заменой рендер-пропсам и компонентам высшего порядка?
- Как хуки повлияют на популярные API, такие как Redux
connect()
и React Router? - Поддерживают ли хуки статическую типизацию?
- Как тестировать компоненты, которые используют хуки?
- Что конкретно проверяют правила линтера в хуках?
- От классов к хукам
- Как методы жизненного цикла соответствуют хукам?
- Как осуществлять запросы данных с помощью хуков?
- Существует что-нибудь наподобие полей экземпляра?
- Сколько переменных состояния я могу использовать – одну или несколько?
- Могу ли я использовать эффект только на обновлениях компонента?
- Как получить предыдущие пропсы или состояние?
- Почему я вижу устаревшие пропсы или состояние в моей функции?
- Как я могу реализовать
getDerivedStateFromProps
? - Существует что-нибудь наподобие forceUpdate?
- Могу ли я изменить реф, переданный в функциональный компонент?
- Как я могу измерить узел DOM?
- Что значит
const [thing, setThing] = useState()
? - Оптимизации производительности
- Могу ли я пропустить эффект при обновлениях?
- Безопасно ли не указывать функции в списке зависимостей?
- Что делать, если зависимости эффекта изменяются слишком часто?
- Как я могу реализовать
shouldComponentUpdate
? - Как закешировать вычисления?
- Как лениво создавать большие объекты?
- Являются ли хуки медленными из-за создания функций на каждом рендере?
- Как избежать передачи колбэков вниз?
- Как получить часто изменяемое значение из хука
useCallback
? - Под капотом
- Как React связывает вызовы хуков с компонентом?
- Что послужило прообразом хуков?
Внедрение хуков¶
В какой версии React появились хуки?¶
Начиная с релиза 16.8.0, React включает в себя стабильную реализацию хуков для:
- React DOM
- React DOM Server
- React Test Renderer
- React Shallow Renderer
Обратите внимание, что хуки будут доступны, только если все React-пакеты версии 16.8.0 или выше. Хуки не будут работать, если вы, например, забыли обновить React DOM.
Поддержка хуков в React Native добавится в следующем стабильном релизе.
Надо ли переписать все мои классовые компоненты?¶
Нет, мы не собираемся удалять классы из React, поскольку никто не может позволить себе такой глобальный рефакторинг. Мы советуем пробовать хуки только в новом коде.
Что можно сделать с помощью хуков, чего невозможно добиться, используя классы?¶
Хуки дают новый мощный способ повторного использования кода в компонентах. «Создание пользовательских хуков» даёт представление об их возможностях. Эта статья, написанная одним из участников команды React, описывает новые возможности, которые открывают хуки.
Какая часть моих знаний о React всё ещё актуальна?¶
Хуки — это новый способ использовать возможности React, которые вы уже знаете: состояние, жизненный цикл, контекст и рефы. Хуки не изменили фундаментальную логику React, поэтому ваши знания компонентов, пропсов и нисходящего потока данных остаются актуальными.
Однако, хуки — не самая простая часть React. Если вам чего-то не хватает в этой документации, дайте нам знать, и мы постараемся вам помочь.
Что мне использовать: хуки, классы или оба подхода?¶
Мы рекомендуем начать с использования хуков в новых компонентах, когда вы будете готовы. Убедитесь, что каждый человек в вашей команде поддерживает вас и знаком с документацией. Мы не рекомендуем переписывать все существующие классы на хуки, пока нет необходимости возвращаться к определённому компоненту (например, чтобы исправить баг).
Вы не можете использовать хуки внутри классового компонента, но вы можете комбинировать классы и функциональные компоненты с хуками в одном дереве. Является ли компонент классовым или функциональным с использованием хуков — неважно, это всего лишь особенность реализации. Мы ожидаем, что в будущем хуки станут главным способом написания React-компонентов.
Дают ли хуки все возможности классов?¶
Наша главная задача, чтобы хуки покрывали всю функциональность классов в ближайшем будущем. Пока не существует хуков, реализующих методы жизненного цикла getSnapshotBeforeUpdate
и componentDidCatch
, но мы планируем скоро их добавить.
Хуки появились совсем недавно, и некоторые сторонние библиотеки могут быть ещё не совместимы с ними.
Являются ли хуки заменой рендер-пропсам и компонентам высшего порядка?¶
Как правило, рендер-пропсы и компоненты высшего порядка рендерят только один дочерний компонент, увеличивая вложенность в дереве, но добавляя потомку новую функциональность. Мы думаем, что хуки — более простой способ сделать это. Оба подхода всё ещё имеют право на жизнь (например, компонент, следящий за скроллом, может иметь проп renderItem
или компонент-контейнер может иметь разные рендер-пропсы для различных слотов). Но в большинстве случаев хуки должны помочь уменьшить вложенность компонентов в вашем дереве.
Как хуки повлияют на популярные API, такие как Redux connect()
и React Router?¶
Вы можете продолжить использовать тот же самый API, который вы использовали – всё продолжит работать как прежде.
В будущем новые версии этих библиотек могут добавить специальные хуки, например useRedux()
или useRouter()
, которые позволят вам использовать ту же функциональность без необходимости оборачивать компоненты.
Поддерживают ли хуки статическую типизацию?¶
Хуки спроектированы с учётом статической типизации. Так как они являются функциями, их легче правильно типизировать чем, например, компоненты высшего порядка. Последние версии определений типов React для Flow и TypeScript поддерживают хуки.
Заметьте, что пользовательские хуки дают вам возможность накладывать ограничения на API React, если вы хотите типизировать их более строго каким-то образом. React предоставляет примитивы, которые вы можете комбинировать по своему усмотрению даже такими способами, которые и не были изначально предусмотрены.
Как тестировать компоненты, которые используют хуки?¶
С точки зрения React, компонент, использующий хуки, является обычным компонентом. Если ваш способ тестирования не опирается на внутреннюю реализацию React, тестирование компонентов с хуками не должно отличатся от тестирования других компонентов.
Например, у нас есть следующий компонент-счётчик:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Мы будем тестировать этот компонент с помощью React DOM. Чтобы убедиться, что поведение совпадает с тем, что случится в браузере, мы обернём код рендера и обновления в вызов ReactTestUtils.act()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Вызовы act()
также запустят эффекты внутри.
Если вам надо протестировать пользовательский хук, вы можете создать компонент в ваших тестах и использовать в нём этот хук. После этого вы можете протестировать сам компонент.
Для уменьшения однотипного кода, мы советуем использовать библиотеку react-testing-library
. Она была создана для написания тестов, использующих ваши компоненты и имитирующих поведение пользователей в браузере.
Что конкретно проверяют правила линтера в хуках?{#what-exactly-do-the-lint-rules-enforce}¶
Мы предоставляем плагин к ESLint, который принуждает следовать правилам хуков, чтобы избежать ошибок. Подразумевается, что любая функция, имя который начинается с «use
» и заглавной буквы далее, является хуком. Мы понимаем, что это предположение не идеальное, и могут случаться ложные срабатывания. Однако, без этого важного соглашения, было бы невозможно заставить хуки работать хорошо, а более длинные имена могли бы помешать людям начать использовать хуки или следовать соглашению.
Правила линтера проверяют, что:
- Вызовы хуков происходят либо внутри функции с именем, написанном в стиле
PascalCase
(подразумевается, что это компонент), либо внутри другой функцииuseSomething
(подразумевается, что это пользовательский хук). - Хуки вызываются в одном и том же порядке при каждом рендере.
Существует ещё несколько предположений, которые могут измениться по мере того, как мы будем регулировать правила и искать баланс между нахождением ошибок и ложными срабатываниями.
От классов к хукам¶
Как методы жизненного цикла соответствуют хукам?¶
constructor
: Функциональному компоненту не нужен конструктор. Вы можете инициализировать состояние, используя вызовuseState
. Если вычисления состояния затратны, вы можете передать функцию вuseState
.getDerivedStateFromProps
: Запланировать обновление при рендере.shouldComponentUpdate
: Смотрите объяснениеReact.memo
ниже.render
: Это тело функционального компонента.componentDidMount
,componentDidUpdate
,componentWillUnmount
: ХукuseEffect
заменяет все их комбинации (включая более редкие случаи).componentDidCatch
иgetDerivedStateFromError
: В данный момент не существует хуков-аналогов для этих методов, но они будут скоро добавлены.
Как осуществлять запросы за данными с помощью хуков?¶
Посмотрите небольшое демо, а затем ознакомьтесь со статьёй, которая рассказывает как делать запросы данных с помощью хуков.
Есть ли что-то вроде переменных экземпляра?¶
Да! Хук useRef()
может использоваться не только для DOM-рефов. Реф – это общий контейнер, а его свойство current
— изменяемое и может хранить любое значение, подобно свойству экземпляра класса.
Вы можете использовать его внутри useEffect
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Для объявления интервала нам не понадобится реф (id
может быть локальным относительно эффекта). Но это будет полезно, если потребуется сбросить интервал из обработчика события:
1 2 3 4 5 |
|
В целом вы можете рассматривать рефы как свойства экземпляра класса. Старайтесь избегать установки рефов во время рендера, пока вы не реализуете ленивую инициализацию, так как это может привести к неожиданному поведению. Как правило, вы будете изменять рефы в обработчиках событий и эффектах.
Сколько переменных состояния я могу использовать – одну или несколько?¶
Если вы привыкли писать классовые компоненты, вы скорее всего захотите вызывать useState()
однократно и сразу сохранить всё состояние в одном объекте. Да, вы можете так сделать. Взгляните на пример компонента, который следует за движением курсора. Мы храним позицию и размер этого компонента в локальном состоянии.
1 2 3 4 5 6 7 8 9 |
|
Допустим, мы хотим написать некоторую логику, которая будет изменять значения left
и top
, когда пользователь двигает курсор мышки. Обратите внимание, что мы обязаны объединить эти поля с предыдущим объектом состояния вручную:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Код написан так, потому что мы обновляем переменную состояния, заменяя её значение. Данное поведение отличается от this.setState
, ведь этот классовый метод объединяет изменённые поля с текущим объектом состояния.
Если вам не хватает автоматического объединения состояния, вы можете написать свой хук useLegacyState
, который объединяет обновления состояния объекта. Однако, вместо этого мы рекомендуем разделять состояние на несколько переменных, учитывая, какие свойства логически относятся друг к другу и будут изменяться вместе.
Например, мы можем разделить состояние нашего компонента на объекты position
и size
, что поможет всегда изменять только position
, без необходимости волноваться об объединении:
1 2 3 4 5 6 7 8 9 |
|
В разделении состояния на независимые переменные есть ещё одно преимущество. Это поможет легко вынести некоторую логику в отдельный хук в будущем, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Обратите внимание, как мы смогли вынести вызов useState
, изменяющий position
, и соответствующих эффект в отдельный хук, практически не меняя их код. Если бы всё состояние хранилось в одном объекте, сделать это перемещение было бы на порядок сложнее.
На самом деле, оба варианта имеют право на жизнь. Однако, компоненты будут более читаемыми, если вы найдёте баланс между двумя подходами и будете группировать связанные друг с другом данные. Если работа с состоянием становится сложной, мы советуем использовать редюсер или выносить логику в отдельные хуки.
Могу ли я использовать эффект только на обновлениях компонента?¶
Это довольно редкий случай. Вы можете использовать изменяемый реф, чтобы вручную хранить логическое значение, показывающее, прошёл ли первый рендер. В свою очередь эффект может опираться на это значение. Если вы замечаете, что эта логика нужна вам часто, вы можете вынести её в отдельный хук.
Как получить предыдущие пропсы или состояние?¶
Сейчас, вы можете сделать это вручную, используя реф:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Это может показаться усложнённым, но вы можете вынести эту логику в отдельный хук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Обратите внимание, как это будет работать для пропсов, состояния или любого другого вычисляемого значения.
1 2 3 4 5 6 |
|
Возможно, в будущем API React введёт хук usePrevious
, так как он требуется довольно часто.
Также смотрите рекомендованный паттерн для производного состояния.
Почему я вижу устаревшие пропсы или состояние в моей функции?¶
Любая функция внутри компонента, включая эффекты и обработчики событий, «видит» пропсы и состояние из функции render()
, в которой она была создана. Рассмотрим такой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Если вы сперва кликните по "Показать предупреждение", а потом увеличите счётчик, то предупреждение отобразит значение переменной count
на момент нажатия по кнопке "Показать предупреждение". Такое поведение предотвращает баги в коде, который предполагает, что пропсы и состояние не меняются.
Если вы намеренно хотите считать из асинхронного колбэка свежайшее состояние, вы можете сперва сохранить его в реф, потом изменить его и затем считать его из рефа.
Наконец, возможна другая ситуация, почему вы видите устаревшие пропсы или состояние: когда вы используете оптимизацию с помощью «массива зависимостей», но неправильно указали какие-то зависимости. Например, если эффект передаёт вторым параметром []
, но при этом использует someProp
, то он продолжит «видеть» исходное значение someProp
. Правильным решением является либо исправление массива, либо отказ от его использования. По этим ссылкам описаны подходы к функциям в аналогичных ситуациях и другие известные способы снижения частоты вызова эффектов, исключающие передачу неправильных зависимостей.
Примечание
Мы предоставляем правило
exhaustive-deps
в нашем пакете линтераeslint-plugin-react-hooks
. Оно предупреждает о неправильно указанных зависимостях и рекомендует их исправить.
Как я могу реализовать getDerivedStateFromProps
?¶
Несмотря на то, что вам скорее всего это не нужно, в редких случаях (например, для реализации компонента <Transition>
), вы можете изменять состояние прямо во время рендера. React моментально сделает повторный рендер компонента с обновлённым состоянием сразу после первого рендера, и это не будет затратно.
В этом примере, мы храним предыдущее значение пропа row
в состоянии, что позволяет сравнить значения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Сперва это может показаться странным, но изменение во время рендера – это именно то, чем всегда концептуально являлся метод getDerivedStateFromProps
.
Существует что-нибудь наподобие forceUpdate?¶
Оба хука useState
и useReducer
игнорируют обновления, если следующее значение равно предыдущему. Мутация состояния и вызов setState
не приведут к повторному рендеру.
Обычно, вы не должны мутировать внутреннее состояние в React. Однако, в качестве лазейки, вы можете использовать увеличивающийся счётчик, чтобы заставить компонент повторно рендериться, если состояние не изменилось:
1 2 3 4 5 |
|
Старайтесь избегать этого подхода, если возможно.
Могу ли я изменить реф, переданный в функциональный компонент?¶
Несмотря на то, что вам не понадобится это часто, вы можете предоставить некоторые императивные методы родительскому компоненту, используя хук useImperativeHandle
.
Как я могу измерить узел DOM?¶
Для определения положения или размера DOM-узла можно использовать колбэк-реф. React будет вызывать этот колбэк всякий раз, когда реф привязывается к другому узлу. Вот небольшая демонстрация:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Мы не выбрали useRef
в этом примере, потому что объект рефа не уведомляет нас об изменениях текущего значения рефа. Использование колбэк-рефа гарантирует, что даже если дочерний компонент отображает измеренный узел позже (например, в ответ на клик), мы по-прежнему получаем уведомление об этом в родительском компоненте и можем обновлять измерения.
Обратите внимание, что мы передаём []
как массив зависимостей в useCallback
. Это гарантирует, что наш колбэк-реф не изменится между повторными рендерами, а значит React не будет вызывать его без необходимости.
При желании вы можете извлечь эту логику в повторно используемый хук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Что значит const [thing, setThing] = useState()
?¶
Если вы не знакомы с этим синтаксисом, ознакомьтесь с объяснением в документации хука состояния.
Оптимизации производительности¶
Могу ли я пропустить эффект при обновлениях?¶
Да. Ознакомьтесь с условным вызовом эффектов. Обратите внимание, что забыв обработать обновления, вы зачастую можете создать ошибки. Поэтому это и не является поведением по умолчанию.
Безопасно ли не указывать функции в списке зависимостей?¶
Вообще говоря, это небезопасно.
1 2 3 4 5 6 7 8 9 |
|
Сложно помнить, какие пропсы и состояние используются функцией определённой вне эффекта. Именно поэтому, лучше объявлять функции нужные эффекту внутри него. Тогда легче увидеть, от каких значений из области видимости компонента зависит эффект:
1 2 3 4 5 6 7 8 9 |
|
Если после такого изменения мы видим, что никакие значения из области видимости компонента не используются, то можно безопасно указать []
:
1 2 3 4 5 6 7 8 |
|
В зависимости от ситуации, может пригодиться один из вариантов, описанных ниже.
Примечание
Мы предоставляем правило
exhaustive-deps
в пакете нашего линтераeslint-plugin-react-hooks
. Оно поможет выявлять компоненты, не обрабатывающие обновления должным образом.
Давайте разберёмся, почему это важно.
Когда вы указываете список зависимостей через последний аргумент хуков useEffect
, useMemo
, useCallback
или useImperativeHandle
, в него должны войти все использованные значения, которые задействованы в потоке данных React, включая пропсы, состояние и их производные.
Безопасно опускать в списке только те функции, которые не используют ни сами, ни через функции, которые они вызывают, пропсы, состояние или их производные. Так, в следующем примере показан баг:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Рекомендовано исправлять такой код, перемещая функцию внутрь вашего эффекта. Это помогает замечать, какие пропсы и состояние используются вашим эффектом, и убедиться, что они перечислены в списке зависимостей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Это также позволяет обрабатывать ответы, пришедшие не в порядке запросов, с помощью локальной переменной внутри эффекта:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Мы переместили функцию внутрь эффекта, так что её не нужно указывать в списке зависимостей.
Совет
Посмотрите этот небольшой пример и эту статью, чтобы узнать больше о загрузке данных с помощью хуков.
Если по какой-то причине вы не можете переместить функцию в эффект, есть другие варианты:
- Можно попробовать поместить функцию снаружи компонента. В таком случае она гарантированно не будет ссылаться на пропсы и состояние, так что её не требуется перечислять в списке зависимостей.
- Если функция, которую вы вызываете, является чистым вычислением и её безопасно вызывать во время рендера, вы можете вызвать её снаружи эффекта, а не внутри, и сделать эффект зависящим от результата этого вызова, а не от самой функции.
- В крайнем случае можете добавить саму функцию в список зависимостей эффекта, но обернув её определение в хук
useCallback
. Тогда функция будет оставаться неизменной, до тех пор, пока не изменятся её зависимости:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Заметьте, что в примере выше мы всё ещё должны указать функцию в списке зависимостей. Тогда гарантируется, что изменение пропа productId
у ProductPage
приведёт к повторному запрашиванию данных в компоненте ProductDetails
.
Что делать, если зависимости эффекта изменяются слишком часто?¶
Бывают случаи, когда эффект может зависеть от состояния, которое очень часто изменяется. У вас может возникнуть желание не включать это состояние в список зависимостей хука, но это как правило приводит к багам:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Если переписать список зависимостей как [count]
, то баг будет устранён, но это приведёт к сбрасыванию интервала при каждом изменении. Такое поведение может быть нежелательно. Чтобы исправить это, мы можем применить форму функционального обновления хука setState
, которая позволяет указать, как должно меняться состояние, не ссылаясь явно на текущее состояние:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
(Идентичность функции setCount
гарантирована, поэтому её можно безопасно не включать в список зависимостей.)
В более сложных случаях (например, когда одно состояние зависит от другого), попробуйте перенести логику обновления состояния из хука эффекта в хук useReducer
. Эта статья иллюстрирует пример того, как это сделать. Идентичность функции dispatch
из хука useReducer
всегда стабильна — даже если функция редюсера объявлена внутри компонента и считывает его пропсы.
В крайнем случае если вы хотите реализовать что-то наподобие this
в классах, вы можете использовать реф, чтобы хранить в нём изменяемую переменную. Тогда можно писать и читать из него. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Рекомендуется прибегать к этому подходу только если не удалось найти лучшей альтернативы, поскольку компоненты, полагающиеся на изменчивость, менее предсказуемы. Если какой-то конкретный паттерн плохо получается выразить, откройте ишью на GitHub с примером исполняемого кода и мы постараемся помочь.
Как я могу реализовать shouldComponentUpdate
?¶
Вы можете обернуть функциональный компонент в вызов React.memo
для поверхностного сравнения его пропсов:
1 2 3 |
|
Это функция не является хуком, так как она не ведёт себя как хук. React.memo
аналогична PureComponent
, но она сравнивает только пропсы. (Вы также можете добавить второй аргумент, чтобы определить свою функцию сравнения, которая примет старые и новые пропсы. Если эта функция вернёт true
, обновление будет пропущено.)
React.memo
не сравнивает состояние, потому что не существует единого объекта, который можно сравнить. Но вы можете также сделать дочерние компоненты чистыми или даже оптимизировать определённые дочерние компоненты, используя хук useMemo
.
Как закешировать вычисления?¶
Хук useMemo
позволяет вам закешировать вычисления между несколькими рендерами, путём запоминания прошлого результата:
1 2 3 4 |
|
Этот код вызовет computeExpensiveValue(a, b)
. Но если зависимости [a, b]
не изменились с прошлого рендера, useMemo
пропустит повторный вызов и повторно использует значения, которые он вернул в прошлый раз.
Помните, что функция, передаваемая в useMemo
, выполняется во время рендера. Не стоит делать в ней что-то, что вы обычно не делаете во время рендера. Например, побочные эффекты выполняются в хуке useEffect
, а не в useMemo
.
Рассматриваете useMemo
, как оптимизацию производительности, но не стоит полагаться на неё на 100%. В будущем React сможет забыть
прошлые закешированные значения и провести перерасчёт на следующем рендере, например, для освобождения памяти, занятой компонентами вне экрана. Пишите ваш код так, чтобы он мог работать без useMemo
, и только тогда добавляйте оптимизацию производительности. (Для редких случаев, когда значение не должно никогда пересчитываться, вы можете лениво инициализировать реф.)
Также удобно, что useMemo
позволяет пропускать затратные повторные рендеры дочерних компонентов:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Обратите внимание, что этот подход не будет работать внутри циклов, так как вызовы хуков не могут быть помещены внутрь циклов. Но вы можете создать отдельный компонент для элемента списка, и вызвать useMemo
там.
Как лениво создавать большие объекты?¶
Хук useMemo
позволяет вам закешировать ресурсозатратные вычисления, если зависимости не изменились. Однако, этот способ не гарантирует, что повторных вычислений не случится. Иногда вам нужно удостовериться, что объект будет создан только один раз.
Первый частый вариант использования – создание изначального состояния затратно:
1 2 3 4 5 |
|
Чтобы повторно не создавать начальное состояние (которое по факту игнорируется), мы можем передать функцию в useState
:
1 2 3 4 5 6 7 |
|
React вызовет эту функцию один раз во время первого рендера. Смотрите справочник API по хуку useState
.
Иногда, вы захотите избежать повторных вызовов хука useRef()
. Например, возможно вы захотите убедиться, что экземпляр некоторого императивного класса создаётся только один раз:
1 2 3 4 5 |
|
У хука useRef
нет реализации, принимающей специальную функцию, как у useState
. Вместо этого, вы можете написать свою функцию, которая будет лениво создавать и назначать этот экземпляр:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Этот подход позволяет избежать создания больших объектов на всех этапах, кроме первого рендера, когда это действительно необходимо. Если вы используете Flow или TypeScript, вы можете также установить для getObserver()
непустой тип для уверенности.
Являются ли хуки медленными из-за создания функций на каждом рендере?¶
Нет. В современных браузерах сырая производительность замыканий не сильно отличается от классов, кроме особых случаев.
Также учитывайте, что реализация хуков более эффективна по нескольким причинам:
-
Хуки не требуют больших затрат, которые неизбежны в классах. Например, создание экземпляра класса и связывание обработчиков событий в конструкторе.
-
Проект, полностью написанный на хуках, имеет менее глубокое дерево компонентов. В случае использования рендер-пропсов, компонентов высшего порядка или контекста, финальное дерево компонентов было бы в несколько раз больше. Тут стоит понимать, что чем меньше дерево компонентов, тем меньше работы React должен делать.
Традиционно, проблемы с оптимизацией встроенных функций в React связаны с тем, как передача новых колбэков на каждом рендере ломает оптимизации shouldComponentUpdate
в дочерних компонентах. Подход с хуками может решить эту проблему тремя способами.
- Хук
useCallback
позволяет сохранять ту же самую ссылку на колбэк между повторными рендерами, поэтомуshouldComponentUpdate
продолжит корректную работу:
1 2 3 4 |
|
-
Хук
useMemo
облегчает контроль, когда конкретный дочерний компонент должен обновляться, уменьшая нужду в использовании чистых компонентов. -
Наконец, хук
useReducer
уменьшает необходимость передавать колбэки глубоко вниз по дереву, как описано ниже.
Как избежать передачи колбэков вниз?¶
Мы поняли, что большинству разработчиков не нравится вручную передавать колбэки вниз на каждом уровне дерева компонентов. Даже учитывая, что этот подход более явный, это может показаться чересчур громоздким.
В качестве альтернативы для больших деревьев компонентов мы рекомендуем передавать вниз функцию dispatch
из хука useReducer
через контекст:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Любой дочерний компонент в дереве внутри TodosApp
может использовать функцию dispatch
, чтобы передать объекты-действия в TodosApp
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Этот вариант более удобен как с точки зрения поддержки кода (не нужно добавлять пропсы для новых колбэков), так и в разрезе избежания общих проблем с колбэками. Передача функции dispatch
по такому принципу – это рекомендуемый подход для вызова колбэков и избежания нежелательных обновлений в глубине дерева компонентов.
Обратите внимание, что вы также можете передавать состояние приложения вниз через пропсы (более явно) или через контекст (более удобно для глубокого дерева). Если ваш контекст в том числе передаёт некое состояние, используйте два разных типа контекста. Так как контекст dispatch
никогда не изменяется, то компоненты, использующие его, не будут нуждаться в повторном рендере, если только им не требуется ещё и контекст с состоянием приложения.
Как получить часто изменяемое значение из хука useCallback
?¶
Примечание
Мы рекомендуем передавать
dispatch
вниз через контекст вместо передачи отдельных колбэков через пропсы. Следующий подход приведён тут только для полноты картины в качестве лазейки.Также обратите внимание, что этот подход может вызвать проблемы в параллельном режиме. Мы планируем предоставить более удобные альтернативы в будущем, но самый безопасный вариант сейчас — всегда отменять колбэк, если значение, на которое он опирается, поменялось.
В некоторых редких случаях, вам может понадобится закешировать колбэк с помощью хука useCallback
, но кеширование не будет работать хорошо, потому что внутренняя функция будет пересоздаваться слишком часто. Если функция, которую вы кешируете, является обработчиком событий и не используется во время рендера, вы можете использовать реф в качестве свойства экземпляра, и вручную сохранить туда последнее значение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Это довольно запутанный подход, но он показывает, что вы можете сделать эту хитрую оптимизацию, если это вам действительно необходимо. В любом случае, советуем вынести эту логику в отдельный хук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
В любом случае мы не советуем использовать этот подход и показываем его тут только для полноты картины. Вместо этого, старайтесь избегать передачи колбэков глубоко вниз в дерево.
Под капотом¶
Как React связывает вызовы хуков с компонентом?¶
React следит за тем, какой компонент рендерится в данный момент. Благодаря правилам хуков, мы знаем, что хуки вызываются только из React-компонентов (пользовательские хуки также вызываются только из React-компонентов).
Существует внутренний список ячеек памяти
, связанных с каждым компонентом. Они являются JavaScript-объектами, в которых мы можем хранить некоторые данные. Когда вызывается некий хук, например useState()
, он читает значение текущей ячейки (или инициализирует её во время первого рендера) и двигает указатель на следующую. Таким способом каждый вызов useState()
получит своё независимое состояние.
Что послужило прообразом хуков?¶
Хуки были основаны на следующих идеях и экспериментах:
- Наши старые эксперименты с функциональным API в репозитории react-future.
- Эксперименты с API рендер-пропс от React-сообщества, включая компонент Reactions от Райна Флоренса (Ryan Florence).
- Dominic Gannaway и его предложение ключевого слова
adopt
, в качестве синтаксического сахара для паттерна рендер-пропс. - Переменные и ячейки состояния в DisplayScript.
- Компоненты-редюсеры в ReasonReact.
- Подписки в Rx.
- Алгебраические эффекты в Multicore OCaml.
Себастьян Маркбаге (Sebastian Markbåge) предложил изначальный дизайн хуков, который позднее улучшили Эндрю Кларк (Andrew Clark), Софи Алперт (Sophie Alpert), Доминик Ганнауэй (Dominic Gannaway) и другие участники команды React.