Заметка о том, как работают хуки в React



    Доброго времени суток, друзья!

    Хочу поделиться с вами некоторыми наблюдениями относительно того, как работает React, а именно: предположениями о том, почему хуки нельзя использовать в if, циклах, обычных функциях и т.д. И действительно ли их нельзя использовать подобным образом?

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

    Начнем с правил использования хуков.

    Используйте хуки только на верхнем уровне (выделил ключевые моменты, на которые следует обратить внимание):

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

    Нам интересно, смотрим ниже.

    Объяснение (примеры опущены для краткости):

    "… как же React сопоставляет переменные состояния с вызовами useState? Ответ таков: React полагается на порядок вызова хуков.… До тех пор пока порядок вызова хуков одинаков в каждом рендере, React может сопоставить некое внутреннее состояние с каждым из них. Но что случится, если мы поместим вызов хука внутрь условного оператора?… во время рендера хук будет пропущен и порядок вызовов хуков изменится. React не будет знать, что вернуть для второго вызова хука useState. React ожидал, что второй вызов хука в этом компоненте соответствует эффекту persistForm, так же как при предыдущем рендере, но это больше не так. Начиная с этого момента, вызов каждого хука, следующего за пропущенным, также будет сдвинут на один назад, что приведёт к ошибкам.… Вот почему хуки должны вызываться на верхнем уровне компонента.… теперь вы знаете, почему хуки работают таким образом ..."

    Понятно? Да как-то не очень. Что значит «React полагается на порядок вызова хуков»? Как он это делает? Что за «некое внутреннее состояние»? К каким ошибкам приводит пропуск хука при повторном рендере? Являются ли эти ошибки критическими для работы приложения?

    Есть ли в документации что-нибудь еще по этому поводу? Есть специальный раздел «Хуки: ответы на вопросы». Там мы находим следующее.

    Как React связывает вызовы хуков с компонентом?

    «React следит за тем, какой компонент рендерится в данный момент.… Существует внутренний список ячеек памяти, связанных с каждым компонентом. Они являются JavaScript-объектами, в которых мы можем хранить некоторые данные. Когда вызывается некий хук, например useState(), он читает значение текущей ячейки (или инициализирует её во время первого рендера) и двигает указатель на следующую. Таким способом каждый вызов useState() получит своё независимое состояние.»

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

    Если это и в самом деле так, то последовательность хуков, формируемая React при первом рендеринге, выглядит следующим образом (представим, что прямоугольники — это хуки, каждый хук содержит указатель на следующий):


    Отлично, у нас есть рабочая гипотеза, которая выглядит более-менее разумно. Как нам ее проверить? Гипотеза гипотезой, но хочется фактов. А за фактами придется идти на GitHub, в репозиторий с исходниками React.

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


    Все эти источники отсылают к исходникам React. Пришлось немного в них покопаться. Итак, тезисно и на примере «useState».

    Реализация useState() и других хуков находится в ReactHooks.js:

    export function useState<S>(
      initialState: (() => S) | S
    ): [S, Dispatch<BasicStateAction<S>>] {
      const dispatcher = resolveDispatcher()
      return dispatcher.useState(initialState)
    }
    

    Для вызова useState() (и других хуков) используется некий диспетчер. В начале того же файла видим следующее:

    import ReactCurrentDispatcher from './ReactCurrentDispatcher'
    
    function resolveDispatcher() {
      const dispatcher = ReactCurrentDispatcher.current
    
      return ((dispatcher: any): Dispatcher)
    }
    

    Диспетчер, который используется для вызова 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<S>(
      initialState: (() => S) | S
    ): [S, Dispatch<BasicStateAction<S>>] {
      // создаем объект хука
      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<S>
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue
      ): any))
      // обратите внимание, что возвращается не сам хук, а его мемоизированное состояние и диспетчер
      return [hook.memoizedState, dispatch]
    }
    
    // 1266
    function updateState<S>(
      initialState: (() => S) | S
    ): [S, Dispatch<BasicStateAction<S>>] {
      return updateReducer(basicStateReducer, (initialState: any))
    }
    

    Для обновления состояния используется функция «updateReducer», поэтому мы говорим, что useState внутренне использует useReducer или что useReducer — более низкоуровневая реализация useState.

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: (I) => S
    ): [S, Dispatch<A>] {
      // создаем хук, но уже с помощью другой функции (!)
      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<A> = (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 "${context.getSource(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 id = setTimeout(() => {
          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 id = setInterval(() => {
          setCount((count) => count + 1)
        }, 1000)
        return () => {
          clearInterval(id)
        }
      }, [])
    
      return count
    }
    
    // компонент, в котором используется один из кастомных хуков в зависимости от условия
    const Content = ({ active }) => <p>{active ? useText() : useCount()}</p>
    
    function ConditionalHook() {
      const [active, setActive] = useState(false)
    
      return (
        <>
          <button onClick={() => setActive(!active)}>Другой хук</button>
          <Content active={active} />
        </>
      )
    }
    
    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 }) => <p>{active ? getText() : getCount()}</p>
    

    Результат такой же. Компонент рендерится с 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 }) => <p>{active ? <Text /> : <Count />}</p>
    

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

    Проведем еще один эксперимент. Мы знаем, что в случае с рендерингом списка элементов, каждому элементу добавляется атрибут «key», позволяющий React отслеживать состояние списка. Что если использовать этот атрибут в нашем примере?

    function useText() {
      // ...
    }
    
    function useCount() {
      // ...
    }
    
    const Content = ({ active }) => <p>{active ? useText() : useCount()}</p>
    
    function ConditionalHook() {
      const [active, setActive] = useState(false)
    
      return (
        <>
          <button onClick={() => setActive(!active)}>Другой хук</button>
          {/* добавляем key */}
          <Content key={active} active={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 id = setInterval(() => 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 (
        <>
          <h3>Правила использования хуков? <br /> Нет, не слышали</h3>
          <button onClick={() => setActive(!active)}>Это не кнопка</button>
          <p>{active && num}</p>
          {active && <NumWrapper setNum={setNum} />}
        </>
      )
    }
    
    export default ConditionalHook2
    

    В приведенном примере у нас имеется пользовательский хук «useNum», каждую секунду возвращающий случайное целое число в диапазоне от 100 до 1000. Мы заворачиваем его в компонент «NumWrapper», который ничего не возвращает (точнее, возвращает null), но… за счет использования setNum из родительского компонента происходит подъем состояния. Конечно, фактически мы снова реализовали условный рендеринг компонента. Тем не менее, это показывает, что, при желании, добиться условного использования хуков все-таки можно.

    Код примеров находится здесь.

    Песочница:


    Подведем итоги. Для управления хуками React использует связный список. Каждый (текущий) хук содержит указатель на следующий хук или null (в свойстве «next»). Вот почему важно соблюдать порядок вызова хуков при каждом рендеринге.

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

    Еще парочка наблюдений, связанных с исходниками React: классы практически не используются, а функции и их композиции являются максимально простыми (даже тернарный оператор используется редко); названия функций и переменных являются довольно информативными, хотя из-за большого количества переменных возникает необходимость использования префиксов «base», «current» и т.д., что приводит к некоторой путанице, но, учитывая размер кодовой базы, такая ситуация является вполне закономерной; присутствуют развернутые комментарии, включая TODO.

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

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

    Похожие публикации

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 154 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

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

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

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

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

        +1, что не довод в статье то "WAT?".


        Подведем итоги. Для управления хуками React использует связный список.

        А могли и обычный массив использовать. Ничего бы снаружи не поменялось.


        Каждый (текущий) хук содержит указатель на следующий хук или null (в свойстве «next»).

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


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

        Да нет же. Не путайте причину и следствие. Просто представьте как бы вы смогли реализовать вызов хуков без этих вот ограничений, не выдавая им уникальные ID-ки? Условно если бы автор React сделали так:


        const [state, setState] = useState('uniqId1', 0);
        const onClick = useCallback('uniqId2', ...);
        useEffect('uniqId3', ...);
        // etc

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


        Касательно "условного рендеринга компонент". Come on, при условном render-е целых компонент React их монтирует и демонтирует. Включая работу по DOM-у. Это вот прямо несопоставимые по затратам операции. Зачем вы это связали с порядков вызова хуков — мне не понять. Особенно вот это:


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

        Хех. Подсказка — вы можете в runtime случайным образом создавать новые компоненты, которые случайным образом будут вызывать любые хуки в любом порядке. Главное чтобы не больше 1-го рендера :)


        P.S. автору: если делать нечего и скучно — посмотрите как работает определение древа вызова хуков (включая custom-ые, конечно) в React Dev Tools. Много интересного откроете :)

          +1

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

            0
            Спасибо за RFC, было интересно почитать

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

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