Cara melakukan pencarian pengguna di GitHub menggunakan React + RxJS 6 + Komposisi Ulang

Sebuah gambar untuk menarik perhatian


Artikel ini ditujukan untuk orang yang berpengalaman dengan Bereaksi dan RxJS. Saya hanya berbagi templat yang menurut saya berguna untuk membuat UI seperti itu.


Inilah yang kami lakukan:



Tanpa kelas, bekerja dengan siklus hidup atau setState .


Persiapan


Yang Anda butuhkan adalah di repositori saya di GitHub.


 git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install 

Di cabang master ada proyek selesai. Beralih ke cabang start jika Anda ingin pergi langkah demi langkah.


 git checkout start 

Dan jalankan proyeknya.


 npm start 

Aplikasi harus dimulai di localhost:3000 dan ini adalah UI awal kami.



Luncurkan editor favorit Anda dan buka file src/index.js .



Komposisikan ulang


Jika Anda tidak terbiasa dengan Komposisi Ulang , ini adalah pustaka hebat yang memungkinkan Anda membuat komponen Bereaksi dalam gaya fungsional. Ini berisi sejumlah besar fungsi. Ini adalah favorit saya .


Ini seperti Lodash / Ramda, React saja.


Saya juga sangat senang karena mendukung pola Observer. Mengutip dokumentasi :


Ternyata sebagian besar React Component API dapat dinyatakan dalam pola Observer.

Hari ini kita akan berlatih konsep ini!


Komponen sebaris


Sejauh ini, kami memiliki App - komponen Bereaksi yang paling umum. Menggunakan fungsi componentFromStream dari pustaka komposisi ulang kita bisa mendapatkannya melalui objek yang bisa diamati.


Fungsi componentFromStream memulai render pada setiap nilai baru dari yang kami amati. Jika belum ada nilai, itu berarti null .


Konfigurasi


Aliran dalam Komposisi mengikuti dokumen Proposal Yang Dapat Diamati ECMAScript . Ini menjelaskan bagaimana objek yang dapat diobservasi bekerja ketika mereka diimplementasikan di browser modern.


Sementara itu, kami akan menggunakan perpustakaan seperti RxJS, xstream, most, Flyd, dll.


Komposisi ulang tidak tahu perpustakaan mana yang kami gunakan, jadi ia menyediakan fungsi setObservableConfig . Dengan itu, Anda dapat mengonversi semua yang kita butuhkan menjadi ES Observable.


Buat file baru di folder src dan beri nama observableConfig.js .


Untuk menghubungkan RxJS 6 ke Komposisi Ulang, tulis kode berikut di dalamnya:


 import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from }); 

Impor file ini ke index.js :


 import './observableConfig'; 

Itu saja!


Komposisi ulang + RxJS


Tambahkan impor componentFromStream ke index.js :


 import { componentFromStream } from 'recompose'; 

Mari kita mulai mengganti komponen App :


 const App = componentFromStream(prop$ => { ... }); 

Perhatikan bahwa componentFromStream mengambil sebagai argumen fungsi dengan prop$ parameter, yang merupakan versi props dapat diamati. Idenya adalah menggunakan peta untuk mengubah props biasa menjadi komponen Bereaksi.


Jika Anda menggunakan RxJS, Anda harus terbiasa dengan operator peta .


Peta


Seperti namanya, peta mengubah Observable(something) menjadi Observable(somethingElse) . Dalam kasus kami - Observable(props) di Observable(component) .


Impor operator map :


 import { map } from 'rxjs/operators'; 

Tambahan komponen App kami:


 const App = componentFromStream(prop$ => { return prop$.pipe( map(() => ( <div> <input placeholder="GitHub username" /> </div> )) ) }); 

Dengan RxJS 5, kami menggunakan pipe bukan rantai pernyataan.


Simpan file dan periksa hasilnya. Tidak ada yang berubah!



Tambahkan pengendali acara


Sekarang kita akan membuat kolom input kita sedikit reaktif.


Tambahkan impor createEventHandler :


 import { componentFromStream, createEventHandler } from 'recompose'; 

Kami akan menggunakannya seperti ini:


 const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

Objek yang dibuat oleh createEventHandler memiliki dua bidang yang menarik: handler dan stream .


Di bawah tenda, handler adalah sebuah event emiter yang mentransfer nilai untuk stream . Dan stream pada gilirannya, adalah objek yang dapat diamati yang meneruskan nilai ke pelanggan.


Kami akan menautkan stream dan prop$ untuk mendapatkan nilai saat ini dari bidang input.


Dalam kasus kami, pilihan yang baik adalah dengan menggunakan fungsi combineLatest .


Masalah telur dan ayam


Untuk menggunakan combineLatest , stream dan prop$ harus menghasilkan nilai. Tetapi stream tidak akan melepaskan apa pun sampai beberapa nilai melepaskan prop$ dan sebaliknya.


Anda dapat memperbaiki ini dengan mengatur stream nilai awal.


Impor pernyataan startWith dari RxJS:


 import { map, startWith } from 'rxjs/operators'; 

Buat variabel baru untuk mendapatkan nilai dari stream diperbarui:


 // App component const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value) startWith('') ); 

Kami tahu bahwa stream akan membuang peristiwa ketika bidang input berubah, jadi mari segera terjemahkan ke dalam teks.


Dan karena nilai default untuk bidang input adalah string kosong, inisialisasi value$ objek dengan value$ ''


Merajut bersama


Sekarang kita siap untuk menghubungkan kedua aliran. Impor combineLatest sebagai metode menciptakan objek yang dapat combineLatest , bukan sebagai operator .


 import { combineLatest } from 'rxjs'; 

Anda juga dapat mengimpor pernyataan tap untuk memeriksa nilai input.


 import { map, startWith, tap } from 'rxjs/operators'; 

Gunakan seperti ini:


 const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value), startWith('') ); return combineLatest(prop$, value$).pipe( tap(console.warn), // <---      map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

Sekarang, jika Anda mulai mengetik sesuatu di bidang input kami, nilai [props, value] akan muncul di konsol.



Komponen Pengguna


Komponen ini akan bertanggung jawab untuk menampilkan pengguna yang namanya akan kami transfer kepadanya. Ini akan menerima value dari komponen App dan menerjemahkannya ke dalam permintaan AJAX.


Jsx / css


Semua ini didasarkan pada proyek Kartu GitHub yang luar biasa. Sebagian besar kode, terutama gaya, disalin atau disesuaikan.


Buat folder src/User . Buat file User.css di dalamnya dan salin kode ini ke dalamnya.


Dan salin kode ini ke file src/User/Component.js .


Komponen ini hanya mengisi templat dengan data dari panggilan ke API GitHub.


Wadah


Sekarang komponen ini "bodoh" dan kita tidak di jalan, mari kita buat komponen "pintar".


Berikut ini adalah src/User/index.js


 import React from 'react'; import { componentFromStream } from 'recompose'; import { debounceTime, filter, map, pluck } from 'rxjs/operators'; import Component from './Component'; import './User.css'; const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(user => ( <h3>{user}</h3> )) ); return getUser$; }); export default User; 

Kami mendefinisikan User sebagai componentFromStream , yang mengembalikan objek prop$ dapat prop$ yang mengubah properti yang masuk ke <h3> .


debounceTime


User kami akan menerima nilai baru setiap kali tombol ditekan pada keyboard, tetapi kami tidak memerlukan perilaku ini.


Saat pengguna mulai mengetik, debounceTime(1000) akan melewati semua acara yang berlangsung kurang dari satu detik.


memetik


Kami berharap objek user dilewatkan sebagai props.user . Operator pemetik mengambil bidang yang ditentukan dari objek dan mengembalikan nilainya.


filter


Di sini kami memastikan bahwa user lulus dan bukan string kosong.


peta


Kami membuat tag <h3> dari user .


Terhubung


Kembali ke src/index.js dan impor komponen User :


 import User from './User'; 

Kami meneruskan nilai value sebagai parameter user :


  return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( <div> <input onChange={handler} placeholder="GitHub username" /> <User user={value} /> </div> )) ); 

Sekarang nilai kami ditampilkan dengan penundaan satu detik.



Lumayan, sekarang kita perlu mendapat informasi tentang pengguna.


Permintaan data


GitHub menyediakan API untuk mendapatkan informasi pengguna: https://api.github.com/users/${user} . Kita dapat dengan mudah menulis fungsi bantu:


 const formatUrl = user => `https://api.github.com/users/${user}`; 

Dan sekarang kita dapat menambahkan map(formatUrl) setelah filter :


 const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), // <--   map(user => ( <h3>{user}</h3> )) ); 

Dan sekarang, alih-alih nama pengguna, layar menampilkan URL.


Kami perlu membuat permintaan! switchMap dan ajax datang untuk switchMap .


switchMap


Operator ini sangat ideal untuk beralih di antara beberapa yang dapat diamati.


Katakanlah pengguna mengetik nama, dan kami akan membuat permintaan di dalam switchMap .


Apa yang terjadi jika pengguna memasukkan sesuatu sebelum respons dari API tiba? Haruskah kita khawatir dengan permintaan sebelumnya?


Tidak.


switchMap akan membatalkan permintaan lama dan beralih ke yang baru.


ajax


RxJS menyediakan implementasi ajax sendiri yang bekerja sangat baik dengan switchMap !


Coba


Kami mengimpor kedua operator. Kode saya terlihat seperti ini:


 import { ajax } from 'rxjs/ajax'; import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

Dan gunakan seperti ini:


 const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), switchMap(url => ajax(url).pipe( pluck('response'), map(Component) ) ) ); return getUser$; }); 

switchMap beralih dari bidang input kami ke permintaan AJAX. Ketika sebuah jawaban datang, jawaban itu diteruskan ke komponen bodoh kami.


Dan inilah hasilnya!



Menangani kesalahan


Coba masukkan nama pengguna yang tidak ada.



Aplikasi kami rusak.


catchError


Dengan operator catchError kami dapat menampilkan respons yang waras alih-alih diam-diam.


Kami mengimpor:


 import { catchError, debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

Dan masukkan di akhir permintaan AJAX kami:


 switchMap(url => ajax(url).pipe( pluck('response'), map(Component), catchError(({ response }) => alert(response.message)) ) ) 


Sudah tidak buruk, tapi tentu saja Anda bisa melakukan yang lebih baik.


Kesalahan Komponen


Buat file src/Error/index.js dengan konten:


 import React from 'react'; const Error = ({ response, status }) => ( <div className="error"> <h2>Oops!</h2> <b> {status}: {response.message} </b> <p>Please try searching again.</p> </div> ); export default Error; 

Ini akan menampilkan response dan status permintaan AJAX kami dengan baik.


Kami mengimpornya ke User/index.js , dan pada saat yang sama operator dari RxJS:


 import Error from '../Error'; import { of } from 'rxjs'; 

Ingatlah bahwa fungsi yang diteruskan ke componentFromStream harus kembali dapat diamati. Kita dapat mencapai ini menggunakan operator:



 ajax(url).pipe( pluck('response'), map(Component), catchError(error => of(<Error {...error} />)) ) 

Sekarang UI kami terlihat jauh lebih baik:



Memuat indikator


Sudah waktunya untuk memperkenalkan manajemen negara. Bagaimana lagi Anda bisa menerapkan indikator memuat?


Bagaimana jika tempat setState kita akan menggunakan BehaviorSubject ?


Komposisi ulang dokumentasi menyarankan hal-hal berikut:


Alih-alih setState (), gabungkan beberapa utas

Oke, Anda perlu dua impor baru:


 import { BehaviorSubject, merge, of } from 'rxjs'; 

BehaviorSubject akan berisi status unduhan, dan merge akan mengaitkannya dengan komponen.


Di dalam componentFromStream :


 const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ... 

BehaviorSubject diinisialisasi dengan nilai awal, atau "status." Karena kami tidak melakukan apa-apa sampai pengguna mulai memasukkan teks, inisialisasi ke false .


Kami akan mengubah status loading$ menggunakan operator tap :


 import { catchError, debounceTime, filter, map, pluck, switchMap, tap // <--- } from 'rxjs/operators'; 

Kami akan menggunakannya seperti ini:


 const loading$ = new BehaviorSubject(false); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), tap(() => loading$.next(true)), // <--- switchMap(url => ajax(url).pipe( pluck('response'), map(Component), tap(() => loading$.next(false)), // <--- catchError(error => of(<Error {...error} />)) ) ) ); 

Tepat sebelum permintaan switchMap dan AJAX, kami memberikan true ke nilai loading$ , dan false setelah respon yang berhasil.


Dan sekarang kita hanya menghubungkan loading$ dan getUser$ .


 return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) ); 

Sebelum kita melihat pekerjaannya, kita dapat mengimpor pernyataan delay agar transisinya tidak terlalu cepat.


 import { catchError, debounceTime, delay, filter, map, pluck, switchMap, tap } from 'rxjs/operators'; 

Tambahkan delay sebelum map(Component) :


 ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) ) 

Hasil?



Semua :)

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


All Articles