Вопросы для собеседования по хукам React
Хуки — это новая функция, добавленная в React v16.8. Хуки позволяют использовать все возможности React без написания классовых компонентов. Например, до версии 16.8 для управления состоянием компонента нам нужен классовый компонент. Теперь мы можем сохранять состояние в функциональном компоненте с помощью хука useState.
Будут ли хуки React работать внутри классовых компонентов?
Зачем были введены хуки в React?
Одной из причин введения хуков была сложность работы с ключевым словом this внутри классовых компонентов. Если с ним не обращаться должным образом, this будет иметь несколько другое значение. Это приведет к поломке таких строк, как this.setState() и других обработчиков событий. Используя хуки, мы избегаем этой сложности при работе с функциональными компонентами.
Классовые компоненты не очень хорошо минимизируются, а также делают горячую перезагрузку ненадежной. Это еще одна причина для создания хуков.
Другая причина в том, что не существует конкретного способа повторно использовать логику компонента, наделенного состоянием. Несмотря на то, что HOC (Higher-Order Component) и шаблоны Render Props (метод передачи props от родителя ребенку, используя функцию или замыкание) решают эту проблему, здесь требуется изменить код классового компонента. Хуки позволяют совместно использовать логику с отслеживанием состояния без изменения иерархии компонентов.
Четвертая причина заключается в том, что в сложном классовом компоненте связанный код разбросан по разным методам жизненного цикла. Например, в случае загрузки данных (fetch) мы делаем это в основном в componentDidMount() и componentDidUpdate(). Другой пример: в случае слушателей событий мы используем componentDidMount() для подписки на события и componentWillUnmount() для отмены подписки. Вместо этого хуки помогают объединить связанный код.
Как работает хук useState? Какие аргументы принимает этот хук и что он возвращает?
Хук useState — это функция, которая используется для хранения состояния в функциональном компоненте. Он принимает аргумент как начальное значение состояния и возвращает массив с 2 элементами. Первый элемент — это текущее значение состояния. Второй элемент — это функция обновления состояния.
Сначала мы импортируем useState из React
import React, from "react";
Затем мы используем useState, например:
const [currentStateValue, functionToUpdateState] = useState(initialStateValue);
Задача на использование useState
У нас есть классовый компонент с состоянием. Каждый раз, когда нажимается кнопка в компоненте, счетчик увеличивается.
class Counter extends Component < state = < count: 0, >; incrementCount = () => < this.setState(< count: this.state.count + 1, >); >; render() < return ( >Count: ); > >
Перепишите этот компонент, используя хуки React.
Решение
import React, < useState >from "react"; function Counter() < const [count, setCount] = useState(0); return ( < setCount(count + 1); >> > Count: ); >
Задача на использование useState 2
Ниже у нас есть классовый компонент. Он содержит код для обновления состояния на основе предыдущего значения состояния.
class Counter extends Component < state = < count: 0 >; incrementCount = () => < this.setState((prevState) =>< return < count: prevState.count + 1, >; >); >; decrementCount = () => < this.setState((prevState) =>< return < count: prevState.count - 1, >; >); >; render() < return ( Count: ); > >
Перепишите вышеуказанный код, используя хуки React.
Решение.
Можно обновить значение переменной состояния, просто передав новое значение в функцию обновления или передав callback. Второй способ с передачей callback-функции безопасен в использовании.
import React, < useState >from "react"; function Counter() < const [count, setCount] = useState(0); const incrementCount = () => < setCount((prevCount) =>< return prevCount + 1; >); >; const decrementCount = () => < setCount((prevCount) =>< return prevCount - 1; >); >; return ( Count: ); >
Задача на использование useState 3
Здесь у нас есть классовый компонент, который обновляет состояние, используя ввод из формы.
export class Profile extends Component < state = < name: "Backbencher", age: 23, >; onNameChange = (e) => < this.setState(< name: e.target.value, >); >; onAgeChange = (e) => < this.setState(< age: e.target.value, >); >; render() < return ( ); > >
Перепишите этот компонент, используя хуки React.
Решение
import React, < useState >from "react"; function Profile() < const [profile, setProfile] = useState(< name: "Backbencher", age: 23, >); const onNameChange = (e) => < setProfile(< . profile, name: e.target.value >); >; const onAgeChange = (e) => < setProfile(< . profile, age: e.target.value >); >; return ( ); >
Функция обновления состояния в useState() не выполняет автоматическое слияние, если в состоянии хранится объект. Но в случае использования метода setState() в классовых компонентах происходит автоматическое слияние.
Здесь мы объединяем свойства объекта с помощью spread оператора JavaScript.
Каковы различия в использовании хуков и классовых компонентов в отношении управления состоянием?
При использовании setState() в классовых компонентах переменная состояния всегда является объектом. В то время как переменная состояния в хуках может иметь любой тип, например, число, строку, логическое значение, объект или массив.
Когда переменная состояния является объектом, setState() в классовых компонентах автоматически объединяет новое значение с объектом состояния. Но в случае функции обновления состояния в useState() нам нужно явно объединить обновленное свойство объекта с помощью spread оператора.
Зачем нужен хук useEffect?
Хук useEffect позволяет нам выполнять побочные эффекты в функциональных компонентах. Это помогает нам избежать избыточного кода в различных методах жизненного цикла классового компонента. Это помогает сгруппировать связанный код.
Задача на использование useEffect
Вот классовый компонент, который печатает Boom в консоли каждый раз, когда он монтируется или обновляется.
export class Banner extends Component < state = < count: 0, >; updateState = () => < this.setState(< count: this.state.count + 1, >); >; componentDidMount() < console.log("Boom"); >componentDidUpdate() < console.log("Boom"); >render() < return ( >State: ); > >
Удалите избыточные выражения console.log с помощью хуков React.
Решение.
componentDidMount() и componentDidUpdate() — это методы жизненного цикла. Такие побочные эффекты можно сделать с помощью хука useEffect. Хук useEffect — это функция, которая принимает callback. Этот callback вызывается каждый раз, когда происходит рендеринг.
Код может быть переписан таким образом:
import React, < useState, useEffect >from "react"; function Banner() < const [count, setCount] = useState(0); useEffect(() =>< console.log("Boom"); >); const updateState = () => < setCount(count + 1); >; return ( >State: ); >
Задача на использование useEffect 2
Понять данный код:
function Banner() < const [count, setCount] = useState(0); const [name, setName] = useState(""); useEffect(() =>< console.log("Счетчик обновлен"); >); return ( setCount(count + 1)>>State: onChange= setName(e.target.value)> /> ); >
Код печатает сообщение «Счетчик обновлен» даже при обновлении значения в текстовом поле. Как мы можем отображать сообщение только при обновлении состояния счетчика?
Решение.
Функция useEffect принимает второй параметр, который должен быть массивом. В этом массиве нам нужно передать props или состояние, за которым нам нужно следить. Эффект выполняется только в том случае, если эти свойства или состояние, упомянутые в массиве, изменяются. Поэтому в нашем коде мы добавляем второй аргумент и указываем только значение count в массиве.
Вот обновленный код useEffect:
useEffect(() => < console.log("Count is updated"); >, [count]);
Задача на использование useEffect 3
У нас есть классовый компонент, который обновляет время каждую секунду. Он использует componentDidMount() для установки таймера.
export class Clock extends Component < state = < date: new Date(), >; componentDidMount() < setInterval(() =>< this.setState(< date: new Date(), >); >, 1000); > render() < return ; > >
Перепишите код выше с помощью хуков React.
Решение.
componentDidMount() — это метод жизненного цикла, который выполняется только один раз в жизненном цикле компонента. Мы используем useEffect, чтобы вызвать эффекты componentDidMount(). Но useEffect запускается при каждом обновлении props или состояния. Чтобы предотвратить это, мы используем второй аргумент-массив useState. Мы оставляем этот массив пустым. Итак, для React нет никаких переменных состояния или props, за которыми нужно следить. Следовательно, useEffect запускается только один раз, как componentDidMount().
Вот код с использованием хуков React.
function Clock() < const [date, setDate] = useState(new Date()); useEffect(() => < setInterval(() =>< setDate(new Date()); >, 1000); >, []); return ; >
Задача на использование useEffect 4
У нас есть фрагмент кода из классового компонента, который регистрирует и удаляет слушатель событий.
componentDidMount() < window.addEventListener("mousemove", this.handleMousePosition); >componentWillUnmount()
Перепишите этот код в стиле хуков React.
Решение.
Чтобы использовать функциональность метода жизненного цикла componentWillUnmount() в хуке useEffect нужно вернуть callback с кодом, который необходимо выполнить при размонтировании компонента.
useEffect(() => < window.addEventListener("mousemove", handleMousePosition); return () => < window.removeEventListener("mousemove", handleMousePosition); >>, []);
Задача на использование useContext
Вот фрагмент кода из компонента Context.Consumer.
import < NameContext, AgeContext >from "./ProviderComponent"; export class ConsumerComponent extends Component < render() < return ( < return ( ( Name: , Age: )> ); >> ); > >
Перепишите ConsumerComponent, используя хук useContext.
Решение.
Хуки можно использовать только в функциональном компоненте.
ConsumerComponent можно переписать так:
function ConsumerComponent() < const name = useContext(NameContext); const age = useContext(AgeContext); return ( Name: , Age: ); >
Заметка о том, как работают хуки в React

Хочу поделиться с вами некоторыми наблюдениями относительно того, как работает React, а именно: предположениями о том, почему хуки нельзя использовать в if, циклах, обычных функциях и т.д. И действительно ли их нельзя использовать подобным образом?
Вопрос звучит следующим образом: почему хуки можно использовать только на верхнем уровне? Вот что по этому поводу говорит официальная документация.
Начнем с правил использования хуков.
Используйте хуки только на верхнем уровне (выделил ключевые моменты, на которые следует обратить внимание):
«Не вызывайте хуки внутри циклов, условных операторов или вложенных функций. Вместо этого всегда используйте хуки только внутри React-функций, до возврата какого-либо значения из них. Исполнение этого правила гарантирует, что хуки вызываются в одинаковой последовательности при каждом рендере компонента. Это позволит React правильно сохранять состояние хуков между множественными вызовами useState и useEffect. (Если вам интересно, подробное объяснение ниже.)»
Нам интересно, смотрим ниже.
Объяснение (примеры опущены для краткости):
«… как же React сопоставляет переменные состояния с вызовами useState? Ответ таков: React полагается на порядок вызова хуков.… До тех пор пока порядок вызова хуков одинаков в каждом рендере, React может сопоставить некое внутреннее состояние с каждым из них. Но что случится, если мы поместим вызов хука внутрь условного оператора?… во время рендера хук будет пропущен и порядок вызовов хуков изменится. React не будет знать, что вернуть для второго вызова хука useState. React ожидал, что второй вызов хука в этом компоненте соответствует эффекту persistForm, так же как при предыдущем рендере, но это больше не так. Начиная с этого момента, вызов каждого хука, следующего за пропущенным, также будет сдвинут на один назад, что приведёт к ошибкам.… Вот почему хуки должны вызываться на верхнем уровне компонента.… теперь вы знаете, почему хуки работают таким образом . »
Понятно? Да как-то не очень. Что значит «React полагается на порядок вызова хуков»? Как он это делает? Что за «некое внутреннее состояние»? К каким ошибкам приводит пропуск хука при повторном рендере? Являются ли эти ошибки критическими для работы приложения?
Есть ли в документации что-нибудь еще по этому поводу? Есть специальный раздел «Хуки: ответы на вопросы». Там мы находим следующее.
«React следит за тем, какой компонент рендерится в данный момент.… Существует внутренний список ячеек памяти, связанных с каждым компонентом. Они являются JavaScript-объектами, в которых мы можем хранить некоторые данные. Когда вызывается некий хук, например useState(), он читает значение текущей ячейки (или инициализирует её во время первого рендера) и двигает указатель на следующую. Таким способом каждый вызов useState() получит своё независимое состояние.»
Уже кое-что. Внутренний список ячеек памяти, связанных с компонентами и содержащих некоторые данные. Хук читает значение текущей ячейки и двигает указатель на следующую. Какую структуру данных вам это напоминает? Возможно, речь идет о связанном (связном) списке.
Если это и в самом деле так, то последовательность хуков, формируемая React при первом рендеринге, выглядит следующим образом (представим, что прямоугольники — это хуки, каждый хук содержит указатель на следующий):

Отлично, у нас есть рабочая гипотеза, которая выглядит более-менее разумно. Как нам ее проверить? Гипотеза гипотезой, но хочется фактов. А за фактами придется идти на GitHub, в репозиторий с исходниками React.
Не думайте, что я сразу решился на такой отчаянный шаг. Разумеется, сначала в поисках ответов на интересующие меня вопросы я обратился к всеведущему Гуглу. Вот что удалось обнаружить:
- Вопрос о том, как хуки React определяют свою принадлежность к конкретному компоненту, на Stack Overflow
- Статья «Первое погружение в исходники хуков (задел на будущие статьи)» на Хабре
- Предложения по использованию хуков в инструкции «if» на GitHub
- Также кое-какую информацию можно почерпнуть из определений TypsScript-типов для React. Вот соответствующие выдержки
Реализация useState() и других хуков находится в ReactHooks.js:
export function useState( initialState: (() => S) | S ): [S, Dispatch]
Для вызова useState() (и других хуков) используется некий диспетчер. В начале того же файла видим следующее:
import ReactCurrentDispatcher from './ReactCurrentDispatcher' function resolveDispatcher()
Диспетчер, который используется для вызова useState() (и других хуков), является значением свойства «current» объекта «ReactCurrentDispatcher», который импортируется из ReactCurrentDispatcher.js:
import type < Dispatcher >from 'react-reconciler/src/ReactInternalTypes' const ReactCurrentDispatcher = < current: (null: null | Dispatcher) >export default ReactCurrentDispatcher
ReactCurrentDispatcher — это пустой объект со свойством «current». Значит, инициализируется он где-то в другом месте. Но где именно? Подсказка: импорт типа «Dispatcher» указывает на то, что текущий диспетчер как-то связан с «внутренностями» React. И действительно, вот что мы находим в ReactFiberHooks.new.js (число в комментарии — это номер строки):
// 118 const < ReactCurrentDispatcher, ReactCurrentBatchConfig >= ReactSharedInternals
Однако в ReactSharedInternals.js мы упираемся в «секретные внутренние данные, за использование которых можно быть уволенным»:
const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED export default ReactSharedInternals
И что, это все? Неужели наши поиски, не успев начаться, подошли к концу? Не совсем. Деталей внутренней реализации React мы не узнаем, но нам это и не нужно для понимания того, как React управляет хуками. Возвращаемся в ReactFiberHooks.new.js:
// 405 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate
В качестве диспетчера, который используется для вызова хуков, фактически используются два разных диспетчера — HooksDispatcherOnMount (при монтировании) и HooksDispatcherOnUpdate (при обновлении, повторном рендеринге).
// 2086 const HooksDispatcherOnMount: Dispatcher = < useState: mountState, // другие хуки и еще кое-что >// 2111 const HooksDispatcherOnUpdate: Dispatcher = < useState: updateState, // другие хуки и еще кое-что >
Разделение «монтирование/обновление» сохраняется на уровне хуков.
function mountState( initialState: (() => S) | S ): [S, Dispatch] < // создаем объект хука const hook = mountWorkInProgressHook() // если значением начального состояния является функция if (typeof initialState === 'function') < initialState = initialState() >// записываем начальное состояние в два свойства хука // эти свойства в дальнейшем используются для определения необходимости в обновлении hook.memoizedState = hook.baseState = initialState // создаем очередь и записываем ее в свойство хука // очередь связана с планированием обновлений const queue = (hook.queue = < pending: null, interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any) >) // создаем диспетчера - функцию для обновления состояния (setState) const dispatch: Dispatch < BasicStateAction> = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue ): any)) // обратите внимание, что возвращается не сам хук, а его мемоизированное состояние и диспетчер return [hook.memoizedState, dispatch] > // 1266 function updateState( initialState: (() => S) | S ): [S, Dispatch ]
Для обновления состояния используется функция «updateReducer», поэтому мы говорим, что useState внутренне использует useReducer или что useReducer — более низкоуровневая реализация useState.
function updateReducer( reducer: (S, A) => S, initialArg: I, init?: (I) => S ): [S, Dispatch] < // создаем хук, но уже с помощью другой функции (!) const hook = updateWorkInProgressHook() // получаем очередь const queue = hook.queue // записывает редуктор в качестве последнего отрендеренного в очередь queue.lastRenderedReducer = reducer const current: Hook = (currentHook: any) // состояние обновляется асинхронно, следовательно, операции обновления помещаются в очередь let baseQueue = current.baseQueue // если у нас имеется очередь из операций обновления if (baseQueue !== null) < const first = baseQueue.next let newState = current.baseState let newBaseState = null let newBaseQueueFirst = null let newBaseQueueLast = null let update = first do < // вычисляем определенные выше переменные >while (update !== null && update !== first) // присваиваем свойствам хука новые значения hook.memoizedState = newState hook.baseState = newBaseState hook.baseQueue = newBaseQueueLast // записываем новое состояние в качестве последнего отрендеренного в очередь queue.lastRenderedState = newState > // создаем диспетчера const dispatch: Dispatch = (queue.dispatch: any) // возвращаем мемоизированное состояние и диспетчера return [hook.memoizedState, dispatch] >
Пока что мы увидели только, как работают сами хуки. Где же список? Подсказка: хуки при монтировании/обновлении создаются с помощью функций «mountWorkInProgressHook» и «updateWorkInProgressHook», соответственно.
// 592 function mountWorkInProgressHook(): Hook < // создаем хук const hook: Hook = < memoizedState: null, baseState: null, baseQueue: null, queue: null, // указатель на следующий хук (?!) next: null >// если workInProgressHook равняется null, значит, данный хук является первым в очереди if (workInProgressHook === null) < currentlyRenderingFiber.memoizedState = workInProgressHook = hook >else < // в противном случае, добавляем хук в конец списка workInProgressHook = workInProgressHook.next = hook >return workInProgressHook > // 613 function updateWorkInProgressHook(): Hook < // Данная функция используется как для обновления, так и для повторного рендеринга // Она предполагает, что существует либо текущий хук (current hook), который можно клонировать (см. ниже), либо workInProgressHook из предыдущего рендеринга, // который можно взять за основу // После достижения конца списка, происходит переключение на диспетчера, используемого для монтирования let nextCurrentHook: null | Hook if (currentHook === null) < const current = currentlyRenderingFiber.alternate if (current !== null) < nextCurrentHook = current.memoizedState >else < nextCurrentHook = null >> else < nextCurrentHook = currentHook.next >let nextWorkInProgressHook: null | Hook if (workInProgressHook === null) < nextWorkInProgressHook = currentlyRenderingFiber.memoizedState >else < nextWorkInProgressHook = workInProgressHook.next >if (nextWorkInProgressHook !== null) < // используем существующий workInProgressHook workInProgressHook = nextWorkInProgressHook nextWorkInProgressHook = workInProgressHook.next currentHook = nextCurrentHook >else < // клонируем текущий хук // Данное исключение говорит о том, что было отрендерно больше хуков, чем в прошлый раз // Хм, означает ли это, что мы можем вызывать либо один, либо другой хук // в зависимости от условия, если при этом общее количество хуков останется неизменным? // Или значение имеет не только количество, но и "качество" хуков? invariant( nextCurrentHook !== null, 'Rendered more hooks than during the previous render.' ) currentHook = nextCurrentHook const newHook: Hook = < memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null >// если workInProgressHook равняется null, значит, данный хук является первым в очереди if (workInProgressHook === null) < currentlyRenderingFiber.memoizedState = workInProgressHook = newHook >else < // добавляем хук в конец списка workInProgressHook = workInProgressHook.next = newHook >> return workInProgressHook >
Полагаю, наша гипотеза о том, что для управления хуками используется связный список, нашла свое подтверждение. Мы выяснили, что каждый хук имеет свойство «next», значением которого является ссылка на следующий хук. Вот хорошая иллюстрация этого списка из указанной выше статьи:

Для тех, кому интересно, вот как выглядит простейшая реализация однонаправленного связного списка на JavaScript:
Немного много кода
class Node < constructor(data, next = null) < this.data = data this.next = next >> class LinkedList < constructor() < this.head = null >insertHead(data) < this.head = new Node(data, this.head) >size() < let counter = 0 let node = this.head while (node) < counter++ node = node.next >return counter > getHead() < return this.head >getTail() < if (!this.head) return null let node = this.head while (node) < if (!node.next) return node node = node.next >> clear() < this.head = null >removeHead() < if (!this.head) return this.head = this.head.next >removeTail() < if (!this.head) return if (!this.head.next) < this.head = null return >let prev = this.head let node = this.head.next while (node.next) < prev = node node = node.next >prev.next = null > insertTail(data) < const last = this.getTail() if (last) last.next = new Node(data) else this.head = new Node(data) >getAt(index) < let counter = 0 let node = this.head while (node) < if (counter === index) return node counter++ node = node.next >return null > removeAt(index) < if (!this.head) return if (index === 0) < this.head = this.head.next return >const prev = this.getAt(index - 1) if (!prev || !prev.next) return prev.next = prev.next.next > insertAt(index, data) < if (!this.head) < this.head = new Node(data) return >const prev = this.getAt(index - 1) || this.getTail() const node = new Node(data, prev.next) prev.next = node > forEach(fn) < let node = this.head let index = 0 while (node) < fn(node, index) node = node.next index++ >> *[Symbol.iterator]() < let node = this.head while (node) < yield node node = node.next >> > // пример использования const chain = new LinkedList() chain.insertHead(1) console.log( chain.head.data, // 1 chain.size(), // 1 chain.getHead().data // 1 ) chain.insertHead(2) console.log(chain.getTail().data) // 1 chain.clear() console.log(chain.size()) // 0 chain.insertHead(1) chain.insertHead(2) chain.removeHead() console.log(chain.size()) // 1 chain.removeTail() console.log(chain.size()) // 0 chain.insertTail(1) console.log(chain.getTail().data) // 1 chain.insertHead(2) console.log(chain.getAt(0).data) // 2 chain.removeAt(0) console.log(chain.size()) // 1 chain.insertAt(0, 2) console.log(chain.getAt(1).data) // 2 chain.forEach((node, index) => (node.data = node.data + index)) console.log(chain.getTail().data) // 3 for (const node of chain) node.data = node.data + 1 console.log(chain.getHead().data) // 2 // поиск центрального элемента function middle(list) < let one = list.head let two = list.head while (two.next && two.next.next) < one = one.next two = two.next.next >return one > chain.clear() chain.insertHead(1) chain.insertHead(2) chain.insertHead(3) console.log(middle(chain).data) // 2 // создание циклического списка function circular(list) < let one = list.head let two = list.head while (two.next && two.next.next) < one = one.next two = two.next.next if (two === one) return true >return false > chain.head.next.next.next = chain.head console.log(circular(chain)) // true
Получается, что при повторном рендеринге с меньшим (или большим) количеством хуков, updateWorkInProgressHook() возвращает хук, не соответствующий своей позиции в предыдущем списке, т.е. в новом списке будет недоставать узла (или появится дополнительный узел). И в дальнейшем для вычисления нового состояния будет использовано неправильное мемоизированное состояние. Безусловно, это серьезная проблема, но насколько она критична? Неужели React не умеет перестраивать список хуков на лету? И существует ли какой-то способ реализовать условное использование хуков? Давайте это выясним.
Да, пока мы не ушли из исходников, поищем линтер, обеспечивающий соблюдение правил использования хуков. RulesOfHooks.js:
if (isDirectlyInsideComponentOrHook) < if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) < const message = `React Hook "$" is called ` + 'conditionally. React Hooks must be called in the exact ' + 'same order in every component render.' + (possiblyHasEarlyReturn ? ' Did you accidentally call a React Hook after an' + ' early return?' : '') context.report(< node: hook, message >) > >
Не будет вдаваться в подробности того, как определяется разница между количеством хуков. А вот как определяется, что функция — это хук:
function isHookName(s) < return /^use[A-Z0-9].*$/.test(s) >function isHook(node) < if (node.type === 'Identifier') < return isHookName(node.name) >else if ( node.type === 'MemberExpression' && !node.computed && isHook(node.property) ) < const obj = node.object const isPascalCaseNameSpace = /^[A-Z].*/ return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name) >else < return false >>
Набросаем компонент, в котором имеет место условное использование хуков, и посмотрим, что произойдет при его рендеринге.
import < useEffect, useState >from 'react' // первый кастомный хук function useText() < const [text, setText] = useState('') useEffect(() => < const => < setText('Hello') const _id = setTimeout(() => < setText((text) =>text + ' World') clearTimeout(_id) >, 1000) >, 1000) return () => < clearTimeout(id) >>, []) return text > // второй кастомный хук function useCount() < const [count, setCount] = useState(0) useEffect(() => < const => < setCount((count) =>count + 1) >, 1000) return () => < clearInterval(id) >>, []) return count > // компонент, в котором используется один из кастомных хуков в зависимости от условия const Content = (< active >) => function ConditionalHook() < const [active, setActive] = useState(false) return ( <> /> >) > export default ConditionalHook
В приведенном примере у нас имеется два пользовательских хука — useText() и useCount(). Мы пытаемся использовать тот или иной хук в зависимости от состояния переменной «active». Рендерим. Получаем ошибку «React Hook ‘useText’ is called conditionally. React Hooks must be called in the exact same order in every component render», которая говорит о том, что хуки должны вызываться в одинаковом порядке при каждом рендеринге.
Может быть, дело не столько в React, сколько в ESLint. Попробуем его отключить. Для этого добавляем /* eslint-disable */ в начале файла. Теперь компонент «Content» рендерится, но переключение между хуками не работает. Значит, дело все-таки в React. Что еще можно сделать?
Что если сделать пользовательские хуки обычными функциями? Пробуем:
function getText() < // . >function getCount() < // . >const Content = (< active >) =>
Результат такой же. Компонент рендерится с getCount(), но переключиться между функциями не получается. К слову, без /* eslint-disable */ мы получим ошибку «React Hook „useState“ is called in function „getText“ that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter», которая говорит о том, что хук вызывается внутри функции, которая не является ни компонентом, ни пользовательским хуком. В этой ошибке кроется подсказка.
Что если сделать наши функции компонентами?
function Text() < // . >function Count() < // . >const Content = (< active >) => : >
Теперь все работает, как ожидается, причем, даже с включенным линтером. Это объясняется тем, что мы фактически реализовали условный рендеринг компонентов. Очевидно, для реализации условного рендеринга компонентов React использует другой механизм. Почему этот механизм нельзя было применить в отношении хуков?
Проведем еще один эксперимент. Мы знаем, что в случае с рендерингом списка элементов, каждому элементу добавляется атрибут «key», позволяющий React отслеживать состояние списка. Что если использовать этот атрибут в нашем примере?
function useText() < // . >function useCount() < // . >const Content = (< active >) => function ConditionalHook() < const [active, setActive] = useState(false) return ( <> * добавляем key */> active= /> >) >
С линтером получаем ошибку. Без линтера… все работает! Но почему? Возможно, React считает Content с useText() и Content с useCount() двумя разными компонентами и выполняет условный рендеринг компонентов в зависимости от состояния active. Как бы то ни было, мы нашли обходной путь. Другой пример:
import < useEffect, useState >from 'react' const getNum = (min = 100, max = 1000) => ~~(min + Math.random() * (max + 1 - min)) // кастомный хук function useNum() < const [num, setNum] = useState(getNum()) useEffect(() => < const =>setNum(getNum()), 1000) return () => clearInterval(id) >, []) return num > // компонент-обертка function NumWrapper(< setNum >) < const num = useNum() useEffect(() =>< setNum(num) >, [setNum, num]) return null > function ConditionalHook2() < const [active, setActive] = useState(false) const [num, setNum] = useState(0) return ( <>Правила использования хуков?
Нет, не слышали
/>> >) > export default ConditionalHook2
В приведенном примере у нас имеется пользовательский хук «useNum», каждую секунду возвращающий случайное целое число в диапазоне от 100 до 1000. Мы заворачиваем его в компонент «NumWrapper», который ничего не возвращает (точнее, возвращает null), но… за счет использования setNum из родительского компонента происходит подъем состояния. Конечно, фактически мы снова реализовали условный рендеринг компонента. Тем не менее, это показывает, что, при желании, добиться условного использования хуков все-таки можно.
Код примеров находится здесь.
Подведем итоги. Для управления хуками React использует связный список. Каждый (текущий) хук содержит указатель на следующий хук или null (в свойстве «next»). Вот почему важно соблюдать порядок вызова хуков при каждом рендеринге.
Несмотря на то, что добиться условного использования хуков через условный рендеринг компонентов можно, делать этого не следует: последствия могут быть непредсказуемыми.
Еще парочка наблюдений, связанных с исходниками React: классы практически не используются, а функции и их композиции являются максимально простыми (даже тернарный оператор используется редко); названия функций и переменных являются довольно информативными, хотя из-за большого количества переменных возникает необходимость использования префиксов «base», «current» и т.д., что приводит к некоторой путанице, но, учитывая размер кодовой базы, такая ситуация является вполне закономерной; присутствуют развернутые комментарии, включая TODO.
На правах саморекламы: для тех, кто хочет изучить или получше разобраться в инструментах, используемых при разработке современных веб-приложений (React, Express, Mongoose, GraphQL и т.д.), предлагаю взглянуть на этот репозиторий.
Надеюсь, вам было интересно. Конструктивные замечания в комментариях приветствуются. Благодарю за внимание и хорошего дня.
- Веб-разработка
- JavaScript
- Программирование
- ReactJS
useState() — хук состояния в ReactJS
useState() — это хук, который предназначен для работы с состоянием компонента в приложениях на React и работает только в функциональных компонентах.
Для чего нужен useState()
Данный хук используется для:
- управления состоянием, в том числе передачи данных как пропсов другим компонентам
- условного рендеринга — рендер тех или иных данных, в зависимости от того, что хранится в стейте
- для переключения состояния (true/false)
- как счетчик чего-либо
- хранения данных полученных от сервера
Самый простой пример использования:
import React, < useState >from "react"; export default function App() < const [state, setState] = useState('Hello World'); return ( ); >
Хук useState() возвращает состояние (state) и функцию для его изменения (setState). Название данной функции и переменной задаете вы сами. C помощью деструктуризации ( [state, setState] ) мы сразу получаем значения.
В качестве параметра хук принимает значение по умолчанию. В нашем случае это строка Hello World.
Данный пример очень простой и в такой реализации нам состояние даже не понадобиться, мы могли бы сразу в заголовок прописать данную строку.
Давайте рассмотрим другой пример. У нас есть поле ввода и в зависимости от того, что мы вводим, будет изменяться заголовок:
import React, < useState >from "react"; export default function App() < const [title, setTitle] = useState(''); const changeTitle = (e) => < const newTitle = e.target.value; setTitle(newTitle); >return ( onChange= changeTitle(e)> /> ); >
Теперь у нас состояние называется title и функция, которая будет его изменять — setTitle. Добавляем input с обработчиком изменений changeTitle. В обработчике мы получаем значение из инпута (e.target.value) и с помощью функции для обновления состояния setTitle меняем его на новый.
Сейчас независимо от того, есть у нас title или нет, тег h1 будет отображаться все равно. На этом же примере мы можем рассмотреть условный рендеринг. В примере ниже если title не задан, тогда тег не будет отображаться:
return ( < title && > onChange= changeTitle(e)> /> );
Также возможен другой вариант. Вы можете в зависимости от условия делать разный return
if (title) return return ( onChange= changeTitle(e)> /> );
Если title задан отрисовываем его, если нет отрисовываем инпут для ввода тайтла.
В состоянии компонента вы можете хранить строки, числа, массивы, объект и т.д.
Сохранение данных с сервера с помощью хука useState()
Для хранения данных полученных с сервера нам дополнительно понадобиться хук useEffect() :
Теперь давайте рассмотрим, что у нас есть. У нас есть api (https://jsonplaceholder.typicode.com/posts), который возвращает нам массив из 100 постов. Все асинхронные действия в функциональных компонентах должны делаться с помощью хука useEffect , поэтому мы с помощью fecth получаем посты и дальше сохраняем их в posts с помощью функции setPosts .
Далее, чтобы сделать список постов, мы проходимся методом map() по массиву и на каждой итерации отрисовываем элемент li с уникальным ключом key.
Использование useState() с объектом
Рассмотрим теперь такой пример, где у нас state это объект и нам необходимо будет его обновлять:
У нас есть user , по умолчанию это пустой объект, и есть форма ниже для указания необходимых полей пользователя. У всех полей формы есть одинаковый обработчик — handleFieldChange , с его помощью мы получаем вводимые значения, далее с помощью деструктуризации мы получаем имя и значение поля, создаем новый объект, куда помещаем уже существующие поля у user (строка 10) и добавляем новые поля. Затем обновляем state.
Ваши вопросы и комментарии:
Свежие записи
- MySQL IS NULL — проверка поля на NULL
- MySQL LIKE — поиск по паттерну
- Between MySQL — оператор для выборки по диапазону значений
- MySQL IN и NOT IN — несколько условий
- MySQL WHERE — синтаксис и примеры
Копирование материалов разрешено только с ссылкой на источник Web-Dev.guru
2024 © Все права защищены.
Как работает useState в реакте

Есть много примеров упрощенной реализации useState, чтобы объянить как он работает. Суть в следеующем, хранить масссив вызовов useState и при вызове изменения значения, сбрасывать счетчик вызова и доставать оттуда стейт во время рендера, увеличивая счетчик. Но такая реализация легко ломается, если у нас испольуется условный рендеринг. Т.е мы последовательно вызывали useState для 2 компонентов, но если появилось, например, еще 2 новых компонента между первыми двумя, то на выходе получаем, что стейт переместится от 4 компонента ко второму. Но если использывать useState из react, то он как-то понимает, где какой компонент находится, и обрабатывает правильно. Схематично отобразил ниже. Т.е порядок компонентов при условном рендере меняется, и поэтому в new1 (справа) попадет стейт от component2 (слева), а должен попадать из component2 туда же (красная стрелка). Вопрос, можно ли как-то доработать упрощеную реализацию, чтобы она работала правильно? Вот упрощенная реализация: https://codesandbox.io/s/wizardly-rgb-rqixxt?file=/src/index.js Вот реализация из react: https://codesandbox.io/s/dazzling-breeze-c5nz9e?file=/src/index.js
Отслеживать
задан 15 дек 2022 в 8:50
31 5 5 бронзовых знаков
0
Сортировка: Сброс на вариант по умолчанию
Знаете кого-то, кто может ответить? Поделитесь ссылкой на этот вопрос по почте, через Твиттер или Facebook.
- reactjs
- react-hook