Mrr: Total FRP untuk React

Mrr adalah perpustakaan fungsional-reaktif untuk React (saya minta maaf atas tautologi imajiner).

Kata "reaktivitas" biasanya merujuk ke Rx.js sebagai referensi FRP. Namun, serangkaian artikel baru-baru ini tentang masalah ini pada Habrรฉ ( [1] , [2] , [3] ) menunjukkan banyaknya solusi Rx, yang dalam contoh sederhana hilang dalam kejelasan dan kesederhanaan untuk hampir semua pendekatan lainnya. Rx besar dan kuat, dan sangat cocok untuk memecahkan masalah di mana abstraksi aliran menunjukkan dirinya (dalam praktiknya, ini terutama koordinasi tugas asinkron). Tetapi apakah Anda akan menulis, misalnya, validasi formulir sinkron sederhana pada Rx? Apakah dia akan menghemat waktu Anda dibandingkan dengan pendekatan imperatif biasa?

mrr adalah upaya untuk membuktikan bahwa FRF dapat menjadi solusi yang nyaman dan efektif tidak hanya dalam masalah "streaming" tertentu, tetapi juga dalam tugas-tugas front-end rutin yang paling umum.

Pemrograman reaktif adalah abstraksi yang sangat kuat, saat ini hadir di frontend dalam dua cara:

  • variabel reaktif (variabel yang dihitung): sederhana, andal, intuitif, tetapi potensi RP masih jauh dari terungkap sepenuhnya
  • perpustakaan untuk bekerja dengan stream, seperti Rx, Bacon, dll .: kuat, tetapi agak rumit, ruang lingkup penggunaan praktis terbatas pada tugas-tugas tertentu.

Mrr menggabungkan keunggulan dari pendekatan ini. Tidak seperti Rx.js, mrr memiliki API pendek, yang dapat diperluas pengguna dengan tambahannya. Alih-alih puluhan metode dan operator - empat operator dasar, alih-alih diamati (panas dan dingin), Subjek, dll. - satu abstraksi: aliran. Juga, mrr tidak memiliki beberapa konsep kompleks yang secara signifikan dapat menyulitkan keterbacaan kode, misalnya, metastreams.

Namun, mrr bukan "Rx yang disederhanakan dengan cara baru." Berdasarkan prinsip-prinsip dasar yang sama seperti Rx, mrr mengklaim sebagai ceruk yang lebih besar: mengelola status aplikasi global dan lokal (pada tingkat komponen). Meskipun konsep asli pemrograman reaktif dimaksudkan untuk bekerja dengan tugas-tugas yang tidak serempak, mrr telah berhasil menggunakan pendekatan reaktivitas untuk tugas-tugas yang biasa dan sinkron. Ini adalah prinsip "FRP total".

Seringkali ketika membuat aplikasi pada Bereaksi, beberapa teknologi yang berbeda digunakan: mengomposisi ulang (atau segera - kait) untuk status komponen, Redux / mobx untuk keadaan global, Rx dengan redux-observable (atau thunk / saga) untuk mengelola efek samping dan mengoordinasikan asinkron. tugas di editor. Alih-alih seperti "salad" dari berbagai pendekatan dan teknologi dalam aplikasi yang sama, dengan mrr Anda dapat menggunakan teknologi dan paradigma tunggal.

Antarmuka mrr juga sangat berbeda dari Rx dan pustaka sejenis - lebih deklaratif. Berkat abstraksi reaktivitas dan pendekatan deklaratif, mrr memungkinkan Anda untuk menulis kode yang ekspresif dan ringkas. Misalnya, TodoMVC standar pada mrr membutuhkan kurang dari 50 baris kode (tidak termasuk template JSX).

Tapi cukup iklan. Apakah Anda berhasil menggabungkan keunggulan RP "ringan" dan "berat" dalam satu botol - Anda harus menilai, tetapi pertama-tama, harap baca contoh kode.

TodoMVC sudah cukup sakit, dan contoh mengunduh data tentang pengguna Github terlalu primitif untuk dapat merasakan fitur perpustakaan di atasnya. Kami akan mempertimbangkan mrr sebagai contoh aplikasi bersyarat untuk membeli tiket kereta api. Di UI kami akan ada bidang untuk memilih stasiun mulai dan berakhir, tanggal. Kemudian, setelah mengirim data, daftar kereta dan tempat yang tersedia di dalamnya akan dikembalikan. Setelah memilih kereta dan jenis mobil tertentu, pengguna akan memasukkan data penumpang, dan kemudian menambahkan tiket ke keranjang. Ayo pergi.

Kami membutuhkan formulir dengan pilihan stasiun dan tanggal:



Buat bidang pelengkapan otomatis untuk memasuki stasiun.

import { withMrr } from 'mrr'; const stations = [ '', '', '', ' ', ... ] const Tickets = withMrr({ //    $init: { stationFromOptions: [], stationFromInput: '', }, //   - "" stationFromOptions: [str => stations.filter(s => s.indexOf(str)===0), 'stationFromInput'], }, (state, props, $) => { return (<div> <h3>    </h3> <div>  : <input onChange={ $('stationFromInput') } /> </div> <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li>{ s }</li>) } </ul> </div>); }); export default Tickets; 

komponen mrr dibuat menggunakan fungsi withMrr, yang menerima diagram tautan reaktif (deskripsi aliran) dan fungsi render. Fungsi render diteruskan ke penyangga komponen, serta keadaan, yang sekarang sepenuhnya dikendalikan oleh mrr. Ini akan berisi inisial (blok $ init) dan dihitung dengan nilai rumus sel reaktif.

Sekarang kita memiliki dua sel (atau dua aliran, yang merupakan hal yang sama): stationFromInput , nilai yang jatuh dari input pengguna menggunakan $ helper (lewat event.target.value secara default untuk elemen input data), dan sel yang berasal darinya stationFromOptions , berisi larik stasiun yang sesuai dengan nama.

Nilai stationFromOptions secara otomatis dihitung setiap kali sel induk diubah menggunakan fungsi (dalam terminologi mrr disebut " formula " - dengan analogi dengan rumus Excel). Sintaks ekspresi mrr sederhana: di tempat pertama adalah fungsi (atau operator), yang digunakan untuk menghitung nilai sel, lalu ada daftar sel tempat sel ini bergantung: nilainya diteruskan ke fungsi. Sepintas aneh, sintaksis memiliki banyak keunggulan, yang akan kita bahas nanti. Sejauh ini, logika mrr di sini mengingatkan pada pendekatan yang biasa dengan variabel yang dapat dihitung yang digunakan dalam Vue, Svelte dan pustaka lainnya, dengan satu-satunya perbedaan adalah bahwa Anda dapat menggunakan fungsi murni.

Kami menerapkan penggantian stasiun yang dipilih dari daftar di bidang input. Anda juga perlu menyembunyikan daftar stasiun setelah pengguna mengklik salah satunya.

 const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'], }, (state, props, $) => { return (<div> <div>  : <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>); }); 

Penangan acara yang dibuat menggunakan $ helper dalam daftar stasiun akan memancarkan nilai tetap untuk setiap opsi.

mrr konsisten dalam pendekatan deklaratifnya, asing terhadap mutasi apa pun. Setelah memilih stasiun, kami tidak dapat "memaksa" mengubah nilai sel. Sebagai gantinya, kami membuat sel stationFrom baru, yang, menggunakan operator aliran gabung (perkiraan setara pada Rx digabungkanLatest), akan mengumpulkan nilai dua aliran: input pengguna ( stationFromInput ) dan pemilihan stasiun ( pilihStationFrom ).

Kami harus menunjukkan daftar opsi setelah pengguna memasukkan sesuatu, dan bersembunyi setelah dia memilih salah satu opsi. Sel optionsShown akan bertanggung jawab atas visibilitas daftar opsi, yang akan mengambil nilai Boolean tergantung pada perubahan sel lainnya. Ini adalah pola yang sangat umum di mana gula sintaksis ada - operator beralih . Ini mengatur nilai sel menjadi true pada setiap perubahan argumen pertama (stream), dan false - yang kedua.

Tambahkan tombol untuk menghapus teks yang dimasukkan.

 const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], clearVal: [a => '', 'clear'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', 'clearVal'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'], }, (state, props, $) => { return (<div> <div>  : <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> { state.stationFrom && <button onClick={ $('clear') }></button> } </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>); }); 

Sekarang sel stationFrom kami, yang bertanggung jawab atas isi teks di bidang input, mengumpulkan nilainya bukan dari dua, tetapi dari tiga aliran. Kode ini dapat disederhanakan. Konstruk mrr dari bentuk [* formula *, * ... argumen sel *] mirip dengan ekspresi-S di Lisp, dan seperti pada Lisp, Anda dapat secara sewenang-wenang membuat konstruksi seperti itu satu sama lain.

Mari kita singkirkan sel clearVal yang tidak berguna dan perpendek kode:

  stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', [a => '', 'clear']], 

Program yang ditulis dengan gaya imperatif dapat dibandingkan dengan tim yang tidak terorganisir dengan baik, di mana setiap orang terus-menerus memesan sesuatu satu sama lain (saya memberi petunjuk pada panggilan metode dan mengubah perubahan), apalagi, kedua manajer adalah bawahan, dan sebaliknya. Program deklaratif mirip dengan gambaran utopis yang berlawanan: sebuah kolektif di mana setiap orang jelas tahu bagaimana ia harus bertindak dalam situasi apa pun. Tidak perlu untuk pesanan di tim seperti itu, semua orang hanya di tempat mereka dan bekerja dalam menanggapi apa yang terjadi.

Alih-alih menggambarkan semua konsekuensi yang mungkin terjadi dari suatu peristiwa (baca - untuk membuat mutasi tertentu), kami menggambarkan semua kasus di mana peristiwa ini dapat terjadi, mis. berapa nilai sel akan mengambil setiap perubahan di sel lain. Dalam contoh kecil kami sejauh ini, kami telah menggambarkan sel stationFrom dan tiga situasi yang mempengaruhi nilainya. Untuk seorang programmer yang terbiasa dengan kode imperatif, pendekatan seperti itu mungkin tampak tidak biasa (atau bahkan "penopang", "penyimpangan"). Bahkan, ini memungkinkan Anda untuk menghemat upaya karena singkatnya (dan stabilitas) kode, seperti yang akan kita lihat dalam praktiknya.

Bagaimana dengan asinkron? Apakah mungkin untuk menarik daftar stasiun yang diusulkan dengan ajax? Tidak masalah! Pada dasarnya, tidak masalah bagi mrr apakah fungsi mengembalikan nilai atau janji. Ketika mrr dikembalikan, ia akan menunggu resolusi dan "mendorong" data yang diterima ke dalam aliran.

 stationFromOptions: [str => fetch('/get_stations?str=' + str).then(res => res.toJSON()), 'stationFromInput'], 

Ini juga berarti bahwa Anda dapat menggunakan fungsi asinkron sebagai rumus. Kasus yang lebih kompleks (penanganan kesalahan, status janji) akan dipertimbangkan nanti.

Fungsi untuk memilih stasiun keberangkatan siap. Tidak masuk akal untuk menggandakan hal yang sama untuk stasiun kedatangan, ada baiknya memasukkannya ke dalam komponen terpisah yang dapat digunakan kembali. Ini akan menjadi komponen input umum dengan pelengkapan otomatis, jadi kami akan mengganti nama bidang dan membuat fungsi untuk mendapatkan opsi yang sesuai diatur dalam alat peraga.

 const OptionsInput = withMrr(props => ({ $init: { options: [], }, val: ['merge', 'valInput', 'selectOption', [a => '', 'clear']], options: [props.getOptions, 'val'], optionsShown: ['toggle', 'valInput', 'selectOption'], }), (state, props, $) => <div> <div> <input onChange={ $('valInput') } value={ state.val } /> </div> { state.optionsShown && <ul className="options"> { state.options.map(s => <li onClick={ $('selectOption', s) }>{ s }</li>) } </ul> } { state.val && <div className="clear" onClick={ $('clear') }> X </div> } </div>) 

Seperti yang Anda lihat, Anda dapat menentukan struktur sel mrr sebagai fungsi dari komponen alat peraga (namun, itu akan dieksekusi hanya sekali - setelah inisialisasi, dan tidak akan menanggapi perubahan dalam alat peraga).

Komunikasi antar komponen


Sekarang hubungkan komponen ini dalam komponen induk dan lihat bagaimana mrr memungkinkan komponen terkait untuk bertukar data.

 const getMatchedStations = str => fetch('/get_stations?str=' + str).then(res => res.toJSON()); const Tickets = withMrr({ stationTo: 'selectStationFrom/val', stationFrom: 'selectStationTo/val', }, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div>); }); 

Untuk mengaitkan komponen induk dengan komponen turunan , kita harus meneruskan parameter ke dalamnya menggunakan fungsi connectAs (argumen keempat dari fungsi render). Dalam hal ini, kami menunjukkan nama yang ingin kami berikan kepada komponen anak. Dengan melampirkan komponen dengan cara ini, dengan nama ini kita dapat mengakses sel-selnya. Dalam hal ini, kami mendengarkan sel val. Kebalikannya juga dimungkinkan - dengarkan dari komponen anak dari sel induk.

Seperti yang Anda lihat, di sini mrr mengikuti pendekatan deklaratif: tidak ada panggilan balik onChange diperlukan, kita hanya perlu menentukan nama untuk komponen anak dalam fungsi connectAs, setelah itu kita mendapatkan akses ke sel-selnya! Dalam hal ini, sekali lagi karena deklaratif, tidak ada ancaman gangguan dalam pekerjaan komponen lain - kita tidak memiliki kemampuan untuk "mengubah" apa pun di dalamnya, bermutasi, kita hanya dapat "mendengarkan" data.

Sinyal dan Nilai


Tahap selanjutnya adalah mencari kereta yang sesuai untuk parameter yang dipilih. Dalam pendekatan imperatif, kami mungkin akan menulis prosesor tertentu untuk mengirimkan formulir onSubmit, yang akan memulai tindakan lebih lanjut - permintaan ajax dan tampilan hasilnya. Tetapi, seperti yang Anda ingat, kami tidak dapat "memesan" apa pun! Kami hanya dapat membuat set sel lain yang berasal dari sel formulir. Mari menulis satu permintaan lagi.

 const getTrains = (from, to, date) => fetch('/get_trains?from=' + from + '&to=' + to + '&date=' + date).then(res => res.toJSON()); const Tickets = withMrr({ stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', results: [getTrains, 'stationFrom', 'stationTo', 'date', 'searchTrains'], }, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div>); }); 

Namun, kode tersebut tidak akan berfungsi seperti yang diharapkan. Perhitungan (penghitungan ulang nilai sel) dimulai ketika salah satu argumen berubah, sehingga permintaan akan dikirim, misalnya, segera setelah memilih stasiun pertama, dan tidak hanya dengan mengklik "Cari". Kita perlu, di satu sisi, bahwa stasiun dan tanggal diteruskan ke argumen formula, tetapi di sisi lain, tidak menanggapi perubahan mereka. Dalam mrr ada mekanisme yang elegan untuk ini yang disebut mendengarkan pasif.

  results: [getTrains, '-stationFrom', '-stationTo', '-date', 'searchTrains'], 

Tambahkan saja minus di depan nama sel, dan voila! Sekarang, hasil hanya akan merespons perubahan pada sel searchTrains .

Dalam hal ini, sel searchTrains bertindak sebagai "sinyal sel", dan sel stationFrom dan lainnya sebagai "nilai sel". Untuk sel sinyal, hanya saat ketika nilai-nilai "mengalir" melalui itu penting, dan seperti apa data itu - lagipula: itu bisa saja benar, "1" atau apa pun (dalam kasus kami, ini akan menjadi objek acara DOM ) Untuk sel nilai, itu adalah nilainya yang penting, tetapi pada saat yang sama, momen-momen perubahannya tidak signifikan. Kedua jenis sel ini tidak eksklusif satu sama lain: banyak sel adalah sinyal dan nilai. Pada level sintaks dalam mrr, kedua tipe sel ini tidak berbeda dalam cara apa pun, tetapi pemahaman konseptual dari perbedaan seperti itu sangat penting ketika menulis kode reaktif.

Membelah aliran


Permintaan untuk mencari tempat duduk di kereta mungkin memakan waktu, jadi kami harus menunjukkan loader, dan juga merespons jika ada kesalahan. Sudah ada beberapa janji untuk pendekatan default ini dengan penyelesaian otomatis.

 const Tickets = withMrr({ $init: { results: {}, } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['nested', (cb, query) => { cb({ loading: true, error: null, data: null }); getTrains(query.from, query.to, query.date) .then(res => cb('data', res)) .catch(err => cb('error', err)) .finally(() => cb('loading', false)) }, 'searchQuery'], availableTrains: 'results.data', }, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div> <div> { state.results.loading && <div className="loading">...</div> } { state.results.error && <div className="error"> . ,  .   .</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train) => <div />) } </div> } </div> </div>); }); 

Operator bersarang memungkinkan Anda untuk "mendekomposisi" data menjadi subkellel, karena ini, argumen pertama untuk formula ini adalah callback, yang dengannya Anda dapat "mendorong" data ke dalam subsel (satu atau lebih). Sekarang kami memiliki aliran terpisah yang bertanggung jawab atas kesalahan, status janji dan untuk data yang diterima. Operator bersarang adalah alat yang sangat kuat dan salah satu dari sedikit keharusan dalam mrr (kami sendiri menentukan di mana sel-sel untuk meletakkan data). Sementara operator gabungan menggabungkan beberapa utas menjadi satu, nested membagi utas menjadi beberapa sub-utas, sehingga menjadi kebalikannya.

Contoh di atas adalah cara standar untuk bekerja dengan janji, pada mrr itu digeneralisasi sebagai operator janji dan memungkinkan Anda untuk mempersingkat kode:

  results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], //     availableTrains: 'results.data', 

Juga, operator janji memastikan bahwa hanya hasil janji terbaru yang digunakan.



Komponen untuk menampilkan kursi yang tersedia (untuk kesederhanaan kami akan menolak dari berbagai jenis mobil)

 const TrainSeats = withMrr({ selectSeats: [(seatsNumber, { id }) => new Array(Number(seatsNumber)).fill(true).map(() => ({ trainId: id })), '-seatsNumber', '-$props', 'select'], seatsNumber: [() => 0, 'selectSeats'], }, (state, props, $) => <div className="train">  โ„–{ props.num } { props.from } - { props.to }.   : { props.seats || 0 } { props.seats && <div>     : <input type="number" onChange={ $('seatsNumber') } value={ state.seatsNumber || 0 } max={ props.seats } /> <button onClick={ $('select') }></button> </div> } </div>); 

Untuk mengakses alat peraga dalam rumus, Anda dapat berlangganan kotak $ alat peraga khusus.

 const Tickets = withMrr({ ... selectedSeats: '*/selectSeats', }, (state, props, $, connectAs) => { ... <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div> } 

Kami lagi menggunakan pendengaran pasif untuk mengambil jumlah tempat yang dipilih ketika Anda mengklik tombol "Pilih". Kami mengaitkan setiap komponen anak dengan induk menggunakan fungsi connectAs. Pengguna dapat memilih kursi di salah satu kereta yang diusulkan, jadi kami mendengarkan perubahan di semua komponen anak menggunakan mask "*".

Tapi ini masalahnya: pengguna bisa menambahkan kursi pertama di satu kereta, lalu di yang lain, sehingga data baru akan menggiling yang sebelumnya. Bagaimana cara "mengakumulasi" mengalirkan data? Untuk melakukan ini, ada operator penutupan , yang, bersama dengan bersarang dan corong, membentuk dasar mrr (semua yang lain tidak lebih dari gula sintaksis berdasarkan ketiga ini).

  selectedSeats: ['closure', () => { let seats = []; //     return selectedSeats => { seats = [...seats, selectedSeats]; return seats; } }, '*/selectSeats'], 

Saat menggunakan penutupan pertama (pada componentDidMount), penutupan dibuat yang mengembalikan rumus. Dia dengan demikian memiliki akses ke variabel penutupan. Ini memungkinkan Anda untuk menyimpan data antar panggilan dengan cara yang aman - tanpa masuk ke jurang variabel global dan berbagi keadaan yang bisa berubah. Dengan demikian, penutupan memungkinkan Anda untuk mengimplementasikan fungsionalitas operator Rx seperti pemindaian dan lainnya. Namun, metode ini bagus untuk kasus-kasus sulit. Jika kita hanya perlu menyimpan nilai satu variabel, kita cukup menggunakan tautan ke nilai sel sebelumnya menggunakan nama khusus "^":

  selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^'] 

Sekarang pengguna harus memasukkan nama depan dan belakang untuk setiap tiket yang dipilih.

 const SeatDetails = withMrr({}, (state, props, $) => { return (<div> { props.trainId } <input name="name" value={ props.name } onChange={ $('setDetails', e => ['name', e.target.value, props.i]) } /> <input name="surname" value={ props.surname } onChange={ $('setDetails', e => ['surname', e.target.value, props.i]) }/> <a href="#" onClick={ $('removeSeat', props.i) }>X</a> </div>); }) const Tickets = withMrr({ $init: { results: {}, selectedSeats: [], } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], availableTrains: 'results.data', selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^'] }, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div> <div> { state.results.loading && <div className="loading">...</div> } { state.results.error && <div className="error"> . ,  .   .</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div> } { state.selectedSeats.map((seat, i) => <SeatDetails key={i} i={i} { ...seat } {...connectAs('seat' + i)}/>) } </div> </div>); }); 

Sel SelesaiPilih berisi array dari lokasi yang dipilih. Saat pengguna memasukkan nama dan nama keluarga untuk setiap tiket, kami harus mengubah data dalam elemen array yang sesuai.

  selectedSeats: [(seats, details, prev) => { // ??? }, '*/selectSeats', '*/setDetails', '^'] 

Pendekatan standar tidak cocok untuk kita: dalam rumus, kita perlu mengetahui sel mana yang telah berubah dan meresponsnya. Salah satu bentuk operator gabungan akan membantu kami.

  selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, }, '^'/*,       */], 

Ini sedikit mirip reduksi Redux, tetapi dengan sintaksis yang lebih fleksibel dan kuat. Dan Anda tidak bisa takut untuk mengubah array, karena hanya rumus satu sel yang mengendalikannya, sehingga perubahan paralel dikecualikan (tetapi array bermutasi yang diteruskan sebagai argumen tentu tidak layak).

Koleksi Reaktif


Pola ketika sel menyimpan dan mengubah array sangat umum. Semua operasi dengan array terdiri dari tiga jenis: masukkan, ubah, hapus. Untuk menggambarkan ini, ada operator coll yang elegan. Gunakan itu untuk menyederhanakan perhitungan Sel yang dipilih .

Itu:

  selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, 'addToCart': () => [], }, '^'] 

menjadi:

  selectedSeats: ['coll', { create: '*/selectSeats', update: '*/setDetails', delete: ['merge', '*/removeSeat', [() => ({}), 'addToCart']] }] 

namun, format data dalam aliran setDetails perlu diubah sedikit:

  <input name="name" onChange={ $('setDetails', e => [{ name: e.target.value }, props.i]) } /> <input name="surname" onChange={ $('setDetails', e => [{ surname: e.target.value }, props.i]) }/> 

Menggunakan operator coll , kami menjelaskan tiga utas yang akan memengaruhi array kami. Dalam hal ini, aliran buat harus mengandung elemen itu sendiri, yang harus ditambahkan ke array (biasanya objek). Aliran penghapusan menerima indeks elemen yang akan dihapus (baik di '* / removeSeat') dan mask. Mask {} akan menghapus semua elemen, dan, misalnya, mask {name: 'Carl'} akan menghapus semua elemen dengan nama Carl. Aliran pembaruan menerima pasangan nilai: perubahan yang perlu dilakukan dengan elemen (mask atau fungsi), dan indeks atau mask dari elemen yang perlu diubah. Misalnya, [{nama keluarga: 'Johnson'}, {}] akan menetapkan nama belakang Johnson ke semua elemen array.

Operator coll menggunakan sesuatu seperti bahasa permintaan internal, membuatnya lebih mudah untuk bekerja dengan koleksi dan membuatnya lebih deklaratif.

Kode lengkap aplikasi kita di JsFiddle.

Kami berkenalan dengan hampir semua fungsi dasar yang diperlukan dari mrr. Topik yang cukup signifikan yang tetap berlebihan adalah manajemen kekayaan global, yang dapat dibahas dalam artikel mendatang. Tetapi sekarang Anda dapat mulai menggunakan mrr untuk mengelola keadaan di dalam komponen atau grup komponen terkait.

Kesimpulan


Apa kekuatan mrr?


mrr memungkinkan Anda untuk menulis aplikasi dalam Bereaksi dalam gaya fungsional-reaktif (mrr dapat didekripsi sebagai Make React Reactive). mrr sangat ekspresif - Anda menghabiskan lebih sedikit waktu untuk menulis baris kode.

mrr menyediakan serangkaian kecil abstraksi dasar, yang cukup untuk semua kasus - artikel ini menjelaskan hampir semua fitur utama dan teknik mrr.Ada juga alat untuk memperluas set dasar ini (kemampuan untuk membuat operator kustom). Anda dapat menulis kode deklaratif yang indah tanpa membaca ratusan halaman manual dan bahkan tanpa mempelajari kedalaman teoritis pemrograman fungsional - Anda tidak mungkin harus menggunakan, katakanlah, monads, karena mrr sendiri adalah monad raksasa yang memisahkan komputasi murni dari mutasi negara.

Sementara di perpustakaan lain pendekatan heterogen (imperatif menggunakan metode dan deklaratif menggunakan pengikat reaktif) sering hidup berdampingan, di mana programmer secara acak mencampur "salad", di mrr ada esensi dasar tunggal - aliran, yang memberikan kontribusi untuk kode homogenitas dan keseragaman. Kenyamanan, kemudahan, kesederhanaan, menghemat waktu programmer adalah keuntungan utama mrr (dari sini adalah decoding lain mrr sebagai "mrrrr", yaitu mendengkur kucing yang puas dengan kehidupan kucing).

Apa kerugiannya?


Pemrograman dengan "baris" memiliki kelebihan dan kekurangan. Anda tidak akan dapat melengkapi nama sel secara otomatis, atau mencari tempat di mana ia didefinisikan. Di sisi lain, di mrr selalu ada satu dan hanya satu tempat di mana perilaku sel ditentukan, dan mudah untuk menemukannya dengan pencarian teks sederhana, sambil mencari tempat di mana nilai bidang Redux toko ditentukan, atau bahkan lebih sedikit bidang negara ketika menggunakan setState asli mungkin lebih lama.

Siapa yang bisa tertarik dengan ini?


, โ€” , . , ClojureScript, , React . Redux, mrr , . , mrr ยซ ยป, , mrr .

?


, :) , , API , ( ), . , mrr React', (React ).

, !

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


All Articles