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