
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),
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),
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
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 :)