Bekerja dengan panggilan balik di Bereaksi

Selama pekerjaan saya, saya secara berkala menemukan fakta bahwa pengembang tidak selalu memahami dengan jelas bagaimana mekanisme untuk mentransmisikan data melalui alat peraga, khususnya callback, dan mengapa PureComponents mereka sering diperbarui.


Oleh karena itu, dalam artikel ini kita akan memahami bagaimana callback diteruskan ke Bereaksi, dan juga membahas fitur-fitur penangan acara.


TL; DR


  1. Jangan campur dengan JSX dan logika bisnis - ini akan mempersulit persepsi kode.
  2. Untuk optimasi kecil, fungsi penangan cache dalam bentuk classProperties untuk kelas atau menggunakan useCallback untuk fungsi - maka komponen murni tidak akan terus-menerus dirender. Terutama cache callback bisa berguna sehingga ketika mereka diteruskan ke PureComponent, siklus pembaruan yang tidak perlu tidak terjadi.
  3. Jangan lupa bahwa Anda tidak mendapatkan peristiwa nyata dalam panggilan balik, tetapi peristiwa Sintaksis. Jika Anda keluar dari fungsi saat ini, maka Anda tidak akan dapat mengakses bidang acara ini. Cache bidang yang Anda butuhkan jika Anda memiliki penutupan yang tidak sinkron.

Bagian 1. Penangan acara, caching dan persepsi kode


Bereaksi menyediakan cara yang cukup mudah untuk menambahkan event handler untuk elemen html.


Ini adalah salah satu hal dasar yang diketahui pengembang ketika mereka mulai menulis di Bereaksi:


class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } } 

Cukup sederhana? Dari kode ini, segera menjadi jelas apa yang akan terjadi ketika pengguna mengklik tombol.


Tetapi bagaimana jika kode dalam handler menjadi lebih dan lebih banyak?


Misalkan kita mengklik tombol untuk memuat dan memfilter semua orang yang tidak berada di tim tertentu ( user.team === 'search-team' ), lalu urutkan berdasarkan usia.


 class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } } 

Kode ini cukup sulit diketahui. Kode logika bisnis dicampur dengan tata letak yang dilihat pengguna.


Cara termudah untuk menghilangkan ini adalah dengan membawa fungsi ke tingkat metode kelas:


 class MyComponent extends Component { fetchUsers() { //    } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => this.fetchUsers()}>Load users</button> </div> ); } } 

Di sini kami memindahkan logika bisnis dari kode JSX ke bidang terpisah di kelas kami. Untuk membuatnya dapat diakses di dalam fungsi, kami mendefinisikan panggilan balik dengan cara ini: onClick={() => this.fetchUsers()}


Selain itu, saat mendeskripsikan sebuah kelas, kita bisa mendeklarasikan sebuah field sebagai fungsi panah:


 class MyComponent extends Component { fetchUsers = () => { //    }; render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={this.fetchUsers}>Load users</button> </div> ); } } 

Ini akan memungkinkan kami untuk mendeklarasikan callback sebagai onClick={this.fetchUsers}


Apa perbedaan antara kedua metode ini?


onClick={this.fetchUsers} - Di sini, dengan setiap panggilan ke fungsi render dalam alat peraga, button akan selalu dikirimkan tautan yang sama.


Dalam kasus onClick={() => this.fetchUsers()} , setiap kali fungsi render dipanggil, JavaScript menginisialisasi fungsi baru () => this.fetchUsers() dan mengaturnya ke prop onClick . Ini berarti nextProp.onClick dan prop.onClick pada button dalam hal ini akan selalu tidak sama, dan bahkan jika komponen ditandai sebagai bersih, itu akan dirender.


Apa yang mengancam perkembangan ini?


Dalam kebanyakan kasus, Anda tidak akan melihat penurunan kinerja secara visual, karena Virtual DOM yang akan dihasilkan oleh komponen tidak akan berbeda dari yang sebelumnya, dan tidak akan ada perubahan dalam DOM Anda.


Namun, jika Anda membuat daftar besar komponen atau tabel, maka Anda akan melihat "rem" pada sejumlah besar data.


Mengapa memahami bagaimana suatu fungsi ditransfer ke panggilan balik itu penting?


Seringkali di twitter atau di stackoverflow Anda dapat menemukan tips seperti ini:


"Jika Anda memiliki masalah kinerja dengan Bereaksi aplikasi, cobalah mengganti pewarisan dari Komponen dengan PureComponent. Juga, ingat bahwa untuk Komponen Anda selalu dapat menentukan shouldComponentUpdate untuk menyingkirkan loop pembaruan yang tidak perlu."


Jika kita mendefinisikan komponen sebagai Murni, itu berarti bahwa ia sudah memiliki fungsi shouldComponentUpdate yang melakukan shallowEqual antara props dan nextProps.


Melewati fungsi panggilan balik baru ke komponen seperti itu setiap kali, kami kehilangan semua kelebihan dan optimalisasi PureComponent .


Mari kita lihat sebuah contoh.
Buat komponen Input yang juga akan menampilkan informasi berapa kali telah diperbarui:


 class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } } 

Mari kita membuat dua komponen yang akan membuat Input secara internal:


 class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Dan yang kedua:


 class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Anda dapat mencoba contohnya dengan tangan Anda di sini: https://codesandbox.io/s/2vwz6kjjkr
Contoh ini menunjukkan bagaimana Anda bisa kehilangan semua manfaat PureComponent jika Anda melewatkan fungsi panggilan balik baru ke PureComponent setiap kali.


Bagian 2. Menggunakan Event handler dalam komponen fungsi


Dalam versi baru React (16.8), mekanisme React hooks diumumkan, yang memungkinkan Anda untuk menulis komponen fungsional lengkap, dengan siklus hidup yang jelas yang dapat mencakup hampir semua kasus pengguna yang sampai sekarang hanya mencakup kelas.


Kami memodifikasi contoh dengan komponen Input sehingga semua komponen diwakili oleh fungsi dan bekerja dengan React-hooks.


Input harus menyimpan informasi di dalamnya sendiri tentang berapa kali telah diubah. Jika dalam kasus kelas kami menggunakan bidang dalam contoh kami, akses yang diterapkan melalui ini, maka dalam kasus fungsi kami tidak akan dapat mendeklarasikan variabel melalui ini.
React menyediakan hook useRef yang dapat digunakan untuk menyimpan referensi ke HtmlElement di pohon DOM, tetapi juga menarik karena dapat digunakan untuk data reguler yang dibutuhkan komponen kami:


 import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); } 

Kita juga perlu komponen untuk menjadi "bersih", yaitu, itu diperbarui hanya jika alat peraga yang diteruskan ke komponen telah berubah.
Untuk ini, ada beberapa pustaka yang menyediakan HOC, tetapi lebih baik menggunakan fungsi memo, yang sudah ada di React, karena ia bekerja lebih cepat dan lebih efisien:


 import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }); 

Komponen Input siap, sekarang kita menulis ulang komponen A dan B.
Dalam kasus komponen B, ini mudah dilakukan:


 import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); } 

Di sini kami menggunakan hook useState , yang memungkinkan Anda untuk menyimpan dan bekerja dengan keadaan komponen, jika komponen diwakili oleh suatu fungsi.


Bagaimana kita bisa men-cache fungsi callback? Kami tidak dapat menghapusnya dari komponen, karena dalam hal ini akan umum untuk berbagai contoh komponen.
Untuk tugas-tugas seperti itu, React memiliki satu set caching dan memoizing hooks, di mana useCallback adalah yang paling cocok untuk useCallback https://reactjs.org/docs/hooks-reference.html


Tambahkan kait ini ke komponen A :


 import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); } 

Kami men-cache fungsi, yang berarti komponen Input tidak akan diperbarui setiap waktu.


Bagaimana cara kerja useCallback hook?


Hook ini mengembalikan fungsi yang di-cache (mis. Tautan tidak berubah dari render ke render).
Selain fungsi yang akan di-cache, argumen kedua diteruskan ke sana - sebuah array kosong.
Array ini memungkinkan Anda untuk mentransfer daftar bidang, saat mengubah mana Anda perlu mengubah fungsi, mis. kembalikan tautan baru.


useCallback dapat melihat perbedaan antara metode biasa mentransfer fungsi ke panggilan balik dan useCallback sini: https://codesandbox.io/s/0y7wm3pp1w


Mengapa kita membutuhkan sebuah array?


Misalkan kita perlu melakukan cache fungsi yang bergantung pada beberapa nilai melalui penutupan:


 import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ /*a*/ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement); 

Di sini, komponen Aplikasi tergantung pada properti a . Jika Anda menjalankan contoh, maka semuanya akan berfungsi dengan benar hingga saat kami menambahkan sampai akhir:


 setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000); 

Setelah batas waktu dipicu, ketika Anda mengklik tombol dalam peringatan, 1 akan ditampilkan. Ini terjadi karena kami menyimpan fungsi sebelumnya, yang menutup variabel. Dan karena a adalah variabel, yang dalam kasus kami adalah tipe nilai, dan tipe nilai tidak berubah, kami mendapatkan kesalahan ini. Jika kita menghapus komentar /*a*/ , maka kodenya akan berfungsi dengan benar. Bereaksi pada render kedua akan memverifikasi bahwa data yang dikirimkan dalam array berbeda dan akan mengembalikan fungsi baru.


Anda dapat mencoba contoh ini sendiri di sini: https://codesandbox.io/s/6vo8jny1ln


Bereaksi menyediakan banyak fungsi yang memungkinkan Anda untuk membuat memo data, seperti useRef , useCallback dan useMemo .
Jika yang terakhir diperlukan untuk memotret nilai fungsi, dan mereka sangat mirip dengan useRef , maka useRef memungkinkan Anda untuk useRef -cache tidak hanya referensi ke elemen DOM, tetapi juga bertindak sebagai bidang contoh.


Pada pandangan pertama, ini dapat digunakan untuk fungsi cache, karena useRef juga menyimpan data di antara pembaruan komponen yang terpisah.
Namun, menggunakan useRef untuk fungsi cache tidak diinginkan. Jika fungsi kami menggunakan closure, maka dalam render apa pun, nilai tertutup dapat berubah, dan fungsi cache kami akan bekerja dengan nilai lama. Ini berarti bahwa kita perlu menulis logika pembaruan fungsi atau hanya menggunakan useCallback , di mana ia diterapkan karena mekanisme ketergantungan.


https://codesandbox.io/s/p70pprpvvx di sini Anda dapat melihat memoisasi fungsi dengan useCallback benar, dengan yang salah dan dengan useRef .


Bagian 3. Peristiwa sintetik


Kami telah menemukan cara menggunakan penangan acara dan cara bekerja dengan benar dengan penutupan dalam panggilan balik, tetapi dalam Bereaksi ada perbedaan lain yang sangat penting ketika bekerja dengan mereka:


Catatan: sekarang Input , yang kami gunakan di atas, benar-benar sinkron, tetapi dalam beberapa kasus mungkin diperlukan untuk callback terjadi dengan penundaan, sesuai dengan pola debounce atau throttling . Jadi, debounce, misalnya, sangat mudah digunakan untuk input string pencarian - pencarian hanya akan terjadi ketika pengguna berhenti mengetik karakter.


Buat komponen yang secara internal menyebabkan perubahan status:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Kode ini tidak akan berfungsi. Faktanya adalah bahwa Bereaksi proksi peristiwa di dalam dirinya sendiri, dan apa yang disebut Acara Sintaks masuk ke callback onChange kami, yang setelah fungsi kami akan "dihapus" (bidang akan nol). Untuk alasan kinerja, Bereaksi melakukan ini untuk menggunakan objek tunggal, daripada membuat yang baru setiap kali.


Jika kita perlu mengambil nilai, seperti dalam contoh ini, maka itu sudah cukup untuk men-cache bidang yang diperlukan SEBELUM keluar dari fungsi:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Anda dapat melihat contoh di sini: https://codesandbox.io/s/oj6p8opq0z


Dalam kasus yang sangat jarang, perlu untuk mempertahankan seluruh instance acara. Untuk melakukan ini, Anda bisa memanggil event.persist() , yang menghapus
contoh acara Sintaks ini dari kumpulan peristiwa reaksi.


Kesimpulan:


Bereaksi event handler sangat nyaman karena mereka:


  1. Otomatis berlangganan dan berhenti berlangganan (dengan komponen unmount);
  2. Sederhanakan persepsi kode, sebagian besar langganan mudah dilacak dalam kode JSX.

Tetapi pada saat yang sama, ketika mengembangkan aplikasi, Anda mungkin menghadapi beberapa kesulitan:


  1. Mengganti panggilan balik dalam alat peraga;
  2. Peristiwa sintetik yang dihapus setelah pelaksanaan fungsi saat ini.

Overbacking callbacks biasanya tidak terlihat, karena vDOM tidak berubah, tetapi perlu diingat bahwa jika Anda memperkenalkan optimisasi, mengganti komponen dengan Pure melalui pewarisan dari PureComponent atau menggunakan memo , Anda harus berhati-hati dalam menyimpannya, jika tidak, manfaat memperkenalkan PureComponents atau memo tidak akan terlihat. Untuk caching, Anda bisa menggunakan classProperties (saat bekerja dengan kelas) atau useCallback hook (saat bekerja dengan fungsi).


Untuk operasi asinkron yang benar, jika Anda memerlukan data dari suatu acara, juga cache bidang yang Anda butuhkan.

Source: https://habr.com/ru/post/id439138/


All Articles