Setelah menguasai kaitnya, banyak pengembang Bereaksi mengalami euforia, akhirnya mendapatkan toolkit sederhana dan nyaman yang memungkinkan Anda untuk mengimplementasikan tugas dengan kode yang jauh lebih sedikit. Tetapi apakah ini berarti bahwa standar useState dan useReducer hook yang ditawarkan di luar kotak adalah semua yang kita butuhkan untuk mengelola keadaan?
Menurut pendapat saya, dalam bentuk mentah mereka, penggunaannya tidak terlalu nyaman, mereka lebih mungkin dianggap sebagai dasar untuk membangun kait manajemen negara yang benar-benar nyaman. Bereaksi pengembang sendiri sangat mendorong pengembangan kait kustom, jadi mengapa tidak melakukannya? Di bawah potongan, kita akan melihat contoh yang sangat sederhana dan dapat dimengerti tentang apa yang salah dengan kait biasa dan bagaimana kait itu dapat ditingkatkan, sedemikian rupa sehingga mereka sepenuhnya menolak untuk menggunakannya dalam bentuk murni mereka.
Ada bidang tertentu untuk input, syarat, nama. Dan ada tombol dengan mengklik di mana kita harus membuat permintaan ke server dengan nama yang dimasukkan (pencarian tertentu). Tampaknya akan lebih mudah? Namun, solusinya masih jauh dari jelas. Implementasi naif pertama:
const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; }
Apa yang salah di sini? Jika pengguna, memasukkan sesuatu di bidang, mengirim formulir dua kali, hanya permintaan pertama yang akan bekerja untuk kami, karena pada permintaan klik kedua tidak akan berubah dan useEffect tidak akan berfungsi. Jika kita membayangkan bahwa aplikasi kita adalah layanan pencarian tiket, dan pengguna dapat mengirim formulir berkali-kali tanpa membuat perubahan, maka implementasi seperti itu tidak akan berhasil bagi kita! Menggunakan nama sebagai ketergantungan untuk useEffect juga tidak dapat diterima, jika tidak formulir akan segera dikirim ketika teks berubah. Nah, Anda harus menunjukkan kecerdikan.
const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(!request)}/> { result && <div>Result: { result }</div> } </div>; }
Sekarang, dengan setiap klik, kami akan mengubah arti permintaan menjadi sebaliknya, yang akan mencapai perilaku yang diinginkan. Ini adalah kruk yang sangat kecil dan tidak bersalah, tetapi membuat kodenya agak membingungkan untuk dipahami. Mungkin sekarang Anda merasa bahwa saya mengisap masalah dari jari saya dan meningkatkan skalanya. Nah, untuk menjawab apakah itu benar atau tidak, Anda perlu membandingkan kode ini dengan implementasi lain yang menawarkan pendekatan yang lebih ekspresif.
Mari kita lihat contoh ini pada level teoretis menggunakan abstraksi thread. Sangat mudah untuk menggambarkan status antarmuka pengguna. Jadi, kami memiliki dua aliran: data dimasukkan ke dalam bidang teks (nama $), dan aliran klik pada tombol kirim formulir (klik $). Dari mereka kita perlu membuat aliran permintaan gabungan ketiga ke server.
name$ __(C)____(Ca)_____(Car)____________________(Carl)___________ click$ ___________________________()______()________________()_____ request$ ___________________________(Car)___(Car)_____________(Carl)_
Inilah perilaku yang perlu kita capai. Setiap aliran memiliki dua aspek: nilai yang dimilikinya, dan titik waktu di mana nilai mengalir melaluinya. Dalam berbagai situasi, kita mungkin membutuhkan satu atau beberapa aspek, atau keduanya. Anda dapat membandingkan ini dengan ritme dan harmoni dalam musik. Aliran yang hanya memerlukan waktu respons juga disebut sinyal.
Dalam kasus kami, klik $ adalah sinyal murni: tidak masalah nilai mana yang mengalir melaluinya (tidak ditentukan / benar / Peristiwa / apa pun), penting hanya ketika ini terjadi. Nama kasus $
sebaliknya: perubahannya sama sekali tidak memerlukan perubahan apa pun dalam sistem, tetapi kita mungkin perlu maknanya di beberapa titik. Dan dari dua aliran ini kita perlu membuat yang ketiga, mengambil dari yang pertama kali, dari yang kedua.
Dalam hal Rxjs, kami memiliki operator yang hampir siap pakai untuk ini:
const names$ = fromEvent(...); const click$ = fromEvent(...); const request$ = click$.pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(...))));
Namun, penggunaan praktis dari Rx dalam Bereaksi bisa sangat merepotkan. Pilihan yang lebih cocok adalah pustaka mrr , yang dibangun di atas prinsip fungsional-reaktif yang sama dengan Rx, tetapi secara khusus diadaptasi untuk digunakan dengan Bereaksi pada prinsip "reaktivitas total" dan dihubungkan sebagai pengait.
import useMrr from 'mrr/hooks'; const App = props => { const [state, set] = useMrr(props, { result: [name => fetch('//example.api/' + name).then(data => data.result), '-name', 'submit'], }); return <div> <input value={state.name} onChange={set('name')}/> <input type="submit" value="Check" onClick={set('submit')}/> { state.result && <div>Result: { state.result }</div> } </div>; }
Antarmuka useMrr mirip dengan useState atau useReducer: ia mengembalikan objek keadaan (nilai semua utas) dan penyetel untuk memasukkan nilai ke dalam utas. Tetapi di dalam semuanya sedikit berbeda: setiap bidang negara (= aliran), kecuali untuk yang kami beri nilai langsung dari peristiwa DOM, dijelaskan oleh fungsi dan daftar utas induk, perubahan yang akan menyebabkan anak dihitung ulang. Dalam hal ini, nilai-nilai dari thread induk akan diganti ke dalam fungsi. Jika kita hanya ingin mendapatkan nilai streaming, tetapi tidak menanggapi perubahannya, maka kita menulis "minus" di depan nama, seperti dalam kasus nama.
Kami mendapat perilaku yang diinginkan, pada intinya, dalam satu baris. Tapi ini bukan hanya singkatnya. Mari kita bandingkan hasil yang diperoleh secara lebih rinci, dan pertama-tama berkenaan dengan parameter seperti keterbacaan dan kejelasan kode yang dihasilkan.
Di mrr, Anda hampir dapat sepenuhnya memisahkan "logika" dari "templat": Anda tidak harus menulis penangan imperatif kompleks di BEJ. Semuanya sangat deklaratif: kita hanya memetakan peristiwa DOM ke aliran yang sesuai, praktis tanpa konversi (untuk bidang input, nilai e.target.value diekstraksi secara otomatis, kecuali jika Anda menentukan sebaliknya), dan sudah dalam struktur useMrr kami menggambarkan bagaimana aliran dasar terbentuk anak perusahaan. Jadi, dalam kasus transformasi data sinkron dan asinkron, kita selalu dapat dengan mudah melacak bagaimana nilai kita terbentuk.
Dibandingkan dengan Px: kami bahkan tidak harus menggunakan operator tambahan: jika, sebagai akibatnya, fungsi mrr menerima janji, ia akan secara otomatis menunggu hingga diselesaikan dan memasukkan data yang diterima ke dalam aliran. Juga, bukannya withLatestFrom, kami menggunakan
mendengarkan pasif (tanda minus), yang lebih nyaman. Bayangkan bahwa selain nama kita perlu mengirim bidang lain. Kemudian di mrr kita akan menambahkan aliran mendengarkan pasif:
result: [(name, surname) => fetch(...), '-name', '-surname', 'submit'],
Dan di Rx Anda harus memahat satu lagi denganLestestFrom dengan peta, atau pertama-tama gabungkan nama dan nama keluarga menjadi satu aliran.
Tetapi kembali ke kait dan mrr. Catatan dependensi yang lebih mudah dibaca, yang selalu menunjukkan bagaimana data dibentuk, mungkin merupakan salah satu keuntungan utama. Antarmuka useEffect saat ini pada dasarnya tidak memungkinkan untuk merespons aliran sinyal, itulah sebabnya
Saya harus datang dengan tikungan yang berbeda.
Poin lain adalah bahwa opsi kait biasa membawa rendering tambahan. Jika pengguna hanya mengklik tombol, ini belum memerlukan perubahan apa pun pada UI yang perlu dibuat reaksi. Namun, render akan dipanggil. Dalam varian dengan mrr, status yang dikembalikan hanya akan diperbarui ketika respons dari server telah tiba. Menyimpan pada pertandingan, katamu? Yah, mungkin. Tetapi bagi saya pribadi, prinsip "menyerahkan kembali diri Anda dalam situasi yang tidak dapat dipahami", yang merupakan dasar dari kait dasar, menyebabkan penolakan.
Render tambahan berarti formasi penangan acara yang baru. Ngomong-ngomong, di sini kait biasa semuanya buruk. Tidak hanya penangan yang penting, mereka juga harus dilahirkan kembali setiap kali mereka memberikan. Dan tidak mungkin menggunakan caching sepenuhnya di sini, karena banyak penangan harus dikunci ke variabel komponen internal. Penangan mrr lebih bersifat deklaratif, dan caching sudah terintegrasi dalam mrr: set ('name') akan dihasilkan hanya satu kali, dan akan diganti dari cache untuk render selanjutnya.
Dengan peningkatan basis kode, penangan imperatif dapat menjadi lebih rumit. Katakanlah kita juga perlu menunjukkan jumlah pengiriman formulir yang dilakukan oleh pengguna.
const App = () => { const [request, makeRequest] = useState(); const [name, setName] = useState(''); const [result, setResult] = useState(false); const [clicks, setClicks] = useState(0); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => { makeRequest(!request); setClicks(clicks + 1); }}/><br /> Clicked: { clicks } </div>; }
Tidak terlihat cantik. Tentu saja Anda dapat menjadikan pawang sebagai fungsi terpisah di dalam komponen. Keterbacaan akan meningkat, tetapi masalah regenerasi fungsi dengan masing-masing render akan tetap, serta masalah imperatifitas. Pada dasarnya, ini adalah kode prosedural biasa, meskipun ada kepercayaan luas bahwa React API secara bertahap berubah menuju pendekatan fungsional.
Bagi mereka yang skala masalahnya kelihatannya berlebihan, saya dapat menjawab bahwa, misalnya, pengembang Bereaksi sendiri sadar akan masalah generasi penangan yang berlebihan, segera membantu kami menawarkan penopang dalam bentuk useCallback.
Di mrr:
const App = props => { const [state, set] = useMrr(props, { $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); return <div> <input onChange={set('name')}/> <input type="submit" value="Check" onClick={set('makeRequest')}/> </div>; }
Alternatif yang lebih nyaman adalah useReducer, memungkinkan Anda untuk meninggalkan keharusan penangan. Tetapi masalah penting lainnya tetap ada: kurangnya bekerja dengan sinyal (karena efek yang sama akan menyebabkan efek samping), serta keterbacaan terburuk selama konversi asinkron (dengan kata lain, lebih sulit untuk melacak hubungan antara bidang-bidang toko, karena penggunaan yang sama. ) Jika di mrr grafik ketergantungan antara bidang negara (utas) langsung terlihat jelas, di kait Anda harus menjalankan mata sedikit ke atas dan ke bawah.
Juga, berbagi useState dan useReducer dalam komponen yang sama sangat tidak nyaman (sekali lagi akan ada penangan imperatif kompleks yang akan mengubah sesuatu dalam useState
dan mengirimkan tindakan), karena itu, kemungkinan besar, sebelum mengembangkan komponen, Anda harus menerima satu atau opsi lain.
Tentu saja, pertimbangan semua aspek masih bisa dilanjutkan. Agar tidak melampaui ruang lingkup artikel, saya akan menyentuh beberapa poin kurang penting secara penuh.
Logging terpusat, debug. Karena di mrr semua stream terkandung dalam satu hub, untuk debugging cukup untuk menambahkan satu flag:
const App = props => { const [state, set] = useMrr(props, { $log: true, $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); ...
Setelah itu, semua perubahan pada aliran akan ditampilkan di konsol. Untuk mengakses seluruh status (mis., Nilai saat ini dari semua utas), ada kondisi $ pseudo-stream:
a: [({ name, click, result }) => { ... }, '$state', 'click'],
Dengan demikian, jika Anda perlu atau jika Anda sangat terbiasa dengan gaya editorial, Anda dapat menulis dengan gaya editor di mrr, mengembalikan nilai bidang baru berdasarkan peristiwa dan seluruh keadaan sebelumnya. Tetapi sebaliknya (menulis di useReducer atau editor dalam gaya mrr) tidak akan berfungsi, karena kurangnya reaktivitas di dalamnya.
Bekerjalah dengan waktu. Ingat dua aspek aliran: makna dan waktu respons, harmoni dan ritme? Jadi, bekerja dengan yang pertama di kait biasa cukup sederhana dan nyaman, tetapi dengan yang kedua - tidak. Dengan bekerja seiring waktu, maksud saya adalah pembentukan aliran anak, yang "ritme" -nya berbeda dari orangtua. Ini terutama semua jenis filter, debown, trotl, dll. Semua ini kemungkinan besar harus Anda laksanakan sendiri. Di mrr, Anda dapat menggunakan pernyataan siap pakai di luar kotak. Tuan-tuan mengatur mrr lebih rendah daripada berbagai operator Rx, tetapi memiliki penamaan yang lebih intuitif.
Interaksi antar komponen. Saya ingat bahwa di Editor itu dianggap praktik yang baik untuk membuat hanya satu cerita. Jika kita menggunakan useReducer di banyak komponen,
Mungkin ada masalah dengan organisasi interaksi antara para pihak. Pada mrr, flow dapat dengan bebas "mengalir" dari satu komponen ke komponen lainnya baik ke atas atau ke bawah hierarki, tetapi ini tidak akan menimbulkan masalah karena pendekatan deklaratif. Lebih detail
topik ini, serta fitur-fitur lain dari API mrr, dijelaskan dalam artikel Aktor + FRP dalam Bereaksi
Kesimpulan
Kait reaksi baru sangat bagus dan menyederhanakan kehidupan kita, tetapi kait tersebut memiliki beberapa kelemahan yang dapat diperbaiki oleh kait tujuan umum tingkat tinggi (manajemen negara). UseMrr dari pustaka mrr fungsional-reaktif diusulkan dan dianggap demikian.
Masalah dan solusinya:
- penghitungan ulang data yang tidak perlu pada setiap render (dalam mrr tidak ada karena reaktivitas berbasis push)
- rendering tambahan saat perubahan status tidak memerlukan perubahan UI
- keterbacaan kode yang buruk dengan konversi asinkron (dibandingkan dengan yang sinkron). Dalam mrr, kode asinkron tidak kalah dengan sinkron dalam keterbacaan dan ekspresif. Sebagian besar masalah yang dibahas dalam artikel terbaru tentang useEffect pada mrr pada dasarnya tidak mungkin
- penangan imperatif yang tidak selalu dapat di-cache (di mrr mereka di-cache secara otomatis, hampir selalu dapat di-cache, deklaratif)
- menggunakan useState dan useReducer pada saat yang sama dapat membuat kode canggung
- kurangnya alat untuk mengubah aliran dari waktu ke waktu (debounce, throttle, kondisi balapan)
Pada banyak titik, orang dapat berargumen bahwa mereka dapat diselesaikan dengan kait kustom. Tetapi inilah tepatnya yang diusulkan, tetapi alih-alih implementasi yang berbeda, untuk setiap tugas yang terpisah, solusi holistik, yang konsisten diusulkan.
Banyak masalah menjadi terlalu akrab bagi kita untuk dikenali dengan jelas. Misalnya, konversi asinkron selalu terlihat lebih rumit dan membingungkan daripada yang sinkron, dan kaitan dalam pengertian ini tidak lebih buruk daripada pendekatan sebelumnya (eds, dll.). Untuk mewujudkan ini sebagai masalah, Anda harus terlebih dahulu melihat pendekatan lain yang menawarkan solusi yang lebih baik.
Artikel ini dimaksudkan untuk tidak memaksakan pandangan tertentu, tetapi lebih untuk menarik perhatian pada masalah tersebut. Saya yakin ada solusi lain atau sedang dibuat yang bisa menjadi alternatif yang layak, tetapi belum menjadi dikenal luas. API Cache React yang akan datang juga dapat membuat perbedaan besar. Saya akan senang kritik dan diskusi di komentar.
Mereka yang tertarik juga dapat menonton presentasi tentang topik ini di kyivj pada 28 Maret.