Redux ReactiveX

Setiap orang yang bekerja dengan Redux cepat atau lambat akan mengalami masalah tindakan asinkron. Tetapi aplikasi modern tidak dapat dikembangkan tanpa mereka. Ini adalah http-request ke backend, dan semua jenis timer / penundaan. Pembuat Redux sendiri berbicara dengan jelas - secara default, hanya aliran data sinkron yang didukung, semua tindakan asinkron harus ditempatkan dalam middleware.

Tentu saja, ini terlalu verbal dan merepotkan, sehingga sulit menemukan pengembang yang hanya menggunakan middleware "asli". Perpustakaan dan kerangka kerja seperti Thunk, Saga, dan sejenisnya selalu datang untuk menyelamatkan.

Untuk sebagian besar tugas, itu sudah cukup. Tetapi bagaimana jika logika yang sedikit lebih rumit diperlukan daripada mengirim satu permintaan atau membuat satu pengatur waktu? Ini adalah contoh kecil:

async dispatch => { setTimeout(() => { try { await Promise .all([fetchOne, fetchTwo]) .then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); } 

Sangat menyakitkan bahkan untuk melihat kode seperti itu, tetapi tidak mungkin untuk mempertahankan dan memperluas. Apa yang harus dilakukan ketika penanganan kesalahan yang lebih canggih diperlukan? Bagaimana jika Anda membutuhkan permintaan berulang? Dan jika saya ingin menggunakan kembali fitur ini?

Nama saya Dmitry Samokhvalov, dan dalam posting ini saya akan memberi tahu Anda apa konsep Observable dan bagaimana mempraktikkannya bersama dengan Redux, dan juga membandingkan semua ini dengan kemampuan Redux-Saga.

Sebagai aturan, dalam kasus seperti itu, ambil redux-saga. Oke, kami menulis ulang kisah-kisahnya:

 try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); } 

Ini menjadi lebih baik - kode hampir linier, terlihat dan lebih baik dibaca. Tetapi memperluas dan menggunakan kembali masih sulit, karena hikayat sama pentingnya dengan thunk.

Ada pendekatan lain. Ini persis pendekatan, dan bukan hanya perpustakaan lain untuk menulis kode asinkron. Ini disebut Rx (mereka juga dapat diamati, Streaming Reaktif, dll.). Kami akan menggunakannya dan menulis ulang contoh di Observable:

 action$ .delay(2000) .switchMap(() => Observable.merge(fetchOne, fetchTwo) .map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .catch(error => ({ type: 'FAILED', error })) 

Kode tidak hanya menjadi datar dan menurun volumenya, prinsip menggambarkan tindakan asinkron telah berubah. Sekarang kita tidak bekerja secara langsung dengan query, tetapi melakukan operasi pada objek khusus yang disebut Observable.

Lebih mudah untuk mewakili Observable sebagai fungsi yang memberikan aliran (urutan) nilai. Dapat diamati memiliki tiga keadaan utama - berikutnya ("berikan nilai berikutnya"), kesalahan ("terjadi kesalahan") dan lengkap ("nilai-nilai sudah berakhir, tidak ada lagi yang bisa diberikan"). Dalam hal ini, ini agak seperti Janji, tetapi berbeda karena mungkin untuk mengulangi nilai-nilai ini (dan ini adalah salah satu kekuatan super yang Dapat Diamati). Anda dapat membungkus apa pun di Observable - timeout, permintaan http, acara DOM, hanya objek js.



Negara adikuasa kedua yang dapat diamati adalah operator. Operator adalah fungsi yang menerima dan mengembalikan Observable, tetapi melakukan beberapa tindakan pada aliran nilai. Analogi terdekat adalah peta dan filter dari javascript (omong-omong, operator seperti itu ada di Rx).



Bagi saya pribadi yang paling berguna adalah operator zip, forkJoin, dan flatMap. Menggunakan contoh mereka, paling mudah untuk menjelaskan pekerjaan operator.

Operator zip bekerja sangat sederhana - dibutuhkan beberapa yang dapat diobservasi (tidak lebih dari 9) dan mengembalikan dalam array nilai yang dipancarkannya.

 const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .subscribe(e => console.log(`${e[0].x} ${e[1].x}`)); //output [119,120] [120,233] … 

Secara umum, pekerjaan zip dapat diwakili oleh skema:



Zip digunakan jika Anda memiliki beberapa Observable dan Anda perlu secara konsisten menerima nilai dari mereka (terlepas dari kenyataan bahwa mereka dapat dipancarkan pada interval yang berbeda, secara sinkron atau tidak). Ini sangat berguna saat bekerja dengan acara DOM.

Pernyataan forkJoin mirip dengan zip dengan satu pengecualian - hanya mengembalikan nilai terbaru dari setiap Observable.



Dengan demikian, masuk akal untuk menggunakannya ketika hanya nilai terbatas dari aliran diperlukan.
Yang sedikit lebih rumit adalah operator flatMap. Dibutuhkan Observable sebagai input dan mengembalikan Observable baru, dan memetakan nilai-nilai dari itu ke Observable baru, baik menggunakan fungsi pemilih atau Observable lainnya. Kedengarannya membingungkan, tetapi diagramnya cukup sederhana:



Lebih jelas dalam kode:

 const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .flatMap(value => promise(value)) .subscribe(result => console.log(result)); //output "Hello World" 

Paling sering, flatMap digunakan dalam permintaan backend, bersama dengan switchMap dan concatMap.
Bagaimana saya bisa menggunakan Rx di Redux? Ada perpustakaan redux-observable yang indah untuk ini. Arsitekturnya terlihat seperti ini:



Semua operator dan tindakan yang dapat diobservasi dibuat dalam bentuk middleware khusus yang disebut epik. Setiap epik mengambil tindakan sebagai input, membungkusnya dalam Observable, dan harus mengembalikan action, juga sebagai Observable. Anda tidak dapat mengembalikan tindakan biasa, ini membuat loop tanpa akhir. Mari kita menulis epik kecil yang membuat permintaan untuk api.

 const fetchEpic = action$ => action$ .ofType('FETCH_INFO') .map(() => ({ type: 'FETCH_START' })) .flatMap(() => Observable .from(apiRequest) .map(data => ({ type: 'FETCH_SUCCESS', data })) .catch(error => ({ type: 'FETCH_ERROR', error })) ) 

Tidak mungkin dilakukan tanpa membandingkan redux-observable dan redux-saga. Tampaknya banyak yang dekat dengan fungsionalitas dan kemampuan, tetapi ini sama sekali tidak terjadi. Sagas adalah alat yang sangat penting, pada dasarnya seperangkat metode untuk bekerja dengan efek samping. Dapat diamati adalah gaya penulisan kode asinkron yang secara fundamental berbeda, jika Anda mau, filosofi yang berbeda.

Saya menulis beberapa contoh untuk menggambarkan kemungkinan dan pendekatan untuk memecahkan masalah.

Misalkan kita perlu mengimplementasikan timer yang akan berhenti beraksi. Inilah yang terlihat dalam kisah-kisah:

 while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } } 

Sekarang gunakan Rx:

 interval(1000) .takeUntil(action$.ofType('STOP')) 


Misalkan ada tugas untuk mengimplementasikan permintaan dengan pembatalan dalam kisah:

 function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); } 

Semuanya lebih sederhana di Rx:

 switchMap(() => fetchUser()) .takeUntil(action$.ofType('FETCH_CANCEL')) 

Akhirnya, favorit saya. Terapkan permintaan api, jika terjadi kegagalan, buat tidak lebih dari 5 permintaan berulang dengan penundaan 2 detik. Inilah yang kita miliki dalam kisah-kisah:

 for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); } 

Apa yang terjadi pada Rx:

 .retryWhen(errors => errors .delay(1000) .take(5)) 

Jika Anda merangkum pro dan kontra dari saga, Anda mendapatkan gambar berikut:



Sagas mudah dipelajari dan sangat populer, jadi di komunitas Anda dapat menemukan resep untuk hampir semua kesempatan. Sayangnya, gaya imperatif mencegah penggunaan kisah-kisah yang sangat fleksibel.

Rx memiliki situasi yang sama sekali berbeda:



Tampaknya Rx adalah palu ajaib dan peluru perak. Sayangnya, ini tidak benar. Ambang untuk memasukkan Rx jauh lebih tinggi, oleh karena itu lebih sulit untuk memperkenalkan orang baru ke proyek yang secara aktif menggunakan Rx.

Selain itu, ketika bekerja dengan Observable, sangat penting untuk berhati-hati dan selalu memahami dengan baik apa yang terjadi. Jika tidak, Anda mungkin menemukan kesalahan yang tidak terlihat atau perilaku yang tidak terdefinisi.

 action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'}))) 

Setelah saya menulis sebuah epik yang melakukan pekerjaan yang cukup sederhana - dengan setiap tindakan ketik 'HAPUS', metode API dipanggil yang menghapus item. Namun, ada masalah selama pengujian. Penguji mengeluh tentang perilaku aneh - kadang-kadang ketika Anda mengklik tombol hapus tidak ada yang terjadi. Ternyata operator switchMap mendukung pelaksanaan hanya satu Observable pada satu waktu, semacam perlindungan terhadap kondisi balapan.

Sebagai hasilnya, saya akan memberikan beberapa rekomendasi yang saya ikuti dan mendesak semua orang yang mulai bekerja dengan Rx untuk mengikuti:

  • Berhati-hatilah.
  • Periksa dokumentasinya.
  • Periksa di kotak pasir.
  • Tulis tes.
  • Jangan menembak burung pipit dari meriam.

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


All Articles