BerandaComputers and TechnologyMendalam: Bagaimana cara kerja React hooks?

Mendalam: Bagaimana cara kerja React hooks?

Catatan Penulis: Artikel ini telah berubah menjadi ceramah , dengan lebih banyak konteks. Juga, artikel ini tidak menyebutkan Penjadwal React atau bagaimana status sebenarnya disimpan di React .

Pengait adalah cara yang pada dasarnya lebih sederhana untuk merangkum perilaku stateful dan efek samping dalam antarmuka pengguna. Mereka pertama kali diperkenalkan di React dan telah diterapkan secara luas oleh framework lain seperti Vue , Svelte , dan bahkan diadaptasi untuk JS fungsional umum . Namun, desain fungsionalnya membutuhkan pemahaman yang baik tentang closure di JavaScript.

Dalam artikel ini, kami memperkenalkan kembali closure dengan membuat tiruan kecil React Hooks. Ini akan melayani dua tujuan – untuk mendemonstrasikan penggunaan yang efektif dari closure, dan untuk menunjukkan bagaimana Anda bisa membuat klon Hooks hanya dalam 29 baris JS yang dapat dibaca. Akhirnya, kita sampai pada bagaimana Custom Hooks muncul secara alami.

⚠️ Catatan: Anda tidak perlu melakukan semua ini untuk memahami Hooks. Ini mungkin hanya membantu fundamental JS Anda jika Anda melakukan latihan ini. Jangan khawatir, tidak terlalu sulit!

Apa itu Penutupan?

Salah satu Banyak nilai jual menggunakan hook adalah untuk menghindari kompleksitas kelas dan komponen pesanan yang lebih tinggi sama sekali. Namun, dengan pengait, beberapa orang merasa kami mungkin telah menukar satu masalah dengan masalah lainnya. Alih-alih mengkhawatirkan konteks terikat , sekarang kita harus khawatir tentang penutupan . Seperti Mark Dalgleish diringkas dengan baik :

A Star Wars meme about React Hooks and closures

Penutupan adalah konsep fundamental di JS. Terlepas dari ini, mereka terkenal karena membingungkan banyak pengembang terutama yang lebih baru. Kyle Simpson dari Anda Tidak Tahu JS ketenaran mendefinisikan penutupan seperti itu:

Penutupan adalah ketika suatu fungsi dapat mengingat dan mengakses cakupan leksikal bahkan ketika fungsi tersebut dijalankan di luar cakupan leksikal.

Mereka jelas terkait erat dengan konsep pelingkupan leksikal, yang MDN mendefinisikan sebagai “bagaimana parser menyelesaikan nama variabel saat fungsi disarangkan”. Mari kita lihat contoh praktis untuk menggambarkan ini dengan lebih baik:

  // Contoh 0 function useState (initialValue) {   var _val=initialValue // _val adalah variabel lokal yang dibuat oleh useState   status fungsi () {     // state adalah fungsi dalam, closure     return _val // state () menggunakan _val, dideklarasikan oleh fungsi induk   }   function setState (newVal) {     // sama     _val=newVal // menyetel _val tanpa memperlihatkan _val   }   return [state, setState] // memperlihatkan fungsi untuk penggunaan eksternal } var [foo, setFoo]=useState (0) // using array destructuring console.log (foo ()) // logs 0 - initialValue yang kami berikan setFoo (1) // set _val di dalam cakupan useState console.log (foo ()) // logs 1 - new initialValue, meskipun panggilannya sama persis  

Di sini, kami membuat tiruan primitif dari React useState pengait. Dalam fungsi kami, ada 2 fungsi dalam, negara dan setState . negara mengembalikan variabel lokal _ val didefinisikan di atas dan setState menetapkan variabel lokal ke parameter yang diteruskan ke dalamnya (yaitu newVal ).

Implementasi kami dari negara di sini adalah fungsi pengambil, yang tidak ideal , tetapi kami ‘ akan memperbaikinya sebentar lagi. Yang penting adalah dengan foo dan setFoo , kami dapat mengakses dan memanipulasi (alias “Menutup”) variabel internal _ val . Mereka mempertahankan akses ke useState lingkup , dan referensi itu disebut penutupan. Dalam konteks React dan framework lainnya, ini terlihat seperti status, dan memang seperti itulah.

Jika Anda ingin menyelam lebih dalam tentang penutupan, saya sarankan membaca MDN , YDKJS , dan DailyJS tentang topik tersebut, tetapi jika Anda memahami contoh kode di atas, Anda memiliki semua yang Anda butuhkan.

Penggunaan dalam Komponen Fungsi

Mari terapkan useState klon dalam pengaturan yang tampak familier. Kami akan membuat Komponen penghitung !

 // Contoh 1 function Counter () {   const [count, setCount]=useState (0) // useState sama seperti di atas   kembali {     klik: ()=> setCount (count () + 1),     render: ()=> console.log ('render:', {count: count ()})   } } const C=Penghitung () C.render () // render: {count: 0} C. klik () C.render () // render: {count: 1}  

Di sini, alih-alih merender ke DOM, kami memilih untuk hanya console.log di luar negara bagian kita. Kami juga mengekspos API terprogram untuk Penghitung kami sehingga kami dapat menjalankannya dalam skrip alih-alih melampirkan penangan kejadian. Dengan desain ini kami dapat mensimulasikan rendering komponen kami dan bereaksi terhadap tindakan pengguna.

Meskipun ini berfungsi, memanggil getter untuk mengakses status bukanlah API yang sebenarnya React.useState pengait. Mari kita perbaiki itu.

Penutupan Basi

Jika kita ingin mencocokkan React API yang sebenarnya, status kita harus berupa variabel, bukan fungsi. Jika kita hanya mengekspos _val sebagai ganti membungkusnya dalam sebuah fungsi, kami akan menemukan bug:

  // Contoh 0, ditinjau kembali - ini BUGGY! function useState (initialValue) {   var _val=initialValue   // tidak ada fungsi state ()   function setState (newVal) {     _val=newVal   }   return [_val, setState] // mengekspos _val secara langsung } var [foo, setFoo]=useState (0) console.log (foo) // mencatat 0 tanpa membutuhkan pemanggilan fungsi setFoo (1) // set _val di dalam cakupan useState console.log (foo) // log 0 - oops !!  

Ini adalah salah satu bentuk dari masalah Penutupan Basi. Ketika kami merusak foo dari keluaran useState , itu mengacu pada _ val sejak awal useState panggilan… dan tidak pernah berubah lagi! Ini bukan yang kita inginkan; kita biasanya membutuhkan status komponen kita untuk mencerminkan saat ini , sementara hanya variabel alih-alih panggilan fungsi! Kedua tujuan tersebut tampaknya bertentangan.

Penutupan dalam Modul

Kami dapat menyelesaikan useState teka-teki dengan … memindahkan penutupan kami ke dalam penutupan lain! ( Yo dawg Saya mendengar Anda suka penutupan… )

  // Contoh 2 const MyReact=(function () {   biarkan _val // menahan status kita dalam lingkup modul   kembali {     render (Komponen) {       const Comp=Komponen ()       Comp.render ()       kembali Comp     },     useState (initialValue) {       _val=_val || initialValue // tetapkan lagi setiap proses       function setState (newVal) {         _val=newVal       }       kembali [_val, setState]     }   } }) ()  

Di sini kami telah memilih untuk menggunakan pola Modul untuk membuat tiruan React kecil kami. Seperti React, ini melacak status komponen (dalam contoh kami, ini hanya melacak satu komponen, dengan status dalam _ val ). Desain ini memungkinkan MyReact untuk “merender” Anda komponen fungsi, yang memungkinkannya untuk menetapkan internal _ val nilai setiap kali dengan penutupan yang benar:

  // Contoh 2 lanjutan function Counter () {   const [count, setCount]=MyReact.useState (0)   kembali {     klik: ()=> setCount (count + 1),     render: ()=> console.log ('render:', {count})   } } biarkan App App=MyReact.render (Counter) // render: {count: 0} App.click () App=MyReact.render (Counter) // render: {count: 1}  

Sekarang ini lebih mirip React with Hooks!

Kamu bisa baca lebih lanjut tentang pola dan penutupan Modul di YDKJS .

Mereplikasi useEffect

Sejauh ini, kami telah membahas useState , yang merupakan React Hook dasar pertama. Pengait terpenting berikutnya adalah useEffect . Tidak seperti setState , useEffect dijalankan secara tidak sinkron, yang berarti lebih banyak peluang untuk mengalami masalah penutupan.

Kami dapat memperluas model kecil React yang telah kami bangun sejauh ini untuk menyertakan ini:

  // Contoh 3 const MyReact=(function () {   biarkan _val, _deps // menahan status dan dependensi kami dalam cakupan   kembali {     render (Komponen) {       const Comp=Komponen ()       Comp.render ()       kembali Comp     },     useEffect (callback, depArray) {       const hasNoDeps=! depArray       const hasChangedDeps=_deps? ! depArray.every ((el, i)=> el===_deps [i]): benar       if (hasNoDeps || hasChangedDeps) {         panggilan balik ()         _deps=depArray       }     },     useState (initialValue) {       _val=_val || initialValue       function setState (newVal) {         _val=newVal       }       kembali [_val, setState]     }   } }) ()  // penggunaan function Counter () {   const [count, setCount]=MyReact.useState (0)   MyReact.useEffect (()=> {     console.log ('efek', hitung)   }, [count])   kembali {     klik: ()=> setCount (count + 1),     noop: ()=> setCount (hitungan),     render: ()=> console.log ('render', {count})   } } biarkan App App=MyReact.render (Penghitung) // efek 0 // render {count: 0} App.click () App=MyReact.render (Penghitung) // efek 1 // render {count: 1} App.noop () App=MyReact.render (Penghitung) // // tidak ada efek yang dijalankan // render {count: 1} App.click () App=MyReact.render (Penghitung) // efek 2 // render {count: 2}  

Untuk melacak dependensi (sejak useEffect dijalankan ulang saat dependensi berubah), kami memperkenalkan variabel lain untuk dilacak _ deps .

Bukan Sihir, hanya Array

Kami memiliki tiruan yang cukup bagus dari useState dan useEffect , tetapi keduanya diimplementasikan dengan buruk lajang (hanya satu dari setiap yang dapat ada atau bug terjadi). Untuk melakukan sesuatu yang menarik (dan untuk membuat contoh penutupan basi menjadi mungkin), kita perlu menggeneralisasikan mereka untuk mengambil jumlah keadaan dan efek yang berubah-ubah. Untungnya, sebagai Rudi Yardley telah menulis , React Hooks bukanlah sihir, hanya array. Jadi kita akan memiliki kait array. Kami juga akan mengambil kesempatan untuk menutup keduanya _ val dan _ deps ke dalam kait array karena tidak pernah tumpang tindih:

  // Contoh 4 const MyReact=(function () {   biarkan kait=[],     currentHook=0 // larik kait, dan iterator!   kembali {     render (Komponen) {       const Comp=Component () // menjalankan efek       Comp.render ()       currentHook=0 // setel ulang untuk render berikutnya       kembali Comp     },     useEffect (callback, depArray) {       const hasNoDeps=! depArray       const deps=hooks [currentHook] // type: array | tidak terdefinisi       const hasChangedDeps=deps? ! depArray.every ((el, i)=> el===deps [i]): benar       if (hasNoDeps || hasChangedDeps) {         panggilan balik ()         hooks [currentHook]=depArray       }       currentHook ++ // selesai dengan hook ini     },     useState (initialValue) {       kait [currentHook]=kait [currentHook] || initialValue // type: any       const setStateHookIndex=currentHook // untuk penutupan setState!       const setState=newState=> (hooks [setStateHookIndex]=newState)       kembali [hooks[currentHook++], setState]     }   } }) ()  

Perhatikan penggunaan setStateHookIndex di sini, yang sepertinya tidak melakukan apa-apa, tetapi digunakan untuk mencegah setState dari penutupan currentHook variabel! Jika Anda mengambilnya, setState lagi berhenti bekerja karena tertutup currentHook sudah basi . (Cobalah!)

  // Contoh 4 lanjutan - dalam penggunaan function Counter () {   const [count, setCount]=MyReact.useState (0)   const [text, setText]=MyReact.useState ('foo') // hook status ke-2!   MyReact.useEffect (()=> {     console.log ('efek', hitung, teks)   }, [count, text])   kembali {     klik: ()=> setCount (count + 1),     ketik: txt=> setText (txt),     noop: ()=> setCount (hitungan),     render: ()=> console.log ('render', {count, text})   } } biarkan App App=MyReact.render (Penghitung) // efek 0 foo // render {hitungan: 0, teks: 'foo'} App.click () App=MyReact.render (Penghitung) // efek 1 foo // render {hitungan: 1, teks: 'foo'} App.type ('bar') App=MyReact.render (Penghitung) // efek 1 batang // render {hitungan: 1, teks: 'bar'} App.noop () App=MyReact.render (Penghitung) // // tidak ada efek yang dijalankan // render {hitungan: 1, teks: 'bar'} App.click () App=MyReact.render (Penghitung) // efek 2 bilah // render {hitungan: 2, teks: 'bar'}  

Jadi, intuisi dasarnya adalah memiliki serangkaian pengait dan indeks yang hanya bertambah saat setiap hook dipanggil dan disetel ulang saat komponen dirender.

Anda juga mendapatkan kait khusus gratis:

  // Contoh 4, ditinjau kembali function Component () {   const [text, setText]=useSplitURL ('www.netlify.com')   kembali {     ketik: txt=> setText (txt),     render: ()=> console.log ({text})   } } function useSplitURL (str) {   const [text, setText]=MyReact.useState (str)   const masked=text.split ('.')   kembali [masked, setText] } biarkan App App=MyReact.render (Komponen) // {teks: [ 'www', 'netlify', 'com' ]} App.type ('www.reactjs.org') App=MyReact.render (Komponen) // {teks: [ 'www', 'reactjs', 'org' ]}}  

Ini benar-benar mendasari betapa hook “bukan sihir” – Custom Hooks keluar dari primitif yang disediakan oleh framework – apakah itu React, atau tiruan kecil yang telah kami buat.

Mendapatkan Rules of Hooks

Perhatikan bahwa dari sini Anda dapat dengan mudah memahami bagian pertama dari Aturan Hooks : Hanya Hook Panggilan di Tingkat Atas . Kami telah secara eksplisit memodelkan ketergantungan React pada pesanan panggilan dengan currentHook variabel. Anda dapat membaca keseluruhan penjelasan aturan dengan kami implementasi dalam pikiran dan sepenuhnya memahami semua yang terjadi.

Perhatikan juga bahwa aturan kedua, “ Hanya Hooks Panggilan dari React Functions ”, juga bukan hasil yang diperlukan dari penerapan kami, tetapi tentu saja merupakan praktik yang baik untuk secara eksplisit membatasi bagian mana dari kode Anda mengandalkan logika stateful. (Sebagai efek samping yang bagus, ini juga membuatnya lebih mudah untuk menulis perkakas untuk memastikan Anda mengikuti Aturan pertama. Anda tidak dapat secara tidak sengaja menembak diri sendiri dengan membungkus fungsi stateful yang dinamai seperti fungsi JavaScript biasa di dalam loop dan kondisi. Mengikuti Aturan 2 membantu Anda mengikuti Aturan 1.)

Kesimpulan

Pada titik ini kita mungkin telah meregangkan latihan sejauh mungkin. Anda dapat mencoba mengimplementasikan useRef sebagai satu baris , atau membuat fungsi render benar-benar mengambil JSX dan memasang ke DOM , atau sejuta detail penting lainnya yang telah kami hilangkan di klon React Hooks 28-baris yang kecil ini. Tapi semoga Anda mendapatkan pengalaman menggunakan closure dalam konteks, dan memperoleh model mental yang berguna untuk mengungkap cara kerja React Hooks.

Saya ingin berterima kasih Dan Abramov dan Divya Sasidharan untuk meninjau draf awal eassay ini dan menyempurnakannya dengan masukan berharga mereka. Semua kesalahan yang tersisa adalah milik saya ..

Read More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments