Mengapa kita membutuhkan middleware untuk aliran async di Redux?

684

Menurut dokumen, "Tanpa middleware, Redux store hanya mendukung aliran data yang sinkron" . Saya tidak mengerti mengapa ini terjadi. Mengapa komponen kontainer tidak dapat memanggil API async, dan kemudian dispatchtindakannya?

Misalnya, bayangkan UI sederhana: bidang dan tombol. Saat pengguna menekan tombol, bidang akan diisi dengan data dari server jarak jauh.

Bidang dan tombol

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

Ketika komponen yang diekspor dirender, saya dapat mengklik tombol dan input diperbarui dengan benar.

Perhatikan updatefungsi dalam connectpanggilan. Ini mengirimkan tindakan yang memberi tahu App bahwa ia memperbarui, dan kemudian melakukan panggilan async. Setelah panggilan selesai, nilai yang diberikan dikirim sebagai muatan tindakan lain.

Apa yang salah dengan pendekatan ini? Mengapa saya ingin menggunakan Redux Thunk atau Redux Promise, seperti yang ditunjukkan oleh dokumentasi?

EDIT: Saya mencari repo Redux untuk mencari petunjuk, dan menemukan bahwa Pencipta Tindakan diharuskan untuk menjadi fungsi murni di masa lalu. Misalnya, inilah pengguna yang mencoba memberikan penjelasan yang lebih baik untuk aliran data async:

Pembuat tindakan itu sendiri masih merupakan fungsi murni, tetapi fungsi pukulan yang dikembalikan tidak perlu, dan dapat melakukan panggilan async kami

Pembuat aksi tidak lagi harus murni. Jadi, thidd / janji middleware pasti diperlukan di masa lalu, tetapi tampaknya ini tidak lagi terjadi?

sbichenko
sumber
53
Pembuat tindakan tidak pernah dituntut untuk menjadi fungsi murni. Itu adalah kesalahan dalam dokumen, bukan keputusan yang berubah.
Dan Abramov
1
@DanAbramov untuk testabilitas mungkin merupakan praktik yang baik. Redux-saga mengizinkan ini: stackoverflow.com/a/34623840/82609
Sebastien Lorber

Jawaban:

699

Apa yang salah dengan pendekatan ini? Mengapa saya ingin menggunakan Redux Thunk atau Redux Promise, seperti yang ditunjukkan oleh dokumentasi?

Tidak ada yang salah dengan pendekatan ini. Itu hanya merepotkan dalam aplikasi besar karena Anda akan memiliki komponen yang berbeda melakukan tindakan yang sama, Anda mungkin ingin mendebo beberapa tindakan, atau menjaga beberapa negara bagian seperti ID penambahan otomatis dekat dengan pembuat tindakan, dll. Jadi hanya lebih mudah dari sudut pandang pemeliharaan untuk mengekstrak pembuat tindakan ke dalam fungsi yang terpisah.

Anda dapat membaca jawaban saya untuk "Cara mengirim tindakan Redux dengan batas waktu" untuk penelusuran lebih rinci.

Middleware seperti Redux Thunk atau Redux Promise hanya memberi Anda "gula sintaksis" untuk mengirimkan barang atau janji, tetapi Anda tidak harus menggunakannya.

Jadi, tanpa middleware apa pun, pembuat tindakan Anda mungkin terlihat seperti

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

Tetapi dengan Thunk Middleware Anda dapat menulis seperti ini:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

Jadi tidak ada perbedaan besar. Satu hal yang saya sukai dari pendekatan yang terakhir adalah bahwa komponen tidak peduli bahwa pembuat tindakan async. Itu hanya panggilan dispatchbiasa, itu juga dapat digunakan mapDispatchToPropsuntuk mengikat pembuat tindakan seperti itu dengan sintaks pendek, dll. Komponen tidak tahu bagaimana pembuat tindakan diimplementasikan, dan Anda dapat beralih di antara pendekatan async yang berbeda (Redux Thunk, Redux Promise, Redux Saga ) tanpa mengubah komponen. Di sisi lain, dengan pendekatan sebelumnya, eksplisit, komponen Anda tahu persis bahwa panggilan tertentu adalah async, dan perlu dispatchdilewati oleh beberapa konvensi (misalnya, sebagai parameter sinkronisasi).

Pikirkan juga bagaimana kode ini akan berubah. Katakanlah kita ingin memiliki fungsi pemuatan data kedua, dan untuk menggabungkannya dalam satu pembuat tindakan.

Dengan pendekatan pertama, kita perlu memperhatikan tindakan seperti apa yang kita panggil:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

Dengan pembuat tindakan Redux Thunk dapat dispatchmerupakan hasil dari pembuat tindakan lain dan bahkan tidak berpikir apakah itu sinkron atau tidak sinkron:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

Dengan pendekatan ini, jika nanti Anda ingin pembuat tindakan melihat keadaan Redux saat ini, Anda bisa menggunakan getStateargumen kedua yang diteruskan ke thunks tanpa mengubah kode panggilan sama sekali:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

Jika Anda perlu mengubahnya agar sinkron, Anda juga dapat melakukan ini tanpa mengubah kode panggilan apa pun:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

Jadi manfaat menggunakan middleware seperti Redux Thunk atau Redux Promise adalah bahwa komponen tidak menyadari bagaimana pembuat tindakan diimplementasikan, dan apakah mereka peduli dengan kondisi Redux, apakah mereka sinkron atau asinkron, dan apakah mereka memanggil pembuat tindakan lain atau tidak. . Kelemahannya adalah sedikit tipuan, tapi kami yakin ini layak dilakukan dalam aplikasi nyata.

Akhirnya, Redux Thunk dan teman-teman hanyalah salah satu pendekatan yang mungkin untuk permintaan asinkron di aplikasi Redux. Pendekatan lain yang menarik adalah Redux Saga yang memungkinkan Anda mendefinisikan daemon yang sudah berjalan lama (“sagas”) yang mengambil tindakan saat datang, dan mengubah atau melakukan permintaan sebelum menghasilkan tindakan. Ini memindahkan logika dari pencipta aksi ke kisah-kisah. Anda mungkin ingin memeriksanya, dan kemudian memilih yang paling cocok untuk Anda.

Saya mencari repo Redux untuk mencari petunjuk, dan menemukan bahwa Pencipta Tindakan diharuskan untuk menjadi fungsi murni di masa lalu.

Ini salah. Dokter mengatakan ini, tetapi dokter itu salah.
Pembuat tindakan tidak pernah dituntut untuk menjadi fungsi murni.
Kami memperbaiki dokumen untuk mencerminkan hal itu.

Dan Abramov
sumber
57
Mungkin cara singkat untuk mengatakan pemikiran Dan adalah: middleware adalah pendekatan terpusat, cara ini memungkinkan Anda untuk menjaga komponen Anda lebih sederhana dan umum dan mengontrol aliran data di satu tempat. Jika Anda memelihara aplikasi besar, Anda akan menikmatinya =)
Sergey Lapin
3
@asdfasdfads Saya tidak mengerti mengapa itu tidak berhasil. Itu akan bekerja persis sama; dimasukkan alertsetelah dispatch()tindakan.
Dan Abramov
9
Baris kedua dari belakang di contoh kode pertama Anda: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Mengapa saya harus mengirimkannya? Jika dengan konvensi hanya ada satu toko global saja, mengapa saya tidak melakukan rujukan langsung dan melakukannya store.dispatchkapan saja saya perlu, misalnya, di loadData?
Søren Debois
10
@ SørenDebois Jika aplikasi Anda hanya dari sisi klien, itu akan berfungsi. Jika itu diberikan di server, Anda ingin memiliki storecontoh berbeda untuk setiap permintaan sehingga Anda tidak dapat mendefinisikannya sebelumnya.
Dan Abramov
3
Hanya ingin menunjukkan bahwa jawaban ini memiliki 139 baris yang 9,92 kali lebih banyak daripada kode sumber redux-thunk yang terdiri dari 14 baris: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy
447

Kamu tidak.

Tapi ... Anda harus menggunakan redux-saga :)

Jawaban Dan Abramov benar tentang redux-thunktetapi saya akan berbicara sedikit tentang redux-saga yang sangat mirip tetapi lebih kuat.

Imperatif VS deklaratif

  • DOM : jQuery adalah keharusan / Bereaksi adalah deklaratif
  • Monads : IO sangat penting / Gratis adalah deklaratif
  • Efek redux : redux-thunksangat penting / redux-sagabersifat deklaratif

Ketika Anda memiliki pukulan di tangan Anda, seperti monad IO atau janji, Anda tidak dapat dengan mudah tahu apa yang akan dilakukan setelah Anda mengeksekusi. Satu-satunya cara untuk menguji seorang thunk adalah dengan mengeksekusinya, dan mengejek dispatcher (atau seluruh dunia luar jika berinteraksi dengan lebih banyak barang ...).

Jika Anda menggunakan tiruan, maka Anda tidak melakukan pemrograman fungsional.

Dilihat melalui lensa efek samping, ejekan adalah bendera yang kode Anda tidak murni, dan di mata programmer fungsional, bukti bahwa ada sesuatu yang salah. Alih-alih mengunduh perpustakaan untuk membantu kami memeriksa gunung es yang masih utuh, kita harus berlayar mengitarinya. Seorang pria TDD / Java hardcore pernah bertanya kepada saya bagaimana Anda mengejek Clojure. Jawabannya, kita biasanya tidak. Kami biasanya melihatnya sebagai tanda kami perlu memperbaiki kode kami.

Sumber

Kisah-kisah (ketika mereka diterapkan di redux-saga) adalah deklaratif dan seperti komponen Free monad atau Bereaksi, mereka lebih mudah untuk menguji tanpa mock.

Lihat juga artikel ini :

dalam FP modern, kita seharusnya tidak menulis program - kita harus menulis deskripsi program, yang kemudian kita dapat introspeksi, mengubah, dan menafsirkan sesuka hati.

(Sebenarnya, Redux-saga seperti hibrida: aliran sangat penting tetapi efeknya deklaratif)

Kebingungan: tindakan / acara / perintah ...

Ada banyak kebingungan di dunia frontend tentang bagaimana beberapa konsep backend seperti CQRS / EventSourcing dan Flux / Redux mungkin terkait, sebagian besar karena di Flux kita menggunakan istilah "aksi" yang kadang-kadang dapat mewakili kode imperatif ( LOAD_USER) dan peristiwa ( USER_LOADED). Saya percaya bahwa seperti event-sourcing, Anda hanya boleh mengirim acara.

Menggunakan saga dalam praktik

Bayangkan sebuah aplikasi dengan tautan ke profil pengguna. Cara idiomatis untuk menangani ini dengan setiap middleware adalah:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

Kisah ini diterjemahkan menjadi:

setiap kali nama pengguna diklik, ambil profil pengguna lalu kirim acara dengan profil yang dimuat.

Seperti yang Anda lihat, ada beberapa kelebihan redux-saga.

Penggunaan takeLatestizin untuk menyatakan bahwa Anda hanya tertarik untuk mendapatkan data nama pengguna terakhir yang diklik (menangani masalah konkurensi jika pengguna mengklik sangat cepat pada banyak nama pengguna). Hal-hal semacam ini sulit dengan thunks. Anda bisa menggunakan takeEveryjika Anda tidak ingin perilaku ini.

Anda menjaga pembuat aksi tetap murni. Perhatikan bahwa masih berguna untuk menyimpan actionCreators (dalam kisah putdan komponen dispatch), karena ini dapat membantu Anda untuk menambahkan validasi tindakan (pernyataan / aliran / naskah naskah) di masa depan.

Kode Anda menjadi jauh lebih dapat diuji karena efeknya deklaratif

Anda tidak perlu lagi memicu panggilan seperti rpc seperti actions.loadUser(). UI Anda hanya perlu mengirimkan apa yang TELAH TERJADI. Kami hanya memecat acara (selalu dalam bentuk lampau!) Dan bukan tindakan lagi. Ini berarti bahwa Anda dapat membuat "bebek" yang dipisahkan atau Konteks Terbatas dan bahwa saga dapat bertindak sebagai titik penghubung antara komponen-komponen modular ini.

Ini berarti bahwa pandangan Anda lebih mudah dikelola karena mereka tidak perlu lagi memuat lapisan terjemahan antara apa yang telah terjadi dan apa yang seharusnya terjadi sebagai efek

Misalnya bayangkan tampilan gulir yang tak terbatas. CONTAINER_SCROLLEDdapat menyebabkan NEXT_PAGE_LOADED, tetapi apakah itu benar-benar tanggung jawab wadah yang dapat digulir untuk memutuskan apakah kita harus memuat halaman lain? Kemudian dia harus mengetahui hal-hal yang lebih rumit seperti apakah halaman terakhir berhasil dimuat atau tidak, atau apakah sudah ada halaman yang mencoba memuat, atau jika tidak ada lagi item yang tersisa untuk dimuat? Saya tidak berpikir begitu: untuk dapat digunakan kembali secara maksimal, wadah yang dapat digulir harus menggambarkan bahwa itu telah digulir. Pemuatan halaman adalah "efek bisnis" dari gulungan itu

Beberapa mungkin berpendapat bahwa generator secara inheren dapat menyembunyikan keadaan di luar toko redux dengan variabel lokal, tetapi jika Anda mulai mengatur hal-hal kompleks di dalam thunks dengan memulai timer dll Anda akan memiliki masalah yang sama pula. Dan ada selectefek yang sekarang memungkinkan untuk mendapatkan beberapa status dari toko Redux Anda.

Sagas dapat dilalui dengan waktu dan juga memungkinkan pencatatan aliran yang rumit dan perangkat pengembangan yang saat ini sedang dikerjakan. Berikut adalah beberapa pendataan alur async sederhana yang sudah diterapkan:

saga aliran logging

Decoupling

Sagas tidak hanya menggantikan redux thunks. Mereka datang dari backend / sistem terdistribusi / event-sourcing.

Ini adalah kesalahpahaman yang sangat umum bahwa kisah-kisah hanya ada di sini untuk menggantikan redux thunks Anda dengan testability yang lebih baik. Sebenarnya ini hanyalah detail implementasi dari redux-saga. Menggunakan efek deklaratif lebih baik daripada thunks untuk testability, tetapi pola saga dapat diimplementasikan di atas kode imperatif atau deklaratif.

Pertama-tama, saga adalah bagian dari perangkat lunak yang memungkinkan untuk mengoordinasikan transaksi yang telah berjalan lama (konsistensi akhirnya), dan transaksi di berbagai konteks terbatas (jargon desain berbasis domain).

Untuk menyederhanakan ini untuk dunia frontend, bayangkan ada widget1 dan widget2. Ketika beberapa tombol pada widget1 diklik, maka itu akan memiliki efek pada widget2. Alih-alih menggabungkan 2 widget bersama-sama (yaitu widget1 mengirimkan tindakan yang menargetkan widget2), widget1 hanya mengirimkan bahwa tombolnya diklik. Kemudian hikayat mendengarkan tombol ini klik dan kemudian perbarui widget2 dengan membuang acara baru yang diketahui widget2.

Ini menambah tingkat tipuan yang tidak perlu untuk aplikasi sederhana, tetapi membuatnya lebih mudah untuk skala aplikasi yang kompleks. Anda sekarang dapat mempublikasikan widget1 dan widget2 ke repositori npm yang berbeda sehingga mereka tidak perlu tahu tentang satu sama lain, tanpa harus berbagi registri global tindakan. 2 widget tersebut sekarang dibatasi konteks yang dapat hidup secara terpisah. Mereka tidak membutuhkan satu sama lain untuk konsisten dan dapat digunakan kembali di aplikasi lain juga. Saga adalah titik penghubung antara dua widget yang mengoordinasikannya dengan cara yang berarti untuk bisnis Anda.

Beberapa artikel bagus tentang cara membuat struktur aplikasi Redux Anda, di mana Anda dapat menggunakan Redux-saga untuk alasan decoupling:

Sebuah usecase konkret: sistem notifikasi

Saya ingin komponen saya dapat memicu tampilan notifikasi dalam aplikasi. Tetapi saya tidak ingin komponen saya sangat digabungkan ke sistem pemberitahuan yang memiliki aturan bisnis sendiri (maks. 3 pemberitahuan ditampilkan pada saat yang sama, antrian pemberitahuan, waktu tampilan 4 detik dll ...).

Saya tidak ingin komponen JSX saya memutuskan kapan pemberitahuan akan ditampilkan / disembunyikan. Saya hanya memberikannya kemampuan untuk meminta pemberitahuan, dan meninggalkan aturan yang rumit di dalam saga. Hal-hal semacam ini cukup sulit diimplementasikan dengan pukulan atau janji.

pemberitahuan

Saya telah menjelaskan di sini bagaimana ini bisa dilakukan dengan saga

Mengapa itu disebut Saga?

Istilah saga berasal dari dunia backend. Saya awalnya memperkenalkan Yassine (penulis Redux-saga) untuk istilah itu dalam diskusi panjang .

Awalnya, istilah itu diperkenalkan dengan kertas , pola saga seharusnya digunakan untuk menangani konsistensi akhirnya dalam transaksi terdistribusi, tetapi penggunaannya telah diperluas ke definisi yang lebih luas oleh pengembang backend sehingga sekarang juga mencakup "manajer proses" pola (entah bagaimana pola kisah asli adalah bentuk khusus dari manajer proses).

Saat ini, istilah "saga" membingungkan karena dapat menggambarkan 2 hal yang berbeda. Seperti yang digunakan dalam redux-saga, itu tidak menjelaskan cara untuk menangani transaksi terdistribusi tetapi cara untuk mengoordinasikan tindakan di aplikasi Anda. redux-sagabisa juga dipanggil redux-process-manager.

Lihat juga:

Alternatif

Jika Anda tidak menyukai gagasan menggunakan generator tetapi Anda tertarik dengan pola saga dan sifat decouplingnya, Anda juga dapat mencapai hal yang sama dengan redux-observable yang menggunakan nama epicuntuk menggambarkan pola yang sama persis, tetapi dengan RxJS. Jika Anda sudah terbiasa dengan Rx, Anda akan merasa seperti di rumah.

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

Beberapa sumber daya yang berguna redux-saga

2017 menyarankan

  • Jangan terlalu sering menggunakan Redux-saga hanya untuk menggunakannya. Hanya panggilan API yang dapat diuji tidak layak.
  • Jangan hapus thunks dari proyek Anda untuk sebagian besar kasus sederhana.
  • Jangan ragu untuk mengirimkan barang yield put(someActionThunk)jika itu masuk akal.

Jika Anda takut menggunakan Redux-saga (atau Redux-observable) tetapi hanya perlu pola decoupling, periksa redux-dispatch-subscribe : itu memungkinkan untuk mendengarkan pengiriman dan memicu pengiriman baru di pendengar.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
Sebastien Lorber
sumber
64
Ini semakin baik setiap kali saya mengunjungi kembali. Pertimbangkan mengubahnya menjadi posting blog :).
RainerAtSpirit
4
Terima kasih atas artikelnya yang bagus. Namun saya tidak setuju pada aspek-aspek tertentu. Bagaimana LOAD_USER penting? Bagi saya, ini tidak hanya bersifat deklaratif - tetapi juga memberikan kode yang dapat dibaca. Seperti misalnya. "Ketika saya menekan tombol ini saya ingin ADD_ITEM". Saya dapat melihat kode dan mengerti persis apa yang terjadi. Jika itu disebut sesuatu dengan efek "BUTTON_CLICK", saya harus mencarinya.
bersumpah
4
Jawaban bagus. Ada alternatif lain sekarang: github.com/blesh/redux-observable
swennemen
4
@saya mohon maaf atas anwer yang terlambat. Ketika Anda mengirim ADD_ITEM, itu penting karena Anda mengirim tindakan yang bertujuan untuk memiliki efek pada toko Anda: Anda mengharapkan tindakan untuk melakukan sesuatu. Menjadi deklaratif merangkul filosofi pencarian-sumber acara: Anda tidak mengirim tindakan untuk memicu perubahan pada aplikasi Anda, tetapi Anda mengirim peristiwa di masa lalu untuk menggambarkan apa yang terjadi pada aplikasi Anda. Pengiriman acara harus cukup untuk mempertimbangkan bahwa keadaan aplikasi telah berubah. Fakta bahwa ada toko Redux yang bereaksi terhadap peristiwa adalah detail implementasi opsional
Sebastien Lorber
3
Saya tidak suka jawaban ini karena mengalihkan perhatian dari pertanyaan aktual untuk memasarkan perpustakaan seseorang. Jawaban ini memberikan perbandingan dari dua perpustakaan, yang bukan maksud dari pertanyaan. Pertanyaan sebenarnya adalah menanyakan apakah akan menggunakan middleware sama sekali, yang dijelaskan oleh jawaban yang diterima.
Abhinav Manchanda
31

Jawaban singkatnya : sepertinya pendekatan yang benar-benar masuk akal untuk masalah asinkroni bagi saya. Dengan beberapa peringatan.

Saya memiliki garis pemikiran yang sangat mirip ketika mengerjakan proyek baru yang baru kami mulai di pekerjaan saya. Saya adalah penggemar berat sistem elegan vanilla Redux untuk memperbarui toko dan komponen rerendering dengan cara yang tetap keluar dari nyali pohon komponen Bereaksi. Rasanya aneh bagi saya untuk menghubungkan ke dispatchmekanisme elegan untuk menangani asinkron.

Saya akhirnya pergi dengan pendekatan yang sangat mirip dengan apa yang Anda miliki di perpustakaan saya faktor dari proyek kami, yang kami sebut react-redux-controller .

Saya akhirnya tidak akan pergi dengan pendekatan tepat yang Anda miliki di atas karena beberapa alasan:

  1. Cara Anda menulisnya, fungsi-fungsi pengiriman tidak memiliki akses ke toko. Anda dapat menyiasatinya dengan meminta komponen UI Anda memasukkan semua info yang dibutuhkan fungsi pengiriman. Tapi saya berpendapat bahwa ini memasangkan komponen-komponen UI ke logika pengiriman tidak perlu. Dan yang lebih problematis, tidak ada cara yang jelas untuk fungsi pengiriman untuk mengakses status yang diperbarui dalam kelanjutan async.
  2. Fungsi pengiriman memiliki akses ke dispatchdirinya sendiri melalui ruang lingkup leksikal. Ini membatasi opsi untuk refactoring setelah connectpernyataan itu lepas kendali - dan itu terlihat sangat sulit hanya dengan satu updatemetode itu. Jadi, Anda memerlukan beberapa sistem untuk memungkinkan Anda menyusun fungsi-fungsi dispatcher tersebut jika Anda memecahnya menjadi modul-modul terpisah.

Secara bersamaan, Anda harus memasang beberapa sistem untuk memungkinkan dispatchdan toko untuk disuntikkan ke fungsi pengiriman Anda, bersama dengan parameter acara. Saya tahu tiga pendekatan yang masuk akal untuk injeksi ketergantungan ini:

  • redux-thunk melakukan ini dengan cara fungsional, dengan memasukkannya ke dalam thunks Anda (menjadikannya sama sekali bukan thunk , dengan definisi kubah). Saya belum bekerja dengan dispatchpendekatan middleware lain , tapi saya berasumsi mereka pada dasarnya sama.
  • react-redux-controller melakukan ini dengan coroutine. Sebagai bonus, ini juga memberi Anda akses ke "penyeleksi", yang merupakan fungsi yang mungkin Anda berikan sebagai argumen pertama connect, daripada harus bekerja langsung dengan toko yang mentah dan dinormalisasi.
  • Anda juga bisa melakukannya dengan cara yang berorientasi objek dengan menyuntikkan mereka ke dalam thiskonteks, melalui berbagai mekanisme yang mungkin.

Memperbarui

Terlintas dalam benak saya bahwa sebagian dari teka-teki ini adalah batasan dari reaksi-reduks . Argumen pertama yang connectmendapatkan snapshot keadaan, tetapi tidak dikirim. Argumen kedua mendapatkan pengiriman tetapi bukan negara. Tidak ada argumen yang mendapatkan pukulan yang menutup kondisi saat ini, karena dapat melihat status yang diperbarui pada saat kelanjutan / panggilan balik.

acjay
sumber
22

Tujuan Abramov - dan idealnya semua orang - hanya untuk merangkum kompleksitas (dan panggilan async) di tempat yang paling tepat .

Di mana tempat terbaik untuk melakukannya dalam aliran data Redux standar? Bagaimana tentang:

  • Reduksi ? Tidak mungkin. Mereka harus menjadi fungsi murni tanpa efek samping. Memperbarui toko adalah bisnis yang serius dan rumit. Jangan mencemari itu.
  • Komponen Tampilan Bodoh? Jelas Tidak. Mereka memiliki satu perhatian: presentasi dan interaksi pengguna, dan harus sesederhana mungkin.
  • Komponen Kontainer? Mungkin saja, tetapi kurang optimal. Masuk akal bahwa wadah adalah tempat di mana kami merangkum beberapa kompleksitas tampilan terkait dan berinteraksi dengan toko, tetapi:
    • Wadah memang harus lebih kompleks daripada komponen bodoh, tetapi masih merupakan tanggung jawab tunggal: menyediakan ikatan antara tampilan dan keadaan / toko. Logika async Anda sepenuhnya merupakan perhatian yang terpisah dari itu.
    • Dengan menempatkannya di sebuah wadah, Anda akan mengunci logika async Anda ke dalam satu konteks, untuk satu tampilan / rute. Ide buruk. Idealnya itu semua dapat digunakan kembali, dan benar-benar dipisahkan.
  • S ome Modul layanan lainnya? Gagasan buruk: Anda harus menyuntikkan akses ke toko, yang merupakan mimpi buruk pemeliharaan / uji coba. Lebih baik menggunakan butir Redux dan mengakses toko hanya menggunakan API / model yang disediakan.
  • Tindakan dan Middlewares yang menafsirkannya? Kenapa tidak?! Sebagai permulaan, itu satu-satunya pilihan utama yang kami miliki. :-) Lebih logis, sistem aksi dipisahkan logika eksekusi yang dapat Anda gunakan dari mana saja. Itu punya akses ke toko dan dapat mengirim lebih banyak tindakan. Ini memiliki tanggung jawab tunggal yaitu mengatur aliran kontrol dan data di sekitar aplikasi, dan sebagian besar async cocok dengan itu.
    • Bagaimana dengan Pencipta Tindakan? Mengapa tidak melakukan async saja di sana, alih-alih dalam aksi itu sendiri, dan di Middleware?
      • Pertama dan paling penting, pencipta tidak memiliki akses ke toko, seperti halnya middleware. Itu berarti Anda tidak dapat mengirim tindakan kontingen baru, tidak dapat membaca dari toko untuk menyusun async Anda, dll.
      • Jadi, pertahankan kompleksitas di tempat yang kompleks dengan kebutuhan, dan sederhanakan yang lainnya. Pencipta kemudian dapat menjadi fungsi sederhana, relatif murni yang mudah diuji.
XML
sumber
Komponen Kontainer - mengapa tidak? Karena komponen peran bermain di Bereaksi, sebuah wadah dapat bertindak sebagai kelas layanan, dan itu sudah mendapat toko melalui DI (alat peraga). Dengan menempatkannya di sebuah wadah, Anda akan mengunci logika async Anda ke dalam satu konteks, untuk satu tampilan / rute - bagaimana bisa demikian? Sebuah komponen dapat memiliki banyak instance. Itu dapat dipisahkan dari presentasi, misalnya dengan render prop. Saya kira jawabannya bisa mendapat manfaat lebih dari contoh-contoh singkat yang membuktikan maksudnya.
Estus Flask
Saya suka jawaban ini!
Mauricio Avendaño
13

Untuk menjawab pertanyaan yang ditanyakan di awal:

Mengapa komponen kontainer tidak dapat memanggil API async, dan kemudian mengirim tindakan?

Perlu diingat bahwa dokumen itu untuk Redux, bukan Redux plus Bereaksi. Toko Redux yang terhubung dengan React components dapat melakukan apa yang Anda katakan, tetapi toko Plain Jane Redux tanpa middleware tidak menerima argumen dispatchkecuali objek biasa.

Tanpa middleware tentu saja Anda masih bisa melakukannya

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

Tapi itu kasus serupa di mana asynchrony dibungkus sekitar Redux bukan ditangani oleh Redux. Jadi, middleware memungkinkan untuk sinkronisasi dengan memodifikasi apa yang dapat diteruskan langsung ke dispatch.


Yang mengatakan, semangat saran Anda, saya pikir, valid. Pasti ada cara lain Anda bisa menangani asinkron dalam aplikasi Redux + Bereaksi.

Salah satu manfaat menggunakan middleware adalah Anda dapat terus menggunakan pembuat tindakan seperti biasa tanpa khawatir bagaimana mereka terhubung. Misalnya, menggunakan redux-thunk, kode yang Anda tulis akan sangat mirip

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

yang tidak terlihat jauh berbeda dari aslinya - hanya sedikit dikocok - dan connecttidak tahu itu updateThing(atau perlu) asinkron.

Jika Anda juga ingin mendukung janji , yang dapat diobservasi , kisah-kisah , atau kebiasaan gila dan pembuat tindakan yang sangat deklaratif , maka Redux dapat melakukannya hanya dengan mengubah apa yang Anda sampaikan dispatch(alias, apa yang Anda kembalikan dari pembuat tindakan). Tidak perlu melakukan mucking dengan komponen Bereaksi (atau connectpanggilan).

Michelle Tilley
sumber
Anda saran untuk hanya mengirim acara lain pada penyelesaian tindakan. Itu tidak akan berfungsi ketika Anda harus menunjukkan peringatan () setelah tindakan selesai Janji di dalam komponen Bereaksi bekerja dengan baik. Saat ini saya merekomendasikan pendekatan Janji.
catamphetamine
8

OK, mari kita mulai melihat bagaimana middleware bekerja terlebih dahulu, yang cukup menjawab pertanyaan, ini adalah kode sumber fungsi pplyMiddleWare di Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

Lihat bagian ini, lihat bagaimana pengiriman kami menjadi suatu fungsi .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Perhatikan bahwa setiap middleware akan diberikan dispatchdan getStateberfungsi sebagai argumen bernama.

OK, inilah bagaimana Redux-thunk sebagai salah satu middlewares yang paling banyak digunakan untuk Redux memperkenalkan dirinya:

Redux Thunk middleware memungkinkan Anda untuk menulis pembuat tindakan yang mengembalikan fungsi alih-alih tindakan. Thunk dapat digunakan untuk menunda pengiriman suatu tindakan, atau untuk mengirim hanya jika kondisi tertentu terpenuhi. Fungsi bagian dalam menerima pengiriman metode toko dan getState sebagai parameter.

Jadi seperti yang Anda lihat, itu akan mengembalikan fungsi alih-alih tindakan, berarti Anda dapat menunggu dan memanggilnya kapan saja karena itu fungsi ...

Jadi apa-apaan ini? Begitulah diperkenalkan di Wikipedia:

Dalam pemrograman komputer, thunk adalah subrutin yang digunakan untuk menyuntikkan perhitungan tambahan ke subrutin lain. Thunks terutama digunakan untuk menunda perhitungan sampai diperlukan, atau untuk memasukkan operasi di awal atau akhir subrutin lainnya. Mereka memiliki berbagai aplikasi lain untuk mengkompilasi pembuatan kode dan pemrograman modular.

Istilah ini berasal sebagai turunan lucu "berpikir".

Thunk adalah fungsi yang membungkus ekspresi untuk menunda evaluasinya.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

Jadi lihat betapa mudahnya konsep itu dan bagaimana itu dapat membantu Anda mengelola tindakan async Anda ...

Itu sesuatu yang bisa Anda jalani tanpanya, tetapi ingat dalam pemrograman selalu ada cara yang lebih baik, lebih rapi, dan tepat untuk melakukan sesuatu ...

Terapkan middleware Redux

Alireza
sumber
1
Pertama kali pada SO, tidak membaca apa pun. Tetapi hanya menyukai posting menatap gambar. Luar biasa, petunjuk, dan pengingat.
Bhojendra Rauniyar
2

Untuk menggunakan Redux-saga adalah middleware terbaik dalam implementasi React-redux.

Mis .: store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

Dan kemudian saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

Dan kemudian action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

Dan kemudian reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

Dan kemudian main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

coba ini .. sudah berfungsi

SM Chinna
sumber
3
Ini adalah hal serius bagi seseorang yang hanya ingin memanggil titik akhir API untuk mengembalikan entitas atau daftar entitas. Anda merekomendasikan, "lakukan saja ini ... lalu ini, lalu ini, lalu hal lainnya ini, lalu itu, maka hal-hal lain ini, kemudian lanjutkan, kemudian lakukan ..". Tapi kawan, ini FRONTEND, kita hanya perlu menelepon BACKEND untuk memberi kita data yang siap digunakan di frontend. Jika ini adalah cara untuk pergi, ada sesuatu yang salah, ada sesuatu yang benar-benar salah dan seseorang tidak menerapkan KISS saat ini
zameb
Hai, Gunakan coba dan tangkap blok untuk Panggilan API. Setelah API memberikan respons, panggil jenis tindakan Reducer.
SM Chinna
1
@zameb Anda mungkin benar, tetapi keluhan Anda, kemudian, adalah dengan Redux itu sendiri, dan semua yang didengarnya terdengar ketika mencoba untuk mengurangi kompleksitas.
jorisw
1

Ada pembuat tindakan yang sinkron dan kemudian ada pembuat tindakan yang tidak sinkron.

Pembuat tindakan sinkron adalah yang ketika kami menyebutnya, ia segera mengembalikan objek Tindakan dengan semua data yang relevan yang melekat pada objek itu dan siap untuk diproses oleh reduksi kami.

Pembuat aksi asinkron adalah pembuat yang membutuhkan sedikit waktu sebelum siap untuk akhirnya mengirim tindakan.

Menurut definisi, kapan saja Anda memiliki pembuat tindakan yang membuat permintaan jaringan, itu selalu akan memenuhi syarat sebagai pembuat tindakan async.

Jika Anda ingin memiliki pembuat tindakan asinkron di dalam aplikasi Redux Anda harus menginstal sesuatu yang disebut middleware yang akan memungkinkan Anda untuk berurusan dengan pembuat tindakan asinkron tersebut.

Anda dapat memverifikasi ini dalam pesan kesalahan yang memberitahu kami menggunakan middleware khusus untuk tindakan async.

Jadi apa itu middleware dan mengapa kita membutuhkannya untuk aliran async di Redux?

Dalam konteks redux middleware seperti redux-thunk, middleware membantu kita berurusan dengan pembuat aksi asinkron karena itu adalah sesuatu yang Redux tidak bisa menangani di luar kotak.

Dengan middleware yang diintegrasikan ke dalam siklus Redux, kami masih memanggil pembuat tindakan, yang akan mengembalikan tindakan yang akan dikirim tetapi sekarang ketika kami mengirimkan tindakan, daripada mengirimkannya langsung ke semua reduksi kami, kami akan untuk mengatakan bahwa suatu tindakan akan dikirim melalui semua middleware yang berbeda di dalam aplikasi.

Di dalam aplikasi Redux tunggal, kita dapat memiliki middleware sebanyak atau sesedikit yang kita inginkan. Sebagian besar, dalam proyek yang kami kerjakan kami akan memiliki satu atau dua middleware yang terhubung ke toko Redux kami.

Middleware adalah fungsi JavaScript biasa yang akan dipanggil dengan setiap tindakan yang kami kirim. Di dalam fungsi itu, middleware memiliki kesempatan untuk menghentikan tindakan agar tidak dikirim ke salah satu reduksi, itu dapat memodifikasi tindakan atau hanya bermain-main dengan tindakan dengan cara apa pun yang Anda misalnya, kita bisa membuat middleware yang konsol log setiap tindakan yang Anda kirimkan hanya untuk kesenangan menonton Anda.

Ada sejumlah besar middleware open source yang dapat Anda instal sebagai dependensi ke dalam proyek Anda.

Anda tidak terbatas hanya menggunakan middleware open source atau menginstalnya sebagai dependensi. Anda dapat menulis middleware kustom Anda sendiri dan menggunakannya di dalam toko Redux Anda.

Salah satu kegunaan middleware yang lebih populer (dan mendapatkan jawaban Anda) adalah untuk berurusan dengan pembuat tindakan asinkron, mungkin middleware paling populer di luar sana adalah redux-thunk dan ini tentang membantu Anda berurusan dengan pembuat tindakan asinkron.

Ada banyak jenis middleware lain yang juga membantu Anda dalam berurusan dengan pembuat tindakan asinkron.

Daniel
sumber
1

Untuk Menjawab pertanyaan:

Mengapa komponen kontainer tidak dapat memanggil API async, dan kemudian mengirim tindakan?

Saya akan mengatakan setidaknya untuk dua alasan:

Alasan pertama adalah pemisahan kekhawatiran, itu bukan tugas action creatoruntuk memanggil apidan mendapatkan data kembali, Anda harus melewati dua argumen untuk Anda action creator function, action typedan a payload.

Alasan kedua adalah karena redux storemenunggu objek polos dengan jenis tindakan wajib dan opsional a payload(tapi di sini Anda harus melewati payload juga).

Pembuat tindakan harus berupa objek biasa seperti di bawah ini:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

Dan pekerjaan Redux-Thunk midlewareuntuk dispachehasil Anda api callsesuai action.

Mselmi Ali
sumber
0

Ketika bekerja di proyek perusahaan, ada banyak persyaratan yang tersedia di perangkat tengah seperti (saga) tidak tersedia dalam aliran asinkron sederhana, di bawah ini adalah beberapa:

  • Menjalankan permintaan secara paralel
  • Menarik tindakan di masa depan tanpa perlu menunggu
  • Panggilan tanpa pemblokiran Efek balapan, contohnya pickup pertama
  • respons untuk memulai proses Mengurutkan tugas-tugas Anda (pertama di panggilan pertama)
  • Menulis
  • Pembatalan tugas Secara dinamis membentuk tugas.
  • Mendukung Concurrency Running Saga di luar redux middleware.
  • Menggunakan saluran

Daftarnya panjang, cukup tinjau bagian lanjutan dalam dokumentasi saga

Feras
sumber