Menemukan Peluru Perak: Aktor + FRP dalam Bereaksi

Saat ini, beberapa orang menulis di Perl, tetapi pepatah Larry Wall yang terkenal "Menjaga hal-hal sederhana tetap mudah dan sulit" telah menjadi formula yang diterima secara umum untuk teknologi yang efektif. Ini dapat ditafsirkan dalam aspek tidak hanya kompleksitas tugas, tetapi juga pendekatan: teknologi yang ideal harus, di satu sisi, memungkinkan pengembangan aplikasi menengah dan kecil (termasuk "hanya menulis"), di sisi lain, menyediakan alat untuk pengembangan yang bijaksana aplikasi yang kompleks, di mana keandalan, pemeliharaan dan struktur sangat penting. Atau bahkan, menerjemahkan ke dalam pesawat manusia: untuk dapat diakses oleh Jones, dan pada saat yang sama untuk memenuhi permintaan Signyors.


Editor yang sekarang populer dapat dikritik dari kedua belah pihak - ambil setidaknya fakta bahwa menulis bahkan fungsi dasar dapat menghasilkan banyak baris spasi di beberapa file - tetapi kami tidak akan masuk terlalu dalam, karena banyak yang telah dikatakan tentang ini.


"Kamu seharusnya menyimpan semua meja di satu ruangan, dan kursi di ruangan lain"
- Juha Paananen, pencipta perpustakaan Bacon.js, tentang Editor

Teknologi yang akan dibahas hari ini bukanlah peluru perak, tetapi mengklaim lebih konsisten dengan kriteria ini.


Mrr adalah perpustakaan reaktif fungsional yang menganut prinsip "semuanya mengalir." Keuntungan utama yang diberikan oleh pendekatan fungsional-reaktif dalam mrr adalah keringkasan, ekspresi kode, serta pendekatan terpadu untuk transformasi data sinkron dan asinkron.


Pada pandangan pertama, ini tidak terdengar seperti teknologi yang akan mudah diakses oleh pemula: konsep aliran bisa sulit untuk dipahami, itu tidak begitu luas di front-end, terutama terkait dengan perpustakaan bodoh seperti Rx. Dan yang paling penting, tidak sepenuhnya jelas bagaimana menjelaskan aliran berdasarkan skema โ€œaksi-reaksi-pembaruan DOMโ€ dasar. Tapi ... kita tidak akan berbicara secara abstrak tentang aliran! Mari kita bicara tentang hal-hal yang lebih dimengerti: peristiwa, kondisi.


Memasak sesuai resep


Tanpa masuk ke belantara FRP, kami akan mengikuti skema sederhana untuk memformalisasi area subjek:


  • membuat daftar data yang menggambarkan keadaan halaman dan akan digunakan dalam antarmuka pengguna, serta tipenya.
  • membuat daftar peristiwa yang terjadi atau dihasilkan oleh pengguna di halaman, dan jenis data yang akan dikirim bersama mereka
  • buat daftar proses yang akan terjadi pada halaman
  • menentukan saling ketergantungan di antara mereka.
  • Jelaskan saling ketergantungan dengan menggunakan operator yang tepat.

Pada saat yang sama, kita membutuhkan pengetahuan tentang perpustakaan hanya pada tahap terakhir.


Jadi, mari kita ambil contoh sederhana dari toko web, di mana ada daftar produk dengan pagination dan filtering berdasarkan kategori, serta keranjang.


  1. Data atas dasar mana antarmuka akan dibangun:


    • daftar barang (array)
    • kategori yang dipilih (baris)
    • jumlah halaman dengan barang (jumlah)
    • daftar produk yang ada di keranjang (array)
    • halaman saat ini (nomor)
    • jumlah produk dalam keranjang (jumlah)

  2. Peristiwa (dengan "peristiwa" hanya berarti peristiwa sesaat. Tindakan yang terjadi untuk sementara waktu - proses - perlu diuraikan menjadi peristiwa terpisah):


    • halaman pembuka (batal)
    • pemilihan kategori (string)
    • menambahkan barang ke keranjang (objek "barang")
    • penghapusan barang dari keranjang (id barang yang akan dihapus)
    • pergi ke halaman berikutnya dari daftar produk (nomor - nomor halaman)

  3. Proses: ini adalah tindakan yang dimulai dan kemudian dapat berakhir dengan berbagai peristiwa sekaligus atau setelah beberapa waktu. Dalam kasus kami, ini akan menjadi pemuatan data produk dari server, yang dapat menyebabkan dua peristiwa: penyelesaian yang berhasil dan penyelesaian dengan kesalahan.


  4. Saling ketergantungan antara peristiwa dan data. Misalnya, daftar produk akan tergantung pada acara: "berhasil memuat daftar produk." Dan "mulai memuat daftar barang" - dari "membuka halaman", "memilih halaman saat ini", "memilih kategori". Buat daftar bentuk [elemen]: [... dependensi]:


    { requestGoods: ['page', 'category', 'pageLoaded'], goods: ['requestGoods.success'], page: ['goToPage', 'totalPages'], totalPages: ['requestGoods.success'], cart: ['addToCart', 'removeFromCart'], goodsInCart: ['cart'], category: ['selectCategory'] } 


Oh ... tapi ini hampir kode untuk mrr!



Tetap hanya menambahkan fungsi yang akan menggambarkan hubungan. Anda mungkin mengharapkan acara, data, proses menjadi entitas yang berbeda di mrr - tetapi tidak, semua ini adalah utas! Tugas kita adalah menghubungkan mereka dengan benar.


Seperti yang Anda lihat, kami memiliki dua jenis dependensi: "data" dari "acara" (misalnya, halaman dari goToPage) dan "data" dari "data" (barangInCart dari kereta). Untuk masing-masing dari mereka ada pendekatan yang tepat.


Cara termudah adalah dengan "data dari data": di sini kita cukup menambahkan fungsi murni, "rumus":


 goodsInCart: [arr => arr.length, 'cart'], 

Setiap kali array keranjang diubah, nilai barangInCart akan dihitung ulang.


Jika data kami bergantung pada satu peristiwa, maka semuanya juga cukup sederhana:


 category: 'selectCategory', /*     category: [a => a, 'selectCategory'], */ goods: [resp => resp.data, 'requestGoods.success'], totalPages: [resp => resp.totalPages, 'requestGoods.success'], 

Desain bentuk [fungsi, ... argumen-argumen] adalah dasar dari mrr. Untuk pemahaman intuitif, menggambar analogi dengan Excel, aliran dalam mrr juga disebut sel, dan fungsi yang dengannya mereka dihitung disebut rumus.


Jika data kami bergantung pada beberapa peristiwa, kami harus mengubah nilainya secara individual, dan kemudian menggabungkannya menjadi satu aliran menggunakan operator gabungan:


 /* ,  merge -  ,   */ page: ['merge', [a => a, 'goToPage'], [(a, prev) => a < prev ? a : prev, 'totalPages', '-page'] ], cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], 

Dalam kedua kasus, kami merujuk pada nilai sel sebelumnya. Untuk menghindari loop tak terbatas, kami merujuk ke keranjang dan sel halaman secara pasif (tanda minus di depan nama sel): nilainya akan diganti ke dalam rumus, tetapi jika diubah, perhitungan ulang tidak akan dimulai.


Semua utas dibangun atas dasar utas lain atau dipancarkan dari DOM. Tetapi bagaimana dengan aliran "halaman pembuka"? Untungnya, Anda tidak harus menggunakan componentDidMount: di mrr ada aliran khusus $ awal, yang menandakan bahwa komponen tersebut telah dibuat dan dipasang.


"Proses" dihitung secara tidak sinkron, sementara kami memancarkan peristiwa tertentu dari mereka, operator "bersarang" akan membantu kami di sini:


 requestGoods: ['nested', (cb, page, category) => { fetch("...") .then(res => cb('success', res)) .catch(e => cb('error', e)); }, 'page', 'category', '$start'], 

Saat menggunakan operator bersarang, argumen pertama akan diberikan kepada kami fungsi panggilan balik untuk emisi peristiwa tertentu. Dalam hal ini, dari luar mereka akan dapat diakses melalui namespace sel root, misalnya,


 cb('success', res) 

di dalam formula requestGoods, pembaruan sel requestGoods.success akan dihasilkan.


Untuk merender halaman dengan benar sebelum data kami dihitung, Anda dapat menentukan nilai awalnya:


 { goods: [], page: 1, cart: [], }, 

Tambahkan markup. Kami membuat komponen Bereaksi menggunakan fungsi withMrr, yang menerima diagram tautan reaktif dan fungsi render. Untuk "memasukkan" nilai ke aliran, kami menggunakan fungsi $, yang menciptakan (dan menyimpan cache) penangan acara. Sekarang aplikasi kami yang berfungsi penuh terlihat seperti ini:


 import { withMrr } from 'mrr'; const App = withMrr({ $init: { goods: [], cart: [], page: 1, }, requestGoods: ['nested', (cb, page = 1, category = 'all') => { fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, 'page', 'selectCategory', '$start'], goods: [res => res.data, 'requestGoods.success'], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total_pages, 'requestGoods.success'], category: 'selectCategory', cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $) => { return (<section> <h2>Shop</h2> <div> Category: <select onChange={$('selectCategory')}> <option>All</option> <option>Electronics</option> <option>Photo</option> <option>Cars</option> </select> </div> <ul className="goods"> { state.goods.map((item, i) => { const cartI = state.cart.findIndex(a => a.id === item.id); return (<li key={i}> { item.name } <div> { cartI === -1 && <button onClick={$("addToCart", item)}>Add to cart</button> } { cartI !== -1 && <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> } </div> </li>); }) } </ul> <ul className="pages"> { new Array(state.totalPages).fill(true).map((_, p) => { const page = Number(p) + 1; return ( <li className="page" onClick={$('goToPage', page)} key={p}> { page } </li> ); }) } </ul> </section> <section> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<li key={i}> { item.name } <div> <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> </div> </li>); }) } </ul> </section>); }); export default App; 

Konstruksi


 <select onChange={$('selectCategory')}> 

berarti bahwa ketika bidang diubah, nilai akan "didorong" ke aliran selectCategory. Tapi apa artinya? Secara default, ini adalah event.target.value, tetapi jika kita perlu mendorong sesuatu yang lain, kita tentukan dengan argumen kedua, seperti di sini:


 <button onClick={$("addToCart", item)}> 

Semua yang ada di sini - acara, data, dan proses - semuanya mengalir. Pemicu suatu peristiwa menyebabkan perhitungan ulang data atau peristiwa tergantung padanya, dan seterusnya sepanjang rantai. Nilai aliran dependen dihitung menggunakan rumus yang dapat mengembalikan nilai, atau janji (maka mrr akan menunggu resolusi).


API mrr sangat ringkas dan ringkas - untuk sebagian besar kasus, kami hanya membutuhkan 3-4 operator dasar, dan banyak hal dapat dilakukan tanpa mereka. Tambahkan pesan kesalahan ketika daftar produk tidak berhasil dimuat, yang akan ditampilkan selama satu detik:


 hideErrorMessage: [() => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error'], errorMessageShown: [ 'merge', [() => true, 'requestGoods.error'], [() => false, 'hideErrorMessage'], ], 

Garam, merica gula secukupnya


Ada juga gula sintaksis di mrr, yang merupakan pilihan untuk pengembangan, tetapi dapat mempercepatnya. Misalnya, operator sakelar:


 errorMessageShown: ['toggle', 'requestGoods.error', [() => new Promise(res => setTimeout(res, 1000)), 'showErrorMessage']], 

Perubahan pada argumen pertama akan membuat sel menjadi true, dan pada argumen kedua menjadi false.
Pendekatan dengan "mendekomposisi" hasil tugas yang tidak sinkron menjadi subklan sukses dan kesalahan juga sangat luas sehingga Anda dapat menggunakan operator janji khusus (yang secara otomatis menghilangkan kondisi balapan):


  requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ], 

Cukup banyak fungsi yang pas hanya dalam beberapa lusin baris. Persyaratan Juni kami puas - ia berhasil menulis kode kerja, yang ternyata cukup kompak: semua logika masuk dalam satu file dan pada satu layar. Tetapi signor menyipitkan mata dengan tidak percaya: Eka tidak terlihat ... Anda dapat menulis ini di hooks / recompose / etc.


Ya, memang benar! Kode, tentu saja, tidak mungkin lebih kompak dan terstruktur, tetapi bukan itu intinya. Mari kita bayangkan bahwa proyek ini sedang berkembang, dan kita perlu membagi fungsi menjadi dua halaman terpisah: daftar produk dan keranjang. Selain itu, data keranjang, jelas, perlu disimpan secara global untuk kedua halaman.


Satu pendekatan, satu antarmuka


Di sini kita sampai pada masalah lain dari pengembangan reaksi: keberadaan pendekatan heterogen untuk mengelola negara secara lokal (dalam suatu komponen) dan secara global pada tingkat seluruh aplikasi. Banyak, saya yakin, dihadapkan pada dilema: untuk mengimplementasikan beberapa logika secara lokal atau global? Atau situasi lain: ternyata sebagian data lokal perlu disimpan secara global, dan Anda harus menulis ulang bagian dari fungsionalitas, katakanlah, dari recompos ke editor ...


Kontrasnya, tentu saja, buatan, dan pada mrr itu tidak: sama baiknya, dan yang paling penting - seragam! - Cocok untuk manajemen negara lokal dan global. Secara umum, kami tidak memerlukan status global apa pun, kami hanya memiliki kemampuan untuk bertukar data antara komponen, sehingga status komponen root akan menjadi "global".


Skema aplikasi kita sekarang adalah sebagai berikut: komponen root yang berisi daftar barang dalam keranjang, dan dua sub-komponen: barang dan keranjang, dan komponen global "mendengarkan" aliran "menambah keranjang" dan "menghapus dari keranjang" dari komponen anak.


 const App = withMrr({ $init: { cart: [], currentPage: 'goods', }, cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $, connectAs) => { return ( <div> <menu> <li onClick={$('currentPage', 'goods')}>Goods</li> <li onClick={$('currentPage', 'cart')}>Cart{ state.cart && state.cart.length ? '(' + state.cart.length + ')' : '' }</li> </menu> <div> { state.currentPage === 'goods' && <Goods {...connectAs('goods', ['addToCart', 'removeFromCart'], ['cart'])}/> } { state.currentPage === 'cart' && <Cart {...connectAs('cart', { 'removeFromCart': 'remove' }, ['cart'])}/> } </div> </div> ); }) 

 const Goods = withMrr({ $init: { goods: [], page: 1, }, goods: [res => res.data, 'requestGoods.success'], requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total, 'requestGoods.success'], category: 'selectCategory', errorShown: ['toggle', 'requestGoods.error', [cb => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error']], }, (state, props, $) => { return (<div> ... </div>); }); 

 const Cart = withMrr({}, (state, props, $) => { return (<div> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<div> { item.name } <div> <button onClick={$('remove', item.id)}>Remove from cart</button> </div> </div>); }) } </ul> </div>); }); 

Sungguh menakjubkan betapa sedikit yang berubah! Kami hanya meletakkan aliran ke komponen yang sesuai dan meletakkan "jembatan" di antara mereka! Dengan menghubungkan komponen menggunakan fungsi mrrConnect, kami menentukan pemetaan untuk aliran hilir dan hulu:


 connectAs( 'goods', /*  */ ['addToCart', 'removeFromCart'], /*  */ ['cart'] ) 

Di sini, aliran addToCart dan removeFromCart dari komponen turunan akan pergi ke induk, dan aliran keranjang akan kembali. Kami tidak diharuskan menggunakan nama aliran yang sama - jika tidak cocok, kami menggunakan pemetaan:


 connectAs('cart', { 'removeFromCart': 'remove' }) 

Aliran hapus dari komponen turunan akan menjadi sumber untuk aliran removeFromCart di induknya.


Seperti yang Anda lihat, masalah memilih lokasi penyimpanan data dalam kasus mrr sepenuhnya dihapus: Anda menyimpan data yang ditentukan secara logis.


Di sini sekali lagi, orang tidak dapat gagal untuk mencatat kelemahan Editor: di dalamnya Anda harus menyimpan semua data dalam satu repositori pusat. Bahkan data yang dapat diminta dan digunakan hanya oleh satu komponen terpisah atau subtree-nya! Jika kami menulis dalam "gaya editorial", kami juga akan membawa pemuatan dan pagination barang ke tingkat global (dalam keadilan - pendekatan ini, berkat fleksibilitas mrr, juga dimungkinkan dan memiliki hak untuk hidup, kode sumber ).


Namun, ini tidak perlu. Barang dimuat hanya digunakan dalam komponen barang, oleh karena itu, membawanya ke tingkat global, kami hanya menyumbat dan mengembang negara global. Selain itu, kami harus menghapus data yang kedaluwarsa (misalnya, halaman pagination) ketika pengguna kembali ke halaman produk lagi. Memilih tingkat penyimpanan data yang tepat, kami secara otomatis menghindari masalah seperti itu.


Keuntungan lain dari pendekatan ini adalah bahwa logika aplikasi dikombinasikan dengan presentasi, yang memungkinkan kita untuk menggunakan kembali masing-masing komponen Bereaksi sebagai widget yang berfungsi penuh, daripada sebagai template "bodoh". Selain itu, menjaga informasi minimum di tingkat global (idealnya, ini hanya data sesi) dan mengambil sebagian besar logika dalam komponen individu halaman, kami sangat mengurangi koherensi kode. Tentu saja, pendekatan ini tidak selalu berlaku, tetapi ada sejumlah besar tugas di mana negara global sangat kecil dan "layar" individual hampir sepenuhnya independen satu sama lain: misalnya, berbagai jenis admin, dll. Tidak seperti Editor, yang memprovokasi kami untuk mengambil semua yang diperlukan dan tidak diperlukan ke tingkat global, mrr memungkinkan Anda untuk menyimpan data dalam subkontraktor terpisah, mendorong dan memungkinkan enkapsulasi, sehingga aplikasi kami berubah dari "pai" monolitik menjadi "kue" berlapis.


Perlu disebutkan: tentu saja, tidak ada yang revolusioner baru dalam pendekatan yang diusulkan! Komponen, widget mandiri, telah menjadi salah satu pendekatan dasar yang digunakan sejak munculnya kerangka kerja js. Satu-satunya perbedaan signifikan adalah bahwa mrr mengikuti prinsip deklaratif: komponen hanya dapat mendengarkan aliran komponen lain, tetapi tidak dapat memengaruhi mereka (apa yang harus dilakukan dari arah bottom-up, atau arah top-down, yang berbeda dari fluks pendekatan). Komponen pintar yang hanya dapat bertukar pesan dengan komponen dasar dan induknya sesuai dengan model aktor yang populer namun kurang dikenal dalam pengembangan front-end (topik menggunakan aktor dan utas di front-end tercakup dengan baik dalam artikel Pengantar pemrograman reaktif ).
Tentu saja, ini jauh dari implementasi para aktor secara kanonik, tetapi intinya persis seperti ini: peran aktor dimainkan oleh komponen yang bertukar pesan melalui aliran MPP; komponen dapat (secara deklaratif!) membuat dan menghapus aktor komponen anak berkat DOM virtual dan Bereaksi: fungsi render, pada dasarnya, menentukan struktur aktor anak.


Alih-alih situasi standar untuk Bereaksi, ketika kita "menjatuhkan" komponen induk ke panggilan balik tertentu melalui alat peraga, kita harus mendengarkan aliran komponen anak dari induk. Hal yang sama ada di arah yang berlawanan, dari orang tua ke anak. Misalnya, Anda mungkin bertanya: mengapa mentransfer data keranjang keranjang ke komponen Keranjang sebagai aliran, jika kami dapat, tanpa basa-basi lagi, cukup menyerahkannya sebagai alat peraga? Apa bedanya? Memang, pendekatan ini juga dapat digunakan, tetapi hanya sampai ada kebutuhan untuk menanggapi perubahan alat peraga. Jika Anda pernah menggunakan metode componentWillReceiveProps, maka Anda tahu apa ini. Ini adalah semacam "reaktivitas untuk orang miskin": Anda mendengarkan sepenuhnya semua perubahan alat peraga, menentukan apa yang telah berubah, dan bereaksi. Tetapi metode ini akan segera menghilang dari Bereaksi, dan kebutuhan akan reaksi terhadap "sinyal dari atas" dapat muncul.


Dalam mrr, mengalir "mengalir" tidak hanya naik, tetapi juga turun ke hirarki komponen, sehingga komponen dapat secara independen menanggapi perubahan keadaan. Dengan melakukannya, Anda dapat menggunakan kekuatan penuh alat reaktif mrr.


 const Cart = withMrr({ foo: [items => { // -  }, 'cart'], }, (state, props, $) => { ... }) 

Tambahkan sedikit birokrasi


Proyek ini berkembang, menjadi sulit untuk melacak nama-nama arus, yang - oh, horor! - disimpan dalam baris. Kita bisa menggunakan konstanta untuk nama aliran, dan juga untuk pernyataan mrr. Sekarang melanggar aplikasi dengan membuat kesalahan ketik kecil menjadi lebih sulit.


 import { withMrr } from 'mrr'; import { merge, toggle, promise } from 'mrr/operators'; import { cell, nested, $start$, passive } from 'mrr/cell'; const goods$ = cell('goods'); const page$ = cell('page'); const totalPages$ = cell('totalPages'); const category$ = cell('category'); const errorShown$ = cell('errorShown'); const addToCart$ = cell('addToCart'); const removeFromCart$ = cell('removeFromCart'); const selectCategory$ = cell('selectCategory'); const goToPage$ = cell('goToPage'); const Goods = withMrr({ $init: { [goods$]: [], [page$]: 1, }, [goods$]: [res => res.data, requestGoods$.success], [requestGoods$]: promise((page, category) => fetch('https://reqres.in/api/products?page=', page).then(r => r.json()), page$, category$, $start$), [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]), [totalPages$]: [res => res.total, requestGoods$.success], [category$]: selectCategory$, [errorShown$]: toggle(requestGoods$.error, [cb => new Promise(res => setTimeout(res, 1000)), requestGoods$.error]), }, ...); 

Apa yang ada di kotak hitam?


Bagaimana dengan pengujian? Logika yang dijelaskan dalam komponen mrr mudah untuk dipisahkan dari template, dan kemudian diuji.


Mari kita buat struktur mrr secara terpisah dari file kita.


 const GoodsStruct = { $init: { [goods$]: [], [page$]: 1, }, ... } const Goods = withMrr(GoodsStruct, (state, props, $) => { ... }); export { GoodsStruct } 

dan kemudian kami mengimpornya dalam pengujian kami. Dengan bungkus sederhana kami bisa
letakkan nilai dalam stream (seolah-olah itu dilakukan dari DOM), dan kemudian periksa nilai-nilai dari thread lain tergantung padanya.


 import { simpleWrapper} from 'mrr'; import { GoodsStruct } from '../src/components/Goods'; describe('Testing Goods component', () => { it('should update page if it\'s out of limit ', () => { const a = simpleWrapper(GoodsStruct); a.set('page', 10); assert.equal(a.get('page'), 10); a.set('requestGoods.success', {data: [], total: 5}); assert.equal(a.get('page'), 5); a.set('requestGoods.success', {data: [], total: 10}); assert.equal(a.get('page'), 5); }) }) 

Bersinar dan kemiskinan reaktivitas


Perlu dicatat bahwa reaktivitas adalah abstraksi dari tingkat yang lebih tinggi dibandingkan dengan pembentukan "manual" dari suatu negara berdasarkan peristiwa di Editor. Memfasilitasi pengembangan, di satu sisi, itu menciptakan peluang untuk menembak diri sendiri. Pertimbangkan skenario ini: pengguna membuka halaman nomor 5, lalu mengganti filter "kategori". Kami harus memuat daftar produk dari kategori yang dipilih pada halaman kelima, tetapi mungkin ternyata barang dalam kategori ini hanya memiliki tiga halaman. Dalam kasus backend "bodoh", algoritme tindakan kami adalah sebagai berikut:


  • halaman data permintaan = 5 & kategori =% kategori%
  • ambil dari jawaban nilai jumlah halaman
  • jika mengembalikan nol jumlah rekaman, minta halaman terbesar yang tersedia

Jika kita menerapkan ini pada Editor, kita harus membuat satu tindakan asinkron besar dengan logika yang dijelaskan. Dalam kasus reaktivitas pada mrr, tidak perlu untuk menggambarkan skenario ini secara terpisah. Semuanya sudah terkandung dalam baris ini:


  [requestGoods$]: ['nested', (cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$], [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]), 

Jika nilai total Halaman baru kurang dari halaman saat ini, kami akan memperbarui nilai halaman dan dengan demikian memulai permintaan kedua ke server.
Tetapi jika fungsi kami mengembalikan nilai yang sama, itu masih akan dianggap sebagai perubahan dalam aliran halaman, diikuti oleh perulangan semua aliran dependen. Untuk menghindari ini, mrr memiliki arti khusus - lewati. Mengembalikannya, kami memberi sinyal: tidak ada perubahan yang terjadi, tidak ada yang perlu diperbarui.


 import { withMrr, skip } from 'mrr'; [requestGoods$]: nested((cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$), [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : skip, totalPages$, passive(page$)]), 

Dengan demikian, satu kesalahan kecil dapat membawa kita ke loop tak terbatas: jika kita kembali bukan "lewati", tetapi "sebelumnya", sel halaman akan berubah dan permintaan kedua akan terjadi, dan seterusnya dalam lingkaran. Kemungkinan dari situasi seperti itu, tentu saja, bukan "kelemahan cacat" FRP atau mrr, karena kemungkinan rekursi tak terbatas atau loop tidak menunjukkan ide-ide cacat pemrograman struktural. Namun, harus dipahami bahwa mrr masih memerlukan pemahaman tentang mekanisme reaktivitas. Kembali ke metafora pisau yang terkenal, mrr adalah pisau yang sangat tajam yang meningkatkan efisiensi kerja, tetapi juga dapat melukai pekerja yang tidak kompeten.


Omong-omong, mendebit mrr sangat mudah tanpa menginstal ekstensi apa pun:


 const GoodsStruct = { $init: { ... }, $log: true, ... } 

Cukup tambahkan $ log: true ke struktur mrr, dan semua perubahan pada sel akan di-output ke konsol, sehingga Anda bisa melihat perubahan apa dan bagaimana caranya.


Konsep-konsep seperti pendengaran pasif atau makna loncatan bukanlah โ€œkrukโ€ spesifik: mereka memperluas kemungkinan reaktivitas sehingga dapat dengan mudah menggambarkan keseluruhan logika aplikasi tanpa menggunakan pendekatan imperatif. Mekanisme serupa, misalnya, di Rx.js, tetapi antarmuka mereka di sana kurang nyaman. : Mrr: FRP


.


Ringkasan


  • FRP, mrr ,
  • : ,
  • , ,
  • , , - ( - !)
  • mrr : " , !"
  • ,
  • , , ( ). !
  • : , , , TMTOWTDI: , - .

PS


. , mrr , , :


 import useMrr from 'mrr/hooks'; function Foo(props){ const [state, $, connectAs] = useMrr(props, { $init: { counter: 0, }, counter: ['merge', [a => a + 1, '-counter', 'incr'], [a => a - 1, '-counter', 'decr'] ], }); return ( <div> Counter: { state.counter } <button onClick={ $('incr') }>increment</button> <button onClick={ $('decr') }>decrement</button> <Bar {...connectAs('bar')} /> </div> ); } 

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


All Articles