Catatan tentang cara kerja hook di React





Selamat siang teman!



Saya ingin berbagi dengan Anda beberapa wawasan tentang cara kerja React, yaitu, asumsi tentang mengapa hook tidak dapat digunakan dalam ifs, loop, regular functions, dll. Dan apakah mereka benar-benar tidak dapat digunakan dengan cara ini?



Pertanyaannya adalah: mengapa pengait hanya dapat digunakan di tingkat atas? Inilah yang dikatakan dokumentasi resmi tentang itu.



Mari kita mulai dengan aturan penggunaan hook .



Gunakan pengait hanya di tingkat atas (disorot poin-poin penting yang harus diperhatikan):



“Jangan panggil hook di dalam loop, conditionals, atau nested functions. Sebagai gantinya, selalu gunakan hook hanya di dalam fungsi React, sebelum mengembalikan nilai apa pun darinya. Aturan ini memastikan bahwa hook dipanggil dalam urutan yang sama setiap kali komponen dirender . Ini akan memungkinkan React untuk mempertahankan status hook dengan benar di antara beberapa panggilan ke useState dan useEffect. (Jika Anda tertarik, penjelasan rinci ada di bawah.) "



Kami tertarik, lihat di bawah.



Penjelasan (contoh dihilangkan agar singkat):



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



Bersih? Ya, entah kenapa tidak terlalu banyak. Apa maksudmu, "React bergantung pada urutan pengait yang dipanggil"? Bagaimana dia melakukannya? Apakah "semacam keadaan batin" ini? Apa kesalahan yang disebabkan oleh hilangnya hook saat render ulang? Apakah kesalahan ini penting agar aplikasi berfungsi?



Apakah ada hal lain dalam dokumentasi tentang ini? Ada bagian khusus "Hooks: Answers to Questions" . Di sana kami menemukan yang berikut ini.



Bagaimana React bind hook memanggil sebuah komponen?



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



Sudah sesuatu. Daftar internal lokasi memori yang terkait dengan komponen dan berisi beberapa data. Pengait membaca nilai sel saat ini dan memindahkan penunjuk ke sel berikutnya. Struktur data apa yang mengingatkan Anda tentang hal ini? Mungkin kita berbicara tentang daftar yang ditautkan (linked) .



Jika ini memang masalahnya, maka urutan hook yang dihasilkan React saat pertama kali membuat terlihat seperti ini (bayangkan persegi panjang adalah hook, setiap hook berisi pointer ke hook berikutnya):





Hebat, kami memiliki hipotesis kerja yang terlihat kurang lebih masuk akal. Bagaimana cara kami memeriksanya? Hipotesis adalah hipotesis, tetapi saya ingin fakta. Dan untuk fakta, Anda harus pergi ke GitHub, ke repositori sumber React .



Jangan mengira bahwa saya langsung memutuskan untuk mengambil langkah putus asa seperti itu. Tentu saja, pertama, untuk mencari jawaban atas pertanyaan saya, saya beralih ke Google yang mahatahu. Inilah yang kami temukan:





Semua sumber ini mengacu pada sumber React. Saya harus menggali sedikit di dalamnya. Jadi, tesis dan contoh "useState".



UseState () dan hook lainnya diimplementasikan di ReactHooks.js :



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

      
      





Dispatcher digunakan untuk memanggil useState () (dan hook lainnya). Di awal file yang sama, kita melihat yang berikut ini:



import ReactCurrentDispatcher from './ReactCurrentDispatcher'

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current

  return ((dispatcher: any): Dispatcher)
}

      
      





Dispatcher yang digunakan untuk memanggil useState () (dan hook lainnya) adalah nilai properti "current" dari objek "ReactCurrentDispatcher", yang diimpor dari ReactCurrentDispatcher.js :



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

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

export default ReactCurrentDispatcher

      
      





ReactCurrentDispatcher adalah objek kosong dengan properti "saat ini". Ini berarti bahwa ini diinisialisasi di tempat lain. Tapi di mana tepatnya? Petunjuk: impor jenis "Dispatcher" menunjukkan bahwa dispatcher saat ini ada hubungannya dengan internal React. Memang, inilah yang kami temukan di ReactFiberHooks.new.js (nomor di komentar adalah nomor baris):



// 118
const { ReactCurrentDispatcher, ReactCurrentBatchConfig } = ReactSharedInternals

      
      





Namun, di ReactSharedInternals.js kita mengalami "data internal rahasia yang dapat diaktifkan untuk digunakan":



const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

export default ReactSharedInternals

      
      





Apakah itu semuanya? Apakah pencarian kita sudah berakhir sebelum bisa dimulai? Tidak juga. Kami tidak akan mengetahui detail implementasi internal React, tapi kami tidak membutuhkannya untuk memahami bagaimana React menangani hook. Kembali ke ReactFiberHooks.new.js:



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

      
      





Dispatcher yang digunakan untuk memanggil hook sebenarnya adalah dua dispatcher yang berbeda - HooksDispatcherOnMount (on mount) dan HooksDispatcherOnUpdate (saat update, render ulang).



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

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

      
      





Pemisahan mount / update dipertahankan pada level hook.



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

      
      





Fungsi "updateReducer" digunakan untuk mengupdate status, jadi kami mengatakan bahwa useState secara internal menggunakan useReducer atau useReducer adalah implementasi useState tingkat lebih rendah.



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

      
      





Sejauh ini, kami hanya melihat cara kerja pengait itu sendiri. Dimana daftarnya? Petunjuk: kait pemasangan / pembaruan dibuat menggunakan fungsi "mountWorkInProgressHook" dan "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
}

      
      





Saya yakin hipotesis kami bahwa daftar tertaut digunakan untuk mengontrol pengait telah dikonfirmasi. Kami menemukan bahwa setiap hook memiliki properti "next", yang nilainya adalah link ke hook berikutnya. Berikut ilustrasi yang bagus dari daftar ini dari artikel di atas:







Bagi Anda yang bertanya-tanya, berikut adalah implementasi JavaScript paling sederhana dari daftar tertaut satu arah:



Sedikit kode
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

      
      







Ternyata saat rendering ulang dengan lebih sedikit (atau lebih) hook, updateWorkInProgressHook () mengembalikan hook yang tidak cocok dengan posisinya di daftar sebelumnya, yaitu. daftar baru akan kehilangan satu node (atau node tambahan akan muncul). Dan kedepannya, state memoized yang salah akan digunakan untuk menghitung state baru. Tentu saja, ini adalah masalah yang serius, tetapi seberapa kritisnya? Tidakkah React tahu bagaimana membangun kembali daftar hook dengan cepat? Dan apakah ada cara untuk menerapkan pengait bersyarat? Mari kita cari tahu.



Ya, sebelum kita pergi dari sumber, kita akan mencari linter yang memberlakukan aturan penggunaan hook. 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 })
  }
}

      
      





Saya tidak akan menjelaskan secara rinci tentang bagaimana perbedaan antara jumlah kait ditentukan. Dan inilah cara mendefinisikan bahwa suatu fungsi adalah hook:



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

      
      





Mari membuat sketsa komponen tempat penggunaan hook bersyarat, dan lihat apa yang terjadi saat dirender.



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

      
      





Dalam contoh di atas, kami memiliki dua kait khusus - useText () dan useCount (). Kami mencoba menggunakan hook ini atau itu tergantung pada status variabel "aktif". Memberikan. Kami mendapatkan kesalahan "React Hook 'useText' disebut bersyarat. React Hooks harus dipanggil dengan urutan yang sama persis di setiap render komponen ", yang mengatakan bahwa hook harus dipanggil dalam urutan yang sama di setiap render.



Mungkin ini bukan tentang React melainkan tentang ESLint. Mari coba nonaktifkan. Untuk melakukan ini, tambahkan / * eslint-disable * / di awal file. Komponen Konten sekarang sedang dirender, tetapi beralih di antara pengait tidak berfungsi. Jadi ini React. Apa lagi yang bisa kamu lakukan?



Bagaimana jika kita membuat fungsi biasa kait kustom? Mencoba:



function getText() {
  // ...
}

function getCount() {
  // ...
}

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

      
      





Hasilnya sama saja. Komponen dirender dengan getCount (), tetapi tidak mungkin untuk beralih antar fungsi. Ngomong-ngomong, tanpa / * eslint-disable * / kita mendapatkan error “React Hook“ useState ”dipanggil dalam fungsi“ getText ”yang bukan merupakan komponen fungsi React atau fungsi React Hook kustom. Nama komponen React harus dimulai dengan huruf besar ", yang mengatakan bahwa hook dipanggil di dalam fungsi yang bukan merupakan komponen maupun hook kustom. Ada petunjuk dalam kesalahan ini.



Bagaimana jika kita membuat komponen fungsi kita?



function Text() {
  // ...
}

function Count() {
  // ...
}

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

      
      





Sekarang semuanya bekerja seperti yang diharapkan, bahkan dengan linter dihidupkan. Ini karena kami benar-benar menerapkan rendering kondisional dari komponen. Jelas, React menggunakan mekanisme berbeda untuk mengimplementasikan rendering kondisional pada komponen. Mengapa mekanisme ini tidak dapat diterapkan pada kait?



Mari lakukan satu percobaan lagi. Kita tahu bahwa dalam kasus merender daftar item, atribut "key" ditambahkan ke setiap item, memungkinkan React untuk melacak status daftar. Bagaimana jika kita menggunakan atribut ini dalam contoh kita?



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

      
      





Kami mendapatkan kesalahan dengan linter. Tanpa linter ... semuanya bekerja! Tapi kenapa? Mungkin React menganggap Konten dengan useText () dan Konten dengan useCount () sebagai dua komponen berbeda dan merender komponen secara kondisional berdasarkan status aktif. Bagaimanapun, kami menemukan solusi. Contoh lain:



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

      
      





Dalam contoh di atas, kami memiliki hook kustom "useNum" yang setiap detik mengembalikan integer acak dalam rentang dari 100 hingga 1000. Kami membungkusnya dalam komponen "NumWrapper", yang tidak mengembalikan apa pun (lebih tepatnya, mengembalikan null ), tapi ... karena penggunaan setNum dari komponen induk, status dimunculkan. Tentu saja, pada kenyataannya, kami telah mengimplementasikan rendering kondisional dari komponen itu lagi. Namun demikian, ini menunjukkan bahwa, jika diinginkan, masih mungkin untuk mencapai penggunaan kait bersyarat.



Kode contoh ada di sini .



Bak pasir:





Mari kita rangkum. React menggunakan daftar tertaut untuk mengelola hook. Setiap hook (saat ini) berisi pointer ke hook berikutnya, atau null (dalam properti "next"). Inilah mengapa penting untuk mengikuti urutan pemanggilan hook pada setiap render.



Meskipun Anda dapat mencapai penggunaan hook bersyarat melalui rendering komponen bersyarat, Anda tidak boleh melakukan ini: konsekuensinya tidak dapat diprediksi.



Beberapa observasi lagi terkait dengan sumber React: class secara praktis tidak digunakan, dan fungsi serta komposisinya sesederhana mungkin (bahkan operator terner jarang digunakan); nama-nama fungsi dan variabel cukup informatif, meskipun karena jumlah variabel yang banyak, maka perlu menggunakan prefiks "base", "current", dll., yang menyebabkan kebingungan, tetapi mengingat ukuran basis kode , situasi ini sangat wajar; ada komentar rinci, termasuk TODO.



Tentang hak promosi diri: bagi mereka yang ingin mempelajari atau lebih memahami alat yang digunakan dalam pengembangan aplikasi web modern (React, Express, Mongoose, GraphQL, dll.), Saya sarankan untuk melihat repositori ini .



Semoga Anda tertarik. Komentar konstruktif di kolom komentar dipersilahkan. Terima kasih atas perhatiannya dan semoga harimu menyenangkan.



All Articles