Bagaimana cara menampilkan indikator pemuatan di aplikasi React Redux saat mengambil data? [Tutup]


107

Saya baru mengenal React / Redux. Saya menggunakan middleware fetch api di aplikasi Redux untuk memproses API. Ini ( redux-api-middleware ). Saya pikir ini adalah cara yang baik untuk memproses tindakan api asinkron. Tetapi saya menemukan beberapa kasus yang tidak dapat diselesaikan sendiri.

Seperti yang dikatakan beranda ( Lifecycle ), siklus hidup API pengambilan dimulai dengan pengiriman tindakan CALL_API diakhiri dengan pengiriman tindakan FSA.

Jadi kasus pertama saya menunjukkan / menyembunyikan prapemuat saat mengambil API. Middleware akan mengirimkan tindakan FSA di awal dan mengirimkan tindakan FSA di akhir. Kedua tindakan tersebut diterima oleh pereduksi yang seharusnya hanya melakukan beberapa pemrosesan data normal. Tidak ada operasi UI, tidak ada lagi operasi. Mungkin saya harus menyimpan status pemrosesan dalam keadaan lalu merendernya saat menyimpan pembaruan.

Tapi bagaimana melakukan ini? Komponen reaksi mengalir di seluruh halaman? apa yang terjadi dengan pembaruan toko dari tindakan lain? Maksud saya, mereka lebih seperti peristiwa daripada negara!

Bahkan kasus yang lebih buruk, apa yang harus saya lakukan ketika saya harus menggunakan dialog konfirmasi asli atau dialog peringatan di aplikasi redux / react? Di mana mereka harus ditempatkan, tindakan atau pengurang?

Semoga sukses! Berharap untuk membalas.


1
Mengembalikan hasil edit terakhir pada pertanyaan ini karena mengubah keseluruhan poin pertanyaan dan jawaban di bawah.
Gregg B

Peristiwa adalah perubahan keadaan!
企业 应用 架构 模式 大师

Lihat questrar. github.com/orar/questrar
Orar

Jawaban:


152

Maksud saya, mereka lebih seperti peristiwa daripada negara!

Saya tidak akan mengatakannya. Saya pikir indikator pemuatan adalah contoh bagus dari UI yang dengan mudah dijelaskan sebagai fungsi dari negara: dalam hal ini, dari variabel boolean. Meskipun jawaban ini benar, saya ingin memberikan beberapa kode untuk menyertainya.

Dalam asynccontoh di repo Redux , peredam memperbarui bidang yang disebutisFetching :

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: true,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: false,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

Komponen menggunakan connect()dari React Redux untuk berlangganan status toko dan mengembalikan isFetchingsebagai bagian dari nilai mapStateToProps()pengembalian sehingga tersedia di props komponen yang terhubung:

function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Terakhir, komponen menggunakan isFetchingprop dalam render()fungsi tersebut untuk merender label "Memuat ..." (yang dapat dianggap sebagai pemintal):

{isEmpty
  ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
  : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
      <Posts posts={posts} />
    </div>
}

Bahkan kasus yang lebih buruk, apa yang harus saya lakukan ketika saya harus menggunakan dialog konfirmasi asli atau dialog peringatan di aplikasi redux / react? Di mana mereka harus ditempatkan, tindakan atau pengurang?

Efek samping apa pun (dan menampilkan dialog pasti merupakan efek samping) tidak termasuk dalam pereduksi. Pikirkan pereduksi sebagai "pembangun negara" pasif. Mereka tidak benar-benar "melakukan" sesuatu.

Jika Anda ingin menunjukkan peringatan, lakukan ini dari komponen sebelum mengirimkan tindakan, atau lakukan ini dari pembuat tindakan. Pada saat suatu tindakan dikirim, sudah terlambat untuk melakukan efek samping sebagai tanggapannya.

Untuk setiap aturan, ada pengecualian. Terkadang logika efek samping Anda begitu rumit sehingga Anda benar - benar ingin memasangkannya ke jenis tindakan tertentu atau ke pereduksi tertentu. Dalam hal ini, periksa proyek lanjutan seperti Redux Saga dan Redux Loop . Lakukan ini hanya jika Anda merasa nyaman dengan vanilla Redux dan memiliki masalah nyata dengan efek samping yang tersebar yang ingin Anda buat lebih mudah dikelola.


16
Bagaimana jika saya melakukan beberapa pengambilan? Maka satu variabel tidak akan cukup.
philk

1
@philk jika Anda memiliki beberapa pengambilan, Anda dapat mengelompokkannya Promise.allmenjadi satu promise dan kemudian mengirimkan satu tindakan untuk semua pengambilan. Atau Anda harus mempertahankan banyak isFetchingvariabel di negara bagian Anda.
Sebastien Lorber

2
Harap perhatikan dengan cermat contoh yang saya tautkan. Ada lebih dari satu isFetchingbendera. Ini disetel untuk setiap set objek yang sedang diambil. Anda dapat menggunakan komposisi peredam untuk mengimplementasikannya.
Dan Abramov

3
Perhatikan bahwa jika permintaan gagal, dan RECEIVE_POSTStidak pernah terpicu, tanda pemuatan akan tetap ada kecuali Anda telah membuat semacam batas waktu untuk menampilkan error loadingpesan.
Yakobus111

2
@TomiS - Saya secara eksplisit mem-blacklist semua properti isFetching saya dari persistensi redux apa pun yang saya gunakan.
duhseekoh

22

Jawaban bagus Dan Abramov! Hanya ingin menambahkan bahwa saya melakukan kurang lebih persis seperti itu di salah satu aplikasi saya (menyimpan isFetching sebagai boolean) dan akhirnya harus menjadikannya integer (yang akhirnya membaca sebagai jumlah permintaan yang belum diselesaikan) untuk mendukung beberapa secara bersamaan permintaan.

dengan boolean:

permintaan 1 dimulai -> pemintal aktif -> permintaan 2 dimulai -> permintaan 1 berakhir -> pemintal mati -> permintaan 2 berakhir

dengan integer:

permintaan 1 dimulai -> pemintal aktif -> permintaan 2 dimulai -> permintaan 1 berakhir -> permintaan 2 berakhir -> pemintal mati

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching + 1,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching - 1,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

2
Ini wajar. Namun paling sering Anda juga ingin menyimpan beberapa data yang Anda ambil selain flag. Pada titik ini Anda perlu memiliki lebih dari satu objek dengan isFetchingbendera. Jika Anda melihat lebih dekat pada contoh yang saya tautkan, Anda akan melihat bahwa tidak ada satu objek isFetchedtetapi banyak: satu per subreddit (yang diambil dalam contoh itu).
Dan Abramov

2
oh. ya saya tidak menyadarinya. namun dalam kasus saya, saya memiliki satu entri isFetching global dalam status dan entri cache tempat data yang diambil disimpan, dan untuk tujuan saya, saya hanya benar-benar peduli bahwa beberapa aktivitas jaringan sedang terjadi, tidak masalah untuk apa
Nuno Campos

4
Ya! Itu bergantung pada apakah Anda ingin menampilkan indikator pengambilan di satu atau banyak tempat di UI. Bahkan Anda dapat menggabungkan dua pendekatan dan memiliki keduanya global fetchCounteruntuk beberapa progress bar di bagian atas layar dan beberapa spesifik isFetchingbendera untuk daftar dan halaman.
Dan Abramov

Jika saya memiliki Permintaan POST di lebih dari satu file, bagaimana saya menyetel status isFetching untuk melacak statusnya saat ini?
pengguna989988

13

Saya ingin menambahkan sesuatu. Contoh dunia nyata menggunakan bidang isFetchingdi toko untuk mewakili saat koleksi item sedang diambil. Setiap koleksi digeneralisasikan ke paginationperedam yang dapat dihubungkan ke komponen Anda untuk melacak status dan menunjukkan apakah koleksi sedang dimuat.

Kebetulan saya ingin mengambil detail untuk entitas tertentu yang tidak sesuai dengan pola penomoran halaman. Saya ingin memiliki status yang mewakili jika detail diambil dari server tetapi juga saya tidak ingin memiliki peredam hanya untuk itu.

Untuk mengatasi ini saya menambahkan peredam umum lain yang disebut fetching. Ini bekerja dengan cara yang mirip dengan peredam pagination dan tanggung jawabnya hanya untuk menonton serangkaian tindakan dan menghasilkan status baru dengan pasangan [entity, isFetching]. Itu memungkinkan connectperedam ke komponen apa pun dan untuk mengetahui apakah aplikasi saat ini mengambil data tidak hanya untuk koleksi tetapi untuk entitas tertentu.


2
Terima kasih atas jawabannya! Penanganan pemuatan item individu dan statusnya jarang dibahas!
Gilad Peleg

Ketika saya memiliki satu komponen yang bergantung pada tindakan komponen lain, jalan keluar yang cepat dan kotor adalah di mapStateToProps Anda menggabungkannya seperti ini: isFetching: posts.isFetching || comments.isFetching - sekarang Anda dapat memblokir interaksi pengguna untuk kedua komponen saat salah satu komponen sedang diperbarui.
Philip Murphy

5

Saya belum pernah mendengar pertanyaan ini sampai sekarang, tetapi karena tidak ada jawaban yang diterima, saya akan angkat topi. Saya menulis alat untuk pekerjaan ini: react-loader-factory . Ada lebih banyak hal yang terjadi daripada solusi Abramov, tetapi lebih modular dan nyaman, karena saya tidak ingin berpikir setelah saya menulisnya.

Ada empat bagian besar:

  • Pola pabrik: Ini memungkinkan Anda dengan cepat memanggil fungsi yang sama untuk mengatur status mana yang berarti "Memuat" untuk komponen Anda, dan tindakan mana yang akan dikirim. (Ini mengasumsikan bahwa komponen bertanggung jawab untuk memulai tindakan yang ditunggunya.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Pembungkus: Komponen yang diproduksi Pabrik adalah "komponen tingkat tinggi" (seperti yang connect()dikembalikan di Redux), sehingga Anda bisa langsung memasangnya ke barang yang sudah ada.const LoadingChild = loaderWrapper(ChildComponent);
  • Interaksi Tindakan / Peredam: Wrapper memeriksa untuk melihat apakah peredam yang dicolokkan berisi kata kunci yang memberi tahu agar tidak melewati komponen yang membutuhkan data. Tindakan yang dikirim oleh pembungkus diharapkan menghasilkan kata kunci terkait (cara redux-api-middleware mengirimkan ACTION_SUCCESSdan ACTION_REQUEST, misalnya). (Anda dapat mengirimkan tindakan di tempat lain dan hanya memantau dari bungkusnya jika Anda mau, tentu saja.)
  • Throbber: Komponen yang ingin Anda munculkan saat data bergantung pada komponen Anda belum siap. Saya menambahkan div kecil di sana sehingga Anda dapat mengujinya tanpa harus memasangnya.

Modul itu sendiri tidak bergantung pada redux-api-middleware, tetapi itulah yang saya gunakan dengannya, jadi inilah beberapa contoh kode dari README:

Komponen dengan Loader yang membungkusnya:

import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory';
import ChildComponent from './ChildComponent';

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);

const LoadingChild = loaderWrapper(ChildComponent);

const containingComponent = props => {
  // Do whatever you need to do with your usual containing component 

  const childProps = { someProps: 'props' };

  return <LoadingChild { ...childProps } />;
}

Peredam untuk Loader untuk memonitor (meskipun Anda dapat menghubungkannya secara berbeda jika Anda mau):

export function activeRequests(state = [], action) {
  const newState = state.slice();

  // regex that tests for an API action string ending with _REQUEST 
  const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
  // regex that tests for a API action string ending with _SUCCESS 
  const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);

  // if a _REQUEST comes in, add it to the activeRequests list 
  if (reqReg.test(action.type)) {
    newState.push(action.type);
  }

  // if a _SUCCESS comes in, delete its corresponding _REQUEST 
  if (sucReg.test(action.type)) {
    const reqType = action.type.split('_')[0].concat('_REQUEST');
    const deleteInd = state.indexOf(reqType);

    if (deleteInd !== -1) {
      newState.splice(deleteInd, 1);
    }
  }

  return newState;
}

Saya berharap dalam waktu dekat saya akan menambahkan hal-hal seperti batas waktu dan kesalahan ke modul, tetapi polanya tidak akan jauh berbeda.


Jawaban singkat untuk pertanyaan Anda adalah:

  1. Ikat rendering ke kode rendering - gunakan pembungkus di sekitar komponen yang perlu Anda render dengan data seperti yang saya tunjukkan di atas.
  2. Tambahkan peredam yang membuat status permintaan di sekitar aplikasi yang mungkin Anda pedulikan mudah dicerna, sehingga Anda tidak perlu terlalu memikirkan apa yang terjadi.
  3. Peristiwa dan keadaan tidak terlalu berbeda.
  4. Bagi saya, intuisi Anda lainnya tampaknya benar.

4

Apakah saya satu-satunya yang berpikir bahwa indikator pemuatan tidak termasuk dalam toko Redux? Maksud saya, menurut saya itu bukan bagian dari status aplikasi itu sendiri ..

Sekarang, saya bekerja dengan Angular2, dan yang saya lakukan adalah saya memiliki layanan "Memuat" yang memperlihatkan indikator pemuatan yang berbeda melalui RxJS BehaviourSubjects .. Saya kira mekanismenya sama, saya hanya tidak menyimpan informasi di Redux.

Pengguna LoadingService hanya berlangganan ke acara yang ingin mereka dengarkan ..

Pencipta tindakan Redux saya memanggil LoadingService setiap kali ada hal yang perlu diubah. Komponen UX berlangganan observable yang diekspos ...


inilah mengapa saya menyukai ide penyimpanan, di mana semua tindakan dapat disurvei (ngrx dan redux-logic), layanan tidak berfungsi, redux-logika - fungsional. Bacaan bagus
srghma

20
Hai, memeriksa kembali lebih dari setahun setelahnya, hanya untuk mengatakan bahwa saya sangat salah. Tentu saja status UX termasuk dalam status aplikasi. Betapa bodohnya aku?
Spock

3

Anda dapat menambahkan pendengar perubahan ke toko Anda, baik menggunakan connect()dari React Redux atau store.subscribe()metode tingkat rendah . Anda harus memiliki indikator pemuatan di toko Anda, yang kemudian dapat diperiksa oleh penangan perubahan toko dan memperbarui status komponen. Komponen kemudian merender prapemuat jika diperlukan, berdasarkan status.

alertdan confirmseharusnya tidak menjadi masalah. Mereka memblokir dan memperingatkan bahkan tidak menerima masukan apa pun dari pengguna. Dengan confirm, Anda dapat menyetel status berdasarkan apa yang diklik pengguna jika pilihan pengguna memengaruhi rendering komponen. Jika tidak, Anda dapat menyimpan pilihan sebagai variabel anggota komponen untuk digunakan nanti.


tentang kode peringatan / konfirmasi, di mana harus diletakkan, tindakan atau pengurang?
企业 应用 架构 模式 大师

Bergantung pada apa yang ingin Anda lakukan dengannya, tetapi sejujurnya saya akan menempatkannya dalam kode komponen dalam banyak kasus karena mereka adalah bagian dari UI, bukan lapisan data.
Miloš Rašić

beberapa komponen UI bertindak dengan memicu peristiwa (peristiwa perubahan status) alih-alih status itu sendiri. Seperti animasi, menampilkan / menyembunyikan prapemuat. Bagaimana Anda memprosesnya?
企业 应用 架构 模式 大师

Jika Anda ingin menggunakan komponen non-react dalam aplikasi react Anda, solusi yang umum digunakan adalah membuat komponen wrapper react, kemudian gunakan metode siklus hidupnya untuk menginisialisasi, memperbarui, dan menghancurkan sebuah instance dari komponen non-react. Sebagian besar komponen tersebut menggunakan elemen placeholder di DOM untuk menginisialisasi, dan Anda akan merendernya di metode render komponen react. Anda dapat membaca lebih lanjut tentang metode siklus hidup di sini: facebook.github.io/react/docs/component-specs.html
Miloš Rašić

Saya punya kasus: area notifikasi di pojok kanan atas, yang berisi satu pesan notifikasi, setiap pesan muncul kemudian menghilang setelah 5 detik. Komponen ini di luar tampilan web, disediakan oleh aplikasi asli host. Ini menyediakan beberapa antarmuka js seperti addNofication(message). Kasus lainnya adalah prapemuat yang juga disediakan oleh aplikasi asli host dan dipicu oleh API javascriptnya. Saya menambahkan pembungkus untuk api tersebut, di componentDidUpdatedalam komponen React. Bagaimana cara mendesain props atau status komponen ini?
企业 应用 架构 模式 大师

3

Kami memiliki tiga jenis notifikasi di aplikasi kami, yang semuanya dirancang sebagai aspek:

  1. Memuat indikator (modal atau non-modal berdasarkan prop)
  2. Kesalahan Popup (modal)
  3. Snackbar notifikasi (non-modal, menutup sendiri)

Ketiganya berada di level teratas aplikasi kami (Utama), dan dihubungkan melalui Redux seperti yang ditunjukkan pada cuplikan kode di bawah ini. Alat peraga ini mengontrol tampilan dari aspek yang sesuai.

Saya merancang proxy yang menangani semua panggilan API kami, sehingga semua kesalahan isFetching dan (api) dimediasi dengan actionCreators yang saya impor di proxy. (Selain itu, saya juga menggunakan webpack untuk menyuntikkan tiruan layanan dukungan untuk dev sehingga kami dapat bekerja tanpa ketergantungan server.)

Tempat lain dalam aplikasi yang perlu menyediakan jenis notifikasi apa pun hanya mengimpor tindakan yang sesuai. Snackbar & Error memiliki parameter untuk menampilkan pesan.

@connect(
// map state to props
state => ({
    isFetching      :state.main.get('isFetching'),   // ProgressIndicator
    notification    :state.main.get('notification'), // Snackbar
    error           :state.main.get('error')         // ErrorPopup
}),
// mapDispatchToProps
(dispatch) => { return {
    actions: bindActionCreators(actionCreators, dispatch)
}}

) ekspor kelas default Main extends React.Component {


Saya sedang mengerjakan pengaturan serupa dengan menampilkan loader / notifikasi. Saya mengalami masalah; maukah Anda memiliki inti atau contoh bagaimana Anda menyelesaikan tugas-tugas ini?
Aymen

2

Saya menyimpan url seperti ::

isFetching: {
    /api/posts/1: true,
    api/posts/3: false,
    api/search?q=322: true,
}

Dan kemudian saya memiliki pemilih yang dihafal (melalui pemilihan ulang).

const getIsFetching = createSelector(
    state => state.isFetching,
    items => items => Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false
);

Untuk membuat url unik dalam kasus POST, saya meneruskan beberapa variabel sebagai kueri.

Dan di mana saya ingin menunjukkan indikator, saya cukup menggunakan variabel getFetchCount


1
Anda bisa mengganti Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsedengan Object.keys(items).every(item => items[item])cara.
Alexandre Annic

1
Saya pikir Anda bermaksud somebukan every, tapi ya, terlalu banyak perbandingan yang tidak diperlukan dalam solusi yang diusulkan pertama. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.