Uma nota sobre como os ganchos funcionam no React





Bom dia amigos!



Quero compartilhar com vocês alguns insights sobre como o React funciona, ou seja, suposições sobre por que os ganchos não podem ser usados ​​em ifs, loops, funções regulares, etc. E eles realmente não podem ser usados ​​dessa maneira?



A questão é: por que os ganchos podem ser usados ​​apenas no nível superior? Aqui está o que a documentação oficial diz sobre isso.



Vamos começar com as regras para usar ganchos .



Use ganchos apenas no nível superior (destaque os pontos-chave para prestar atenção):



“Não chame ganchos dentro de loops, condicionais ou funções aninhadas. Em vez disso, sempre use ganchos apenas dentro das funções React, antes de retornar qualquer valor delas. Essa regra garante que os ganchos sejam chamados na mesma sequência sempre que o componente for renderizado . Isso permitirá que o React persista adequadamente o estado do gancho entre várias chamadas para useState e useEffect. (Se você estiver interessado, uma explicação detalhada está abaixo.) "



Estamos interessados, veja abaixo.



Explicação (exemplos omitidos por questões de brevidade):



"… React useState? : React .… , React . , ?… . React , useState. React , persistForm, , . , , , , .… .… , ..."



Claro? Sim, de alguma forma, não muito. O que você quer dizer com "React depende da ordem em que os ganchos são chamados"? Como ele faz isso? O que é esse “algum tipo de estado interno”? Quais são os erros causados ​​pela falta de um gancho na nova renderização? Esses erros são críticos para o funcionamento do aplicativo?



Há mais alguma coisa na documentação sobre isso? Existe uma seção especial "Ganchos: Respostas às Perguntas" . Lá encontramos o seguinte.



Como o React vincula chamadas de gancho a um componente?



«React , .… , . JavaScript-, . , useState(), ( ) . useState() .»



Já algo. Uma lista interna de locais de memória associados a componentes e contendo alguns dados. O gancho lê o valor da célula atual e move o ponteiro para a próxima. De que estrutura de dados isso o lembra? Talvez estejamos falando sobre uma lista vinculada (vinculada) .



Se este for realmente o caso, a sequência de ganchos que o React gera quando renderiza pela primeira vez se parece com isto (imagine que os retângulos são ganchos, cada gancho contém um ponteiro para o próximo):





Ótimo, temos uma hipótese de trabalho que parece mais ou menos razoável. Como podemos verificar isso? Uma hipótese é uma hipótese, mas quero fatos. E para saber os fatos, você deve ir ao GitHub, ao repositório de origem do React .



Não pense que imediatamente decidi dar um passo tão desesperado. Claro, primeiro, em busca de respostas para minhas dúvidas, recorri ao onisciente Google. Aqui está o que encontramos:





Todas essas fontes referem-se às fontes React. Eu tive que cavar um pouco neles. Portanto, a tese e o exemplo de "useState".



O useState () e outros ganchos são implementados em ReactHooks.js :



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

      
      





Um despachante é usado para chamar useState () (e outros ganchos). No início do mesmo arquivo, vemos o seguinte:



import ReactCurrentDispatcher from './ReactCurrentDispatcher'

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current

  return ((dispatcher: any): Dispatcher)
}

      
      





O despachante usado para chamar useState () (e outros ganchos) é o valor da propriedade "current" do objeto "ReactCurrentDispatcher", que é importado de ReactCurrentDispatcher.js :



import type { Dispatcher } from 'react-reconciler/src/ReactInternalTypes'

const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher)
}

export default ReactCurrentDispatcher

      
      





ReactCurrentDispatcher é um objeto vazio com uma propriedade "atual". Isso significa que ele foi inicializado em outro lugar. Mas onde exatamente? Dica: as importações do tipo "Dispatcher" indicam que o despachante atual tem algo a ver com os componentes internos do React. Na verdade, isso é o que encontramos em ReactFiberHooks.new.js (o número no comentário é o número da linha):



// 118
const { ReactCurrentDispatcher, ReactCurrentBatchConfig } = ReactSharedInternals

      
      





No entanto, em ReactSharedInternals.js encontramos "dados internos secretos que podem ser acionados para usar":



const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

export default ReactSharedInternals

      
      





Isso é tudo? Nossa busca chegou ao fim antes de começar? Na verdade. Não saberemos os detalhes da implementação interna do React, mas não precisamos deles para entender como o React lida com os ganchos. De volta a ReactFiberHooks.new.js:



// 405
ReactCurrentDispatcher.current =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate

      
      





O despachante usado para chamar os ganchos é na verdade dois despachantes diferentes - HooksDispatcherOnMount (na montagem) e HooksDispatcherOnUpdate (na atualização, re-renderizar).



// 2086
const HooksDispatcherOnMount: Dispatcher = {
  useState: mountState,
  //     -
}

// 2111
const HooksDispatcherOnUpdate: Dispatcher = {
  useState: updateState,
  //     -
}

      
      





A separação de montagem / atualização é mantida no nível do gancho.



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))
}

      
      





A função "updateReducer" é usada para atualizar o estado, então dizemos que useState usa internamente useReducer ou que useReducer é uma implementação de nível inferior de 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]
}

      
      





Até agora, vimos apenas como os próprios ganchos funcionam. Onde está a lista? Dica: ganchos de montagem / atualização são criados usando as funções "mountWorkInProgressHook" e "updateWorkInProgressHook", respectivamente.



// 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
}

      
      





Acredito que nossa hipótese de que uma lista vinculada é usada para controlar ganchos foi confirmada. Descobrimos que cada gancho tem uma propriedade "próximo", cujo valor é um link para o próximo gancho. Aqui está uma boa ilustração desta lista do artigo acima:







Para aqueles que estão se perguntando, eis como é a implementação de JavaScript mais simples de uma lista vinculada unilateral:



Um pouco de código
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

      
      







Acontece que ao renderizar novamente com menos (ou mais) ganchos, updateWorkInProgressHook () retorna um gancho que não corresponde à sua posição na lista anterior, ou seja, a nova lista não terá um nó (ou um nó adicional aparecerá). E, no futuro, o estado memorizado incorreto será usado para calcular o novo estado. Claro, este é um problema sério, mas quão crítico ele é? O React não sabe como reconstruir a lista de anzóis na hora? E há alguma maneira de implementar ganchos condicionais? Vamos descobrir isso.



Sim, antes de partirmos da origem, procuraremos um linter que aplique as regras de uso de ganchos. 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 })
  }
}

      
      





Não vou entrar em detalhes sobre como a diferença entre o número de anzóis é determinada. E aqui está como definir que uma função é um gancho:



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
  }
}

      
      





Vamos esboçar um componente no qual ocorre o uso condicional de ganchos e ver o que acontece quando ele é renderizado.



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

      
      





No exemplo acima, temos dois ganchos personalizados - useText () e useCount (). Estamos tentando usar este ou aquele gancho dependendo do estado da variável "ativa". Render. Obtemos o erro "React Hook 'useText' é chamado condicionalmente. React Hooks devem ser chamados exatamente na mesma ordem em cada renderização do componente ", que diz que os hooks devem ser chamados na mesma ordem em cada render.



Talvez não seja tanto sobre React quanto sobre ESLint. Vamos tentar desativá-lo. Para fazer isso, adicione / * eslint-disable * / no início do arquivo. O componente Conteúdo agora está sendo renderizado, mas alternar entre ganchos não funciona. Afinal, é React. O que mais você pode fazer?



E se criarmos funções regulares de ganchos personalizados? Tentando:



function getText() {
  // ...
}

function getCount() {
  // ...
}

const Content = ({ active }) => <p>{active ? getText() : getCount()}</p>

      
      





O resultado é o mesmo. O componente é renderizado com getCount (), mas não é possível alternar entre as funções. A propósito, sem / * eslint-disable * / obtemos o erro “React Hook“ useState ”é chamado na função“ getText ”que não é um componente da função React nem uma função React Hook customizada. Os nomes dos componentes do React devem começar com uma letra maiúscula ", o que indica que o gancho é chamado dentro de uma função que não é um componente nem um gancho personalizado. Existe uma dica neste erro.



E se fizermos nossos componentes de funções?



function Text() {
  // ...
}

function Count() {
  // ...
}

const Content = ({ active }) => <p>{active ? <Text /> : <Count />}</p>

      
      





Agora tudo funciona conforme o esperado, mesmo com o linter ligado. Isso ocorre porque, na verdade, implementamos a renderização condicional dos componentes. Obviamente, o React usa um mecanismo diferente para implementar a renderização condicional nos componentes. Por que esse mecanismo não pode ser aplicado a ganchos?



Vamos fazer mais uma experiência. Sabemos que, no caso de renderizar uma lista de itens, um atributo "chave" é adicionado a cada item, permitindo que o React acompanhe o estado da lista. E se usarmos esse atributo em nosso exemplo?



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} />
    </>
  )
}

      
      





Recebemos um erro com o linter. Sem linter ... tudo funciona! Mas por que? Talvez o React considere Content com useText () e Content com useCount () como dois componentes diferentes e renderiza condicionalmente os componentes com base no estado ativo. Seja como for, encontramos uma solução alternativa. Outro exemplo:



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

      
      





No exemplo acima, temos um gancho personalizado "useNum" que a cada segundo retorna um inteiro aleatório no intervalo de 100 a 1000. Nós o envolvemos no componente "NumWrapper", que não retorna nada (mais precisamente, ele retorna nulo ), mas ... devido ao uso de setNum do componente pai, o estado é elevado. Claro, de fato, implementamos a renderização condicional do componente novamente. No entanto, isso mostra que, se desejado, ainda é possível conseguir o uso condicional de anzóis.



O código de exemplo está aqui .



Caixa de areia:





Vamos resumir. O React usa uma lista vinculada para gerenciar ganchos. Cada gancho (atual) contém um ponteiro para o próximo gancho ou nulo (na propriedade "próximo"). É por isso que é importante seguir a ordem em que os ganchos são chamados em cada render.



Embora você possa obter o uso condicional de ganchos por meio da renderização condicional de componentes, você não deve fazer isso: as consequências podem ser imprevisíveis.



Mais algumas observações relacionadas às fontes do React: as classes praticamente não são usadas e as funções e suas composições são as mais simples possíveis (mesmo o operador ternário raramente é usado); os nomes das funções e variáveis ​​são bastante informativos, embora devido ao grande número de variáveis ​​seja necessário utilizar os prefixos "base", "atual", etc., o que provoca alguma confusão, mas dado o tamanho da base de código , essa situação é bastante natural; há comentários detalhados, incluindo TODO.



Sobre os direitos de autopromoção: para quem deseja conhecer ou entender melhor as ferramentas utilizadas no desenvolvimento de aplicações web modernas (React, Express, Mongoose, GraphQL, etc.), sugiro que dê uma olhada neste repositório .



Espero que você tenha achado interessante. Comentários construtivos nos comentários são bem-vindos. Obrigado pela atenção e tenha um bom dia.



All Articles