Pro / kontra menggunakan redux-saga dengan generator ES6 vs redux-thunk dengan ES2017 async / menunggu

488

Ada banyak pembicaraan tentang anak terbaru di kota redux sekarang, redux-saga / redux-saga . Ini menggunakan fungsi generator untuk mendengarkan / mengirim tindakan.

Sebelum saya membungkus kepala saya, saya ingin tahu pro / kontra dari menggunakan redux-sagadaripada pendekatan di bawah ini di mana saya menggunakan redux-thunkdengan async / menunggu.

Sebuah komponen mungkin terlihat seperti ini, mengirimkan tindakan seperti biasa.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Maka tindakan saya terlihat seperti ini:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...
hampusohlsson
sumber
6
Lihat juga jawaban saya membandingkan redux-thunk ke redux-saga di sini: stackoverflow.com/a/34623840/82609
Sebastien Lorber
22
Apa yang ::sebelum Anda this.onClicklakukan?
Downhillski
37
@ZhenyangHua itu adalah kependekan untuk mengikat fungsi ke objek ( this), alias this.onClick = this.onClick.bind(this). Bentuk yang lebih panjang biasanya direkomendasikan untuk dilakukan di konstruktor, karena tangan pendek mengikat pada setiap render.
hampusohlsson
7
Saya melihat. Terima kasih! Saya melihat banyak orang menggunakan bind()untuk lulus thiske fungsi, tapi saya mulai menggunakan () => method()sekarang.
Downhillski
2
@Hosar Saya menggunakan redux & redux-saga dalam produksi untuk sementara waktu, tetapi sebenarnya bermigrasi ke MobX setelah beberapa bulan karena lebih sedikit overhead
hampusohlsson

Jawaban:

461

Dalam redux-saga, yang setara dengan contoh di atas adalah

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

Hal pertama yang perlu diperhatikan adalah kita memanggil fungsi api menggunakan formulir yield call(func, ...args). calltidak mengeksekusi efeknya, itu hanya menciptakan objek seperti biasa {type: 'CALL', func, args}. Eksekusi didelegasikan ke middleware redux-saga yang menangani mengeksekusi fungsi dan melanjutkan generator dengan hasilnya.

Keuntungan utama adalah bahwa Anda dapat menguji generator di luar Redux menggunakan pemeriksaan kesetaraan sederhana

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Catatan kami mengejek hasil panggilan api hanya dengan menyuntikkan data yang diolok-olok ke dalam nextmetode iterator. Mengejek data lebih mudah daripada mengolok-olok fungsi.

Hal kedua yang perlu diperhatikan adalah panggilan untuk yield take(ACTION). Thunks dipanggil oleh pencipta aksi pada setiap aksi baru (misalnya LOGIN_REQUEST). yaitu tindakan terus didorong ke thunks, dan thunks tidak memiliki kendali kapan harus berhenti menangani tindakan tersebut.

Di redux-saga, generator melakukan aksi selanjutnya. yaitu mereka memiliki kendali kapan harus mendengarkan suatu tindakan, dan kapan tidak. Pada contoh di atas, instruksi aliran ditempatkan di dalam sebuah while(true)loop, sehingga akan mendengarkan setiap tindakan yang masuk, yang agak meniru perilaku mendorong thunk.

Pendekatan tarikan memungkinkan penerapan aliran kontrol yang kompleks. Misalkan misalnya kita ingin menambahkan persyaratan berikut

  • Menangani tindakan pengguna LOGOUT

  • setelah login pertama yang berhasil, server mengembalikan token yang kedaluwarsa dalam penundaan yang disimpan dalam suatu expires_inbidang. Kami harus menyegarkan otorisasi di latar belakang pada setiap expires_inmilidetik

  • Mempertimbangkan bahwa ketika menunggu hasil panggilan api (baik login awal atau refresh) pengguna dapat keluar di antaranya.

Bagaimana Anda menerapkannya dengan thunks; sementara juga memberikan cakupan uji penuh untuk seluruh aliran? Berikut ini tampilannya dengan Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

Dalam contoh di atas, kami menyatakan persyaratan concurrency kami menggunakan race. Jika take(LOGOUT)memenangkan perlombaan (yaitu pengguna mengklik tombol Logout). Perlombaan akan secara otomatis membatalkan authAndRefreshTokenOnExpirytugas latar belakang. Dan jika authAndRefreshTokenOnExpirydiblokir di tengah call(authorize, {token})panggilan, itu juga akan dibatalkan. Pembatalan menyebar ke bawah secara otomatis.

Anda dapat menemukan demo runnable dari aliran di atas

Yassine Elouafi
sumber
@yassine dari mana delayfungsi itu berasal? Ah, menemukannya: github.com/yelouafi/redux-saga/blob/…
philk
122
The redux-thunkkode cukup dibaca dan self-menjelaskan. Tapi redux-sagassatu ini benar-benar dibaca, terutama karena mereka verba-seperti fungsi: call, fork, take, put...
syg
11
@syg, saya setuju bahwa panggilan, fork, take, dan put bisa lebih ramah secara semantik. Namun, fungsi yang mirip kata kerja itulah yang membuat semua efek sampingnya dapat diuji.
Downhillski
3
@syg masih berfungsi dengan fungsi kata kerja yang aneh lebih mudah dibaca daripada fungsi dengan rantai janji yang dalam
Yasser Sinjab
3
kata kerja "aneh" itu juga membantu Anda membuat konsep hubungan saga dengan pesan yang keluar dari redux. Anda dapat menghapus tipe pesan dari redux - seringkali untuk memicu iterasi berikutnya, dan Anda dapat memasukkan kembali pesan baru untuk menyiarkan hasil efek samping Anda.
worc
104

Saya akan menambahkan pengalaman saya menggunakan saga dalam sistem produksi selain jawaban penulis perpustakaan yang agak menyeluruh.

Pro (menggunakan saga):

  • Testabilitas. Sangat mudah untuk menguji kisah sebagai panggilan () mengembalikan objek murni. Menguji pukulan biasanya mengharuskan Anda untuk memasukkan mockStore di dalam pengujian Anda.

  • redux-saga hadir dengan banyak fungsi pembantu yang berguna tentang tugas. Menurut saya konsep saga adalah menciptakan semacam pekerja latar belakang / utas untuk aplikasi Anda, yang bertindak sebagai bagian yang hilang dalam arsitektur redux reaksi (actionCreator dan reduksi harus merupakan fungsi murni.) Yang mengarah ke poin berikutnya.

  • Sagas menawarkan tempat independen untuk menangani semua efek samping. Biasanya lebih mudah untuk memodifikasi dan mengelola daripada tindakan thunk dalam pengalaman saya.

Menipu:

  • Sintaks generator.

  • Banyak konsep untuk dipelajari.

  • Stabilitas API. Tampaknya redux-saga masih menambahkan fitur (mis. Saluran?) Dan komunitasnya tidak sebesar. Ada kekhawatiran jika perpustakaan membuat pembaruan yang tidak kompatibel ke belakang suatu hari nanti.

yjcxy12
sumber
9
Hanya ingin berkomentar, pencipta aksi tidak harus murni fungsi, yang telah diklaim oleh Dan sendiri berkali-kali.
Marson Mao
14
Sampai sekarang, redux-sagas sangat direkomendasikan karena penggunaan dan komunitas telah berkembang. Selain itu, API telah menjadi lebih matang. Pertimbangkan untuk menghapus Con API stabilitysebagai pembaruan untuk mencerminkan situasi saat ini.
Denialos
1
saga memiliki lebih banyak permulaan daripada thunk dan komit terakhirnya adalah setelah thunk juga
amorenew
2
Ya, FWIW redux-saga sekarang memiliki bintang 12k, redux-thunk memiliki 8k
Brian Burns
3
Saya akan menambahkan tantangan lain dari kisah-kisah, adalah bahwa kisah-kisah sepenuhnya dipisahkan dari tindakan dan pembuat tindakan secara default. Sementara Thunks secara langsung menghubungkan pembuat tindakan dengan efek sampingnya, kisah-kisah membuat para pembuat tindakan sepenuhnya terpisah dari kisah-kisah yang mendengarkan mereka. Ini memiliki keunggulan teknis, tetapi dapat membuat kode lebih sulit untuk diikuti, dan dapat mengaburkan beberapa konsep searah.
theaceofthespade
33

Saya hanya ingin menambahkan beberapa komentar dari pengalaman pribadi saya (menggunakan kedua kisah dan thunk):

Sagas bagus untuk diuji:

  • Anda tidak perlu mengejek fungsi yang dibalut dengan efek
  • Oleh karena itu tes bersih, mudah dibaca dan mudah ditulis
  • Saat menggunakan saga, pembuat aksi sebagian besar mengembalikan literal objek polos. Lebih mudah untuk menguji dan menegaskan tidak seperti janji-janji thunk.

Sagas lebih kuat. Semua yang dapat Anda lakukan di pencipta aksi satu thunk Anda juga dapat melakukannya dalam satu saga, tetapi tidak sebaliknya (atau setidaknya tidak mudah). Sebagai contoh:

  • menunggu tindakan / tindakan yang akan dikirim ( take)
  • membatalkan rutin yang ada ( cancel, takeLatest, race)
  • beberapa rutinitas dapat mendengarkan tindakan yang sama ( take, takeEvery, ...)

Sagas juga menawarkan fungsionalitas bermanfaat lainnya, yang menggeneralisasi beberapa pola aplikasi umum:

  • channels untuk mendengarkan sumber acara eksternal (mis. websockets)
  • model garpu ( fork, spawn)
  • mencekik
  • ...

Sagas adalah alat yang hebat dan kuat. Namun dengan kekuatan datang tanggung jawab. Ketika aplikasi Anda tumbuh, Anda bisa dengan mudah tersesat dengan mencari tahu siapa yang menunggu tindakan untuk dikirim, atau apa yang semuanya terjadi ketika beberapa tindakan sedang dikirim. Di sisi lain thunk lebih sederhana dan lebih mudah untuk dipikirkan. Memilih satu atau yang lain tergantung pada banyak aspek seperti jenis dan ukuran proyek, jenis efek samping apa yang harus ditangani oleh proyek Anda atau pilih preferensi tim. Dalam hal apa pun, jaga agar aplikasi Anda tetap sederhana dan dapat diprediksi.

madox2
sumber
8

Hanya beberapa pengalaman pribadi:

  1. Untuk gaya pengkodean dan keterbacaan, salah satu keuntungan paling signifikan dari menggunakan redux-saga di masa lalu adalah untuk menghindari panggilan balik neraka di redux-thunk - kita tidak perlu menggunakan banyak sarang kemudian / tangkap lagi. Tetapi sekarang dengan popularitas async / wait thunk, kita juga bisa menulis kode async dengan gaya sinkronisasi ketika menggunakan redux-thunk, yang dapat dianggap sebagai peningkatan dalam redux-think.

  2. Orang mungkin perlu menulis kode boilerplate lebih banyak ketika menggunakan redux-saga, terutama di Typecript. Sebagai contoh, jika seseorang ingin mengimplementasikan fungsi ambil async, penanganan data dan kesalahan dapat langsung dilakukan dalam satu unit thunk dalam action.js dengan satu tindakan FETCH. Tetapi dalam redux-saga, seseorang mungkin perlu mendefinisikan FETCH_START, FETCH_SUCCESS, dan FETCH_FAILURE tindakan dan semua pemeriksaan jenisnya yang terkait, karena salah satu fitur di redux-saga adalah menggunakan mekanisme "token" kaya ini untuk membuat efek dan menginstruksikan toko redux untuk pengujian mudah. Tentu saja orang bisa menulis saga tanpa menggunakan tindakan ini, tetapi itu akan membuatnya mirip dengan seorang pencuri.

  3. Dalam hal struktur file, redux-saga tampaknya lebih eksplisit dalam banyak kasus. Seseorang dapat dengan mudah menemukan kode terkait async di setiap sagas.ts, tetapi dalam redux-thunk, orang perlu melihatnya dalam tindakan.

  4. Pengujian mudah dapat menjadi fitur lain dalam redux-saga. Ini benar-benar nyaman. Tetapi satu hal yang perlu diklarifikasi adalah bahwa uji “panggilan” redux-saga tidak akan melakukan panggilan API yang sebenarnya dalam pengujian, sehingga orang perlu menentukan hasil sampel untuk langkah-langkah yang mungkin menggunakannya setelah panggilan API. Karena itu sebelum menulis di redux-saga, akan lebih baik untuk merencanakan kisah dan sagas.spec.ts yang sesuai secara rinci.

  5. Redux-saga juga menyediakan banyak fitur canggih seperti menjalankan tugas secara paralel, pembantu konkurensi seperti takeLatest / takeEvery, fork / spawn, yang jauh lebih kuat daripada thunks.

Sebagai kesimpulan, secara pribadi, saya ingin mengatakan: dalam banyak kasus normal dan aplikasi berukuran kecil hingga sedang, gunakan async / await style redux-thunk. Ini akan menghemat banyak kode / tindakan / typedef boilerplate, dan Anda tidak perlu beralih di banyak sagas.ts yang berbeda dan mempertahankan pohon sagas tertentu. Tetapi jika Anda sedang mengembangkan aplikasi besar dengan banyak logika async yang kompleks dan kebutuhan akan fitur-fitur seperti concurrency / pola paralel, atau memiliki permintaan tinggi untuk pengujian dan pemeliharaan (terutama dalam pengembangan yang digerakkan oleh tes), redux-saga mungkin akan menyelamatkan hidup Anda .

Bagaimanapun, redux-saga tidak lebih sulit dan kompleks daripada redux itu sendiri, dan itu tidak memiliki kurva belajar curam karena memiliki konsep inti dan API yang terbatas. Menghabiskan sedikit waktu untuk belajar redux-saga mungkin bermanfaat bagi diri Anda sendiri suatu hari nanti.

Jonathan
sumber
5

Setelah meninjau beberapa proyek React / Redux skala besar yang berbeda dalam pengalaman saya, Sagas memberikan pengembang cara menulis kode yang lebih terstruktur yang jauh lebih mudah untuk diuji dan sulit untuk mendapatkan kesalahan.

Ya itu agak aneh untuk memulainya, tetapi kebanyakan devs sudah cukup memahaminya dalam sehari. Saya selalu memberi tahu orang-orang untuk tidak khawatir tentang apa yang yieldharus dimulai dan bahwa begitu Anda menulis beberapa tes, itu akan datang kepada Anda.

Saya telah melihat beberapa proyek di mana thunks telah diperlakukan seolah-olah mereka adalah pengontrol dari patten MVC dan ini dengan cepat menjadi kekacauan yang tidak dapat dipecahkan.

Saran saya adalah menggunakan Sagas di mana Anda membutuhkan A memicu jenis barang B yang berkaitan dengan satu peristiwa. Untuk apa pun yang dapat memotong sejumlah tindakan, saya merasa lebih mudah untuk menulis middleware pelanggan dan menggunakan properti meta dari tindakan FSA untuk memicunya.

David Bradshaw
sumber
2

Thunks versus Sagas

Redux-Thunkdan Redux-Sagaberbeda dalam beberapa cara penting, keduanya adalah pustaka middleware untuk Redux (muxleware Redux adalah kode yang memotong tindakan yang masuk ke toko melalui metode dispatch ()).

Suatu tindakan dapat berupa apa saja, tetapi jika Anda mengikuti praktik terbaik, suatu tindakan adalah objek javascript biasa dengan bidang jenis, dan bidang muatan opsional, meta, dan kesalahan. misalnya

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Selain mengirim tindakan standar, Redux-Thunkmiddleware memungkinkan Anda untuk mengirim fungsi khusus, yang disebut thunks.

Thunks (dalam Redux) umumnya memiliki struktur berikut:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Yaitu, a thunkadalah fungsi yang (opsional) mengambil beberapa parameter dan mengembalikan fungsi lainnya. Fungsi dalam mengambil dispatch functiondan getStatefungsi - yang keduanya akan disediakan oleh Redux-Thunkmiddleware.

Redux-Saga

Redux-Sagamiddleware memungkinkan Anda untuk mengekspresikan logika aplikasi yang kompleks sebagai fungsi murni yang disebut saga. Fungsi murni diinginkan dari sudut pandang pengujian karena dapat diprediksi dan berulang, yang membuatnya relatif mudah untuk diuji.

Sagas diimplementasikan melalui fungsi khusus yang disebut fungsi generator. Ini adalah fitur baru dari ES6 JavaScript. Pada dasarnya, eksekusi melompat masuk dan keluar dari generator di mana pun Anda melihat pernyataan hasil. Pikirkan yieldpernyataan yang menyebabkan generator berhenti dan mengembalikan nilai yang dihasilkan. Kemudian, penelepon dapat melanjutkan generator di pernyataan berikut yield.

Fungsi generator didefinisikan seperti ini. Perhatikan tanda bintang setelah kata kunci fungsi.

function* mySaga() {
    // ...
}

Setelah saga login terdaftar Redux-Saga. Tetapi kemudian yieldmengambil pada baris pertama akan menghentikan saga sampai tindakan dengan tipe 'LOGIN_REQUEST'dikirim ke toko. Setelah itu terjadi, eksekusi akan berlanjut.

Untuk lebih jelasnya lihat artikel ini .

Mselmi Ali
sumber
1

Satu catatan singkat. Generator dapat dibatalkan, async / menunggu - tidak. Jadi sebagai contoh dari pertanyaan, itu tidak benar-benar masuk akal apa yang harus dipilih. Tetapi untuk aliran yang lebih rumit kadang-kadang tidak ada solusi yang lebih baik daripada menggunakan generator.

Jadi, ide lain adalah menggunakan generator dengan redux-thunk, tetapi bagi saya, sepertinya mencoba menciptakan sepeda dengan roda persegi.

Dan tentu saja, generator lebih mudah untuk diuji.

Dmitriy
sumber
0

Berikut adalah proyek yang menggabungkan bagian (pro) terbaik dari keduanya redux-sagadan redux-thunk: Anda dapat menangani semua efek samping pada kisah sambil mendapatkan janji dengan dispatchingtindakan yang sesuai: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}
Diego Haz
sumber
1
menggunakan then()di dalam komponen React bertentangan dengan paradigma. Anda harus menangani kondisi yang diubah componentDidUpdatedaripada menunggu janji untuk diselesaikan.
3
@ Maxincredible52 Itu tidak benar untuk Server Side Rendering.
Diego Haz
Dalam pengalaman saya, poin Max masih berlaku untuk rendering sisi server. Ini mungkin harus ditangani di suatu tempat di lapisan perutean.
ThinkingInBits
3
@ Maxincredible52 mengapa itu bertentangan dengan paradigma, di mana Anda membaca itu? Saya biasanya melakukan mirip dengan @Diego Haz tetapi melakukannya di componentDidMount (sesuai Bereaksi docs, panggilan jaringan sebaiknya dilakukan di sana) jadi kami punyacomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421
0

Cara yang lebih mudah adalah dengan menggunakan redux-auto .

dari dokumentasi

redux-auto memperbaiki masalah asinkron ini hanya dengan memungkinkan Anda membuat fungsi "aksi" yang mengembalikan janji. Untuk menyertai logika tindakan fungsi "default" Anda.

  1. Tidak perlu middleware Redux async lainnya. misal thunk, janji-middleware, saga
  2. Mudah memungkinkan Anda untuk memberikan janji ke redux dan mengaturnya untuk Anda
  3. Memungkinkan Anda untuk menentukan lokasi panggilan layanan eksternal dengan tempat mereka akan ditransformasikan
  4. Memberi nama file "init.js" akan memanggilnya sekali saat aplikasi dimulai. Ini bagus untuk memuat data dari server saat start

Idenya adalah memiliki setiap tindakan dalam file tertentu . co-locating panggilan server dalam file dengan fungsi peredam untuk "pending", "terpenuhi" dan "ditolak". Ini membuat penanganan janji sangat mudah.

Itu juga secara otomatis melampirkan objek pembantu (disebut "async") ke prototipe negara Anda, memungkinkan Anda untuk melacak di UI Anda, transisi yang diminta.

codemeasandwich
sumber
2
Saya membuat +1 bahkan itu jawaban yang tidak relevan karena solusi yang berbeda harus dipertimbangkan juga
amorenew
12
Saya pikir the's ada di sana karena dia tidak mengungkapkan dia adalah penulis proyek
jreptak