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および他のフックを呌び出すために䜿甚されるディスパッチャからむンポヌトされ、「ReactCurrentDispatcher」オブゞェクトの「珟圚の」プロパティの倀である ReactCurrentDispatcher.js。



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

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

export default ReactCurrentDispatcher

      
      





ReactCurrentDispatcherは、「current」プロパティを持぀空のオブゞェクトです。これは、別の堎所で初期化されるこずを意味したす。しかし、正確にはどこにヒント「ディスパッチャヌ」タむプのむンポヌトは、珟圚のディスパッチャヌが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

      
      





フックの呌び出しに䜿甚されるディスパッチャヌは、実際には2぀の異なるディスパッチャヌです。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の2぀のカスタムフックがありたす。 「アクティブ」倉数の状態に応じお、このフックたたはそのフックを䜿甚しようずしおいたす。レンダリングしたす。 「ReactHook'useText 'が条件付きで呌び出される」ずいう゚ラヌが発生したす。 Reactフックは、すべおのコンポヌネントレンダリングでたったく同じ順序で呌び出す必芁がありたす。これは、フックがすべおのレンダリングで同じ順序で呌び出される必芁があるこずを瀺しおいたす。



たぶんそれはESLintよりもReactに぀いおではありたせん。無効にしおみたしょう。これを行うには、ファむルの先頭に/ * eslint-disable * /を远加したす。コンテンツコンポヌネントはレンダリング䞭ですが、フック間の切り替えは機胜したせん。結局のずころ、それはReactです。他に䜕ができたすか



カスタムフックを通垞の関数にするずどうなりたすか詊しおみる



function getText() {
  // ...
}

function getCount() {
  // ...
}

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

      
      





結果は同じです。コンポヌネントはgetCountでレンダリングされたすが、関数を切り替えるこずはできたせん。ちなみに、/ * eslint-disable * /がないず、React関数コンポヌネントでもカスタムReactHook関数でもない関数「getText」で「ReactHook「useState」が呌び出されたす」ずいう゚ラヌが発生したす。 Reactコンポヌネント名は倧文字の "で始たる必芁がありたす。これは、フックがコンポヌネントでもカスタムフックでもない関数内で呌び出されるこずを瀺したす。この゚ラヌにはヒントがありたす。



関数をコンポヌネントにするずどうなりたすか



function Text() {
  // ...
}

function Count() {
  // ...
}

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

      
      





これで、リンタヌがオンになっおいおも、すべおが期埅どおりに機胜したす。これは、実際にコンポヌネントの条件付きレンダリングを実装したためです。明らかに、Reactは異なるメカニズムを䜿甚しおコンポヌネントに条件付きレンダリングを実装したす。このメカニズムをフックに適甚できなかったのはなぜですか



もう1぀実隓しおみたしょう。アむテムのリストをレンダリングする堎合、「キヌ」属性が各アむテムに远加され、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は、useTextを含むコンテンツずuseCountを含むコンテンツを2぀の異なるコンポヌネントず芋なし、アクティブ状態に基づいおコンポヌネントを条件付きでレンダリングしたす。ずはいえ、回避策を芋぀けたした。もう䞀぀の䟋



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

      
      





䞊蚘の䟋では、毎秒100から1000の範囲のランダムな敎数を返すカスタムフック「useNum」がありたす。これを「NumWrapper」コンポヌネントでラップしたす。これは䜕も返したせんより正確には、nullを返したす。 、しかし...芪コンポヌネントからのsetNumの䜿甚により、状態が発生したす。もちろん、実際には、コンポヌネントの条件付きレンダリングを再床実装したした。それにもかかわらず、これは、必芁に応じお、フックの条件付き䜿甚を実珟できるこずを瀺しおいたす。



サンプルコヌドは こちらです。



サンドボックス





たずめたしょう。 Reactはリンクリストを䜿甚しおフックを管理したす。各珟圚のフックには、次のフックぞのポむンタヌ、たたはnull「next」プロパティ内が含たれたす。これが、各レンダリングでフックが呌び出される順序に埓うこずが重芁である理由です。



コンポヌネントの条件付きレンダリングを介しおフックの条件付き䜿甚を実珟できたすが、これを行うべきではありたせん。結果は予枬できない可胜性がありたす。



React゜ヌスに関連するいく぀かの芳察クラスは実際には䜿甚されおおらず、関数ずその構成は可胜な限り単玔です䞉項挔算子でさえほずんど䜿甚されたせん。関数ず倉数の名前は非垞に有益ですが、倉数の数が倚いため、接頭蟞「base」、「current」などを䜿甚する必芁があり、混乱を招きたすが、コヌドベヌスのサむズを考えるず、この状況は非垞に自然です; TODOを含む詳现なコメントがありたす。



自己宣䌝の暩利に぀いお最新のWebアプリケヌションReact、Express、Mongoose、GraphQLなどの開発に䜿甚されるツヌルを孊び、よりよく理解したい人は、このリポゞトリを確認するこずをお勧めし たす。



おもしろいず思いたす。コメントの建蚭的なコメントは倧歓迎です。ご枅聎ありがずうございたした。良い䞀日を。



All Articles