Bagaimana cara mengirim tindakan Redux dengan batas waktu?

891

Saya memiliki tindakan yang memperbarui status pemberitahuan aplikasi saya. Biasanya, pemberitahuan ini akan menjadi semacam kesalahan atau info. Saya perlu mengirim tindakan lain setelah 5 detik yang akan mengembalikan status pemberitahuan ke yang awal, jadi tidak ada pemberitahuan. Alasan utama di balik ini adalah untuk menyediakan fungsionalitas di mana notifikasi menghilang secara otomatis setelah 5 detik.

Saya tidak beruntung menggunakan setTimeoutdan mengembalikan tindakan lain dan tidak dapat menemukan bagaimana ini dilakukan secara online. Jadi saran apa pun dipersilahkan.

Ilja
sumber
30
Jangan lupa untuk memeriksa redux-sagajawaban berdasarkan saya jika Anda menginginkan sesuatu yang lebih baik daripada thunks. Terlambat menjawab sehingga Anda harus menggulir waktu yang lama sebelum melihatnya :) tidak berarti itu tidak layak dibaca. Berikut ini jalan pintas: stackoverflow.com/a/38574266/82609
Sebastien Lorber
5
Setiap kali Anda melakukan setTimeout jangan lupa untuk menghapus timer menggunakan clearTimeout di componentWillUnMount metode siklus hidup
Hemadri Dasari
2
redux-saga keren tapi mereka tampaknya tidak memiliki dukungan untuk respons yang diketik dari fungsi generator. Mungkin masalah jika Anda menggunakan naskah dengan reaksi.
Crhistian Ramirez

Jawaban:

2617

Jangan terjebak dalam pemikiran bahwa perpustakaan harus meresepkan cara melakukan segalanya . Jika Anda ingin melakukan sesuatu dengan batas waktu dalam JavaScript, Anda perlu menggunakannya setTimeout. Tidak ada alasan mengapa tindakan Redux harus berbeda.

Redux memang menawarkan beberapa cara alternatif untuk menangani hal-hal yang tidak sinkron, tetapi Anda hanya boleh menggunakannya ketika Anda menyadari Anda mengulangi terlalu banyak kode. Kecuali Anda memiliki masalah ini, gunakan apa yang ditawarkan bahasa dan gunakan solusi paling sederhana.

Menulis Async Code Inline

Sejauh ini, ini adalah cara paling sederhana. Dan tidak ada yang spesifik untuk Redux di sini.

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Demikian pula, dari dalam komponen yang terhubung:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Satu-satunya perbedaan adalah bahwa dalam komponen yang terhubung Anda biasanya tidak memiliki akses ke toko itu sendiri, tetapi mendapatkan salah satu dispatch()atau pembuat tindakan tertentu disuntikkan sebagai alat peraga. Namun ini tidak ada bedanya bagi kami.

Jika Anda tidak suka membuat kesalahan ketik saat mengirim tindakan yang sama dari komponen yang berbeda, Anda mungkin ingin mengekstrak pembuat tindakan alih-alih mengirimkan objek tindakan secara inline:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

Atau, jika sebelumnya Anda telah mengikatnya dengan connect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

Sejauh ini kami belum menggunakan middleware atau konsep canggih lainnya.

Mengekstrak Pencipta Tindakan Async

Pendekatan di atas berfungsi dengan baik dalam kasus-kasus sederhana tetapi Anda mungkin menemukan bahwa ia memiliki beberapa masalah:

  • Ini memaksa Anda untuk menduplikasi logika ini di mana pun Anda ingin menampilkan pemberitahuan.
  • Notifikasi tidak memiliki ID sehingga Anda akan memiliki kondisi balapan jika Anda menampilkan dua notifikasi dengan cukup cepat. Ketika batas waktu pertama selesai, itu akan mengirimkan HIDE_NOTIFICATION, menyembunyikan pemberitahuan kedua lebih cepat daripada setelah batas waktu.

Untuk mengatasi masalah ini, Anda perlu mengekstrak fungsi yang memusatkan logika batas waktu dan mengirimkan dua tindakan tersebut. Mungkin terlihat seperti ini:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
  // for the notification that is not currently visible.
  // Alternatively, we could store the timeout ID and call
  // clearTimeout(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

Sekarang komponen dapat digunakan showNotificationWithTimeouttanpa menduplikasi logika ini atau memiliki kondisi balapan dengan pemberitahuan berbeda:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Mengapa showNotificationWithTimeout()menerima dispatchsebagai argumen pertama? Karena itu perlu mengirimkan tindakan ke toko. Biasanya komponen memiliki akses ke dispatchtetapi karena kita ingin fungsi eksternal untuk mengambil kendali atas pengiriman, kita perlu memberikannya kontrol atas pengiriman.

Jika Anda memiliki toko tunggal yang diekspor dari beberapa modul, Anda bisa mengimpornya dan dispatchlangsung menggunakannya:

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')    

Ini terlihat lebih sederhana tetapi kami tidak merekomendasikan pendekatan ini . Alasan utama kami tidak suka itu adalah karena itu memaksa toko untuk menjadi lajang . Ini membuatnya sangat sulit untuk mengimplementasikan rendering server . Di server, Anda ingin setiap permintaan memiliki toko sendiri, sehingga pengguna yang berbeda mendapatkan data yang dimuat sebelumnya berbeda.

Toko tunggal juga membuat pengujian lebih sulit. Anda tidak lagi dapat mengejek toko ketika menguji pembuat tindakan karena mereka merujuk toko nyata tertentu yang diekspor dari modul tertentu. Anda bahkan tidak dapat mengatur ulang keadaannya dari luar.

Jadi, sementara Anda secara teknis dapat mengekspor toko tunggal dari modul, kami tidak mendukungnya. Jangan lakukan ini kecuali Anda yakin bahwa aplikasi Anda tidak akan pernah menambahkan rendering server.

Kembali ke versi sebelumnya:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Ini menyelesaikan masalah dengan duplikasi logika dan menyelamatkan kita dari kondisi balapan.

Thlk Middleware

Untuk aplikasi sederhana, pendekatannya sudah cukup. Jangan khawatir tentang middleware jika Anda senang.

Namun, dalam aplikasi yang lebih besar, Anda mungkin menemukan ketidaknyamanan tertentu di sekitarnya.

Sebagai contoh, sangat disayangkan kita harus berpapasan dispatch. Ini membuatnya lebih sulit untuk memisahkan wadah dan komponen presentasi karena setiap komponen yang mengirim tindakan Redux secara tidak sinkron dengan cara di atas harus diterima dispatchsebagai penyangga agar dapat meneruskannya lebih lanjut. Anda tidak dapat lagi mengikat pembuat tindakan dengan connect()karena showNotificationWithTimeout()tidak benar-benar pembuat tindakan. Itu tidak mengembalikan tindakan Redux.

Selain itu, mungkin aneh untuk mengingat fungsi mana yang merupakan pembuat tindakan sinkron seperti showNotification()dan yang merupakan pembantu asinkron showNotificationWithTimeout(). Anda harus menggunakannya secara berbeda dan berhati-hati agar tidak salah mengartikannya.

Ini adalah motivasi untuk menemukan cara untuk "melegitimasi" pola penyediaan dispatchfungsi pembantu ini, dan membantu Redux "melihat" seperti pembuat tindakan sinkron sebagai kasus khusus pembuat tindakan normal daripada fungsi yang sama sekali berbeda.

Jika Anda masih bersama kami dan Anda juga mengenali sebagai masalah di aplikasi Anda, Anda dipersilakan untuk menggunakan middleware Redux Thunk .

Dalam intinya, Redux Thunk mengajarkan Redux untuk mengenali jenis tindakan khusus yang sebenarnya berfungsi:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

Ketika middleware ini diaktifkan, jika Anda mengirim suatu fungsi , middleware Redux Thunk akan memberikannya dispatchsebagai argumen. Ini juga akan "menelan" tindakan seperti itu jadi jangan khawatir tentang reduksi Anda menerima argumen fungsi aneh. Pereduksi Anda hanya akan menerima tindakan objek biasa - baik dipancarkan secara langsung, atau dipancarkan oleh fungsi seperti yang baru saja kami jelaskan.

Ini tidak terlihat sangat berguna, bukan? Tidak dalam situasi khusus ini. Namun itu memungkinkan kami mendeklarasikan showNotificationWithTimeout()sebagai pembuat tindakan Redux biasa:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

Perhatikan bagaimana fungsinya hampir identik dengan yang kita tulis di bagian sebelumnya. Namun itu tidak diterima dispatchsebagai argumen pertama. Alih-alih mengembalikan fungsi yang menerima dispatchsebagai argumen pertama.

Bagaimana kita menggunakannya dalam komponen kita? Jelas, kita bisa menulis ini:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

Kami memanggil pembuat tindakan async untuk mendapatkan fungsi batin yang hanya ingin dispatch, dan kemudian kami lulus dispatch.

Namun ini bahkan lebih aneh daripada versi aslinya! Mengapa kita bahkan pergi ke sana?

Karena apa yang saya katakan sebelumnya. Jika Redux Thunk middleware diaktifkan, setiap kali Anda mencoba mengirim fungsi alih-alih objek tindakan, middleware akan memanggil fungsi itu dengan dispatchmetode itu sendiri sebagai argumen pertama .

Jadi kita bisa melakukan ini sebagai gantinya:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

Akhirnya, mengirimkan tindakan asinkron (sebenarnya, serangkaian tindakan) terlihat tidak berbeda dengan mengirimkan satu tindakan secara sinkron ke komponen. Yang bagus karena komponen tidak boleh peduli apakah sesuatu terjadi secara sinkron atau asinkron. Kami baru saja mengabstraksikan hal itu.

Perhatikan bahwa karena kita “diajarkan” Redux untuk mengenali pencipta tindakan “khusus” seperti (kami menyebutnya dunk pencipta tindakan), kita sekarang dapat menggunakannya di mana saja di mana kita akan menggunakan pencipta tindakan biasa. Sebagai contoh, kita dapat menggunakannya dengan connect():

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

Status Membaca dalam Thunks

Biasanya reduksi Anda mengandung logika bisnis untuk menentukan status berikutnya. Namun, reduksi hanya masuk setelah tindakan dikirim. Bagaimana jika Anda memiliki efek samping (seperti memanggil API) di pembuat tindakan thunk, dan Anda ingin mencegahnya dalam kondisi tertentu?

Tanpa menggunakan middleware thunk, Anda cukup melakukan pemeriksaan di dalam komponen:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

Namun, tujuan mengekstraksi pembuat tindakan adalah memusatkan logika berulang ini di banyak komponen. Untungnya, Redux Thunk menawarkan Anda cara untuk membaca keadaan toko Redux saat ini. Selain itu dispatch, getStateargumen ini juga berfungsi sebagai argumen kedua untuk fungsi yang Anda kembali dari pembuat tindakan thunk Anda. Ini memungkinkan thunk membaca keadaan toko saat ini.

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike in a regular action creator, we can exit early in a thunk
    // Redux doesn’t care about its return value (or lack of it)
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

Jangan menyalahgunakan pola ini. Ini bagus untuk membatalkan panggilan API ketika ada data yang di-cache tersedia, tetapi itu bukan fondasi yang sangat baik untuk membangun logika bisnis Anda. Jika Anda getState()hanya menggunakan untuk mengirim tindakan yang berbeda secara kondisional, pertimbangkan untuk memasukkan logika bisnis ke dalam reduksi.

Langkah selanjutnya

Sekarang setelah Anda memiliki intuisi dasar tentang cara kerja thunks, lihat contoh Redux async yang menggunakannya.

Anda dapat menemukan banyak contoh di mana thunks mengembalikan Janji. Ini tidak diperlukan tetapi bisa sangat nyaman. Redux tidak peduli apa yang Anda kembali dari thunk, tetapi itu memberi Anda nilai pengembaliannya dispatch(). Inilah sebabnya mengapa Anda dapat mengembalikan Janji dari seorang pencuri dan menunggu sampai selesai dengan menelepon dispatch(someThunkReturningPromise()).then(...).

Anda juga dapat membagi pembuat tindakan thunk yang kompleks menjadi beberapa pembuat aksi thunk yang lebih kecil. The dispatchMetode yang disediakan oleh thunks dapat menerima thunks sendiri, sehingga Anda dapat menerapkan pola rekursif. Sekali lagi, ini bekerja paling baik dengan Janji karena Anda dapat menerapkan aliran kontrol asinkron di atas semua itu.

Untuk beberapa aplikasi, Anda mungkin menemukan diri Anda dalam situasi di mana persyaratan aliran kontrol asinkron Anda terlalu rumit untuk diungkapkan dengan thunks. Misalnya, mencoba ulang permintaan yang gagal, aliran otorisasi ulang dengan token, atau onboarding langkah-demi-langkah bisa terlalu bertele-tele dan rawan kesalahan saat ditulis dengan cara ini. Dalam hal ini, Anda mungkin ingin melihat solusi aliran kontrol asinkron yang lebih canggih seperti Redux Saga atau Redux Loop . Evaluasi mereka, bandingkan contoh yang relevan dengan kebutuhan Anda, dan pilih yang paling Anda sukai.

Akhirnya, jangan gunakan apa pun (termasuk thunks) jika Anda tidak benar-benar membutuhkannya. Ingat bahwa, tergantung pada persyaratan, solusi Anda mungkin terlihat sesederhana itu

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Jangan berkeringat kecuali Anda tahu mengapa Anda melakukan ini.

Dan Abramov
sumber
27
Tindakan Async tampak seperti solusi yang sederhana dan elegan untuk masalah umum. Mengapa tidak mendukung bagi mereka untuk redux tanpa perlu middleware? Jawaban ini kemudian bisa menjadi jauh lebih ringkas.
Phil Mander
83
@ PhilMander Karena ada banyak pola alternatif seperti github.com/raisemarketplace/redux-loop atau github.com/yelouafi/redux-saga yang sama (jika tidak lebih) elegan. Redux adalah alat tingkat rendah. Anda dapat membangun superset yang Anda sukai dan mendistribusikannya secara terpisah.
Dan Abramov
16
Bisakah Anda jelaskan ini: * pertimbangkan untuk memasukkan logika bisnis ke dalam reduksi *, apakah itu berarti saya harus mengirim tindakan, dan kemudian menentukan dalam peredam tindakan lebih lanjut untuk dikirim tergantung pada kondisi saya? Pertanyaan saya adalah, apakah saya kemudian mengirim tindakan lain langsung di peredam saya, dan jika tidak maka dari mana saya mengirimnya?
froginvasion
25
Kalimat ini hanya berlaku untuk kasus sinkron. Sebagai contoh jika Anda menulis if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })mungkin Anda harus adil dispatch({ type: 'C', something: cond })dan memilih untuk mengabaikan tindakan dalam reduksi, tergantung pada action.somethingdan kondisi saat ini.
Dan Abramov
29
@DanAbramov Anda mendapatkan upvote saya hanya untuk ini "Kecuali Anda memiliki masalah ini, gunakan apa yang ditawarkan bahasa dan gunakan solusi paling sederhana." Baru setelah saya menyadari siapa yang menulisnya!
Matt Lacey
189

Menggunakan Redux-saga

Seperti yang dikatakan Dan Abramov, jika Anda ingin kontrol lebih lanjut atas kode async Anda, Anda mungkin melihat redux-saga .

Jawaban ini adalah contoh sederhana, jika Anda ingin penjelasan yang lebih baik tentang mengapa redux-saga dapat berguna untuk aplikasi Anda, periksa jawaban lain ini .

Gagasan umum adalah bahwa Redux-saga menawarkan interpreter generator ES6 yang memungkinkan Anda untuk dengan mudah menulis kode async yang terlihat seperti kode sinkron (ini sebabnya Anda akan sering menemukan infinite saat loop di Redux-saga). Entah bagaimana, Redux-saga sedang membangun bahasanya sendiri langsung di dalam Javascript. Redux-saga dapat terasa agak sulit untuk dipelajari pada awalnya, karena Anda membutuhkan pemahaman dasar generator, tetapi juga memahami bahasa yang ditawarkan oleh Redux-saga.

Saya akan mencoba di sini untuk menjelaskan di sini sistem pemberitahuan yang saya buat di atas redux-saga. Contoh ini saat ini berjalan dalam produksi.

Spesifikasi sistem pemberitahuan lanjut

  • Anda dapat meminta pemberitahuan untuk ditampilkan
  • Anda dapat meminta pemberitahuan untuk disembunyikan
  • Pemberitahuan tidak boleh ditampilkan lebih dari 4 detik
  • Beberapa notifikasi dapat ditampilkan secara bersamaan
  • Tidak lebih dari 3 notifikasi dapat ditampilkan secara bersamaan
  • Jika pemberitahuan diminta saat sudah ada 3 pemberitahuan ditampilkan, maka antrian / tunda.

Hasil

Cuplikan layar aplikasi produksi saya Stample.co

bersulang

Kode

Di sini saya menamai notifikasi toasttetapi ini adalah detail penamaan.

function* toastSaga() {

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) {
        if ( activeToasts.length >= MaxToasts ) {
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        }
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    }

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() {
        while ( true ) {
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        }
    }


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() {
        while ( true ) {
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) {
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            }
            else {
                yield call(delay,50);
            }
        }
    }

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

Dan peredamnya:

const reducer = (state = [],event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    }
};

Pemakaian

Anda cukup mengirimkan TOAST_DISPLAY_REQUESTEDacara. Jika Anda mengirim 4 permintaan, hanya 3 pemberitahuan yang akan ditampilkan, dan yang 4 akan muncul sedikit kemudian setelah pemberitahuan 1 menghilang.

Perhatikan bahwa saya tidak secara khusus merekomendasikan pengiriman TOAST_DISPLAY_REQUESTEDdari BEJ. Anda lebih suka menambahkan kisah lain yang mendengarkan acara aplikasi yang sudah ada, dan kemudian mengirimkan TOAST_DISPLAY_REQUESTED: komponen Anda yang memicu notifikasi, tidak harus digabungkan dengan erat ke sistem notifikasi.

Kesimpulan

Kode saya tidak sempurna tetapi berjalan dalam produksi dengan 0 bug selama berbulan-bulan. Redux-saga dan generator pada awalnya agak sulit, tetapi begitu Anda memahaminya, sistem semacam ini cukup mudah dibangun.

Bahkan cukup mudah untuk menerapkan aturan yang lebih kompleks, seperti:

  • ketika terlalu banyak notifikasi "antri", kurangi waktu tampilan untuk setiap notifikasi sehingga ukuran antrian dapat berkurang lebih cepat.
  • mendeteksi perubahan ukuran jendela, dan mengubah jumlah maksimum pemberitahuan yang ditampilkan sesuai (misalnya, desktop = 3, potret ponsel = 2, lanskap telepon = 1)

Honnestly, semoga sukses menerapkan hal-hal seperti ini dengan benar pada thunks

Catatan Anda dapat melakukan hal yang persis sama dengan redux-observable yang sangat mirip dengan redux-saga. Itu hampir sama dan merupakan masalah selera antara generator dan RxJS.

Sebastien Lorber
sumber
18
Saya berharap jawaban Anda datang lebih awal ketika pertanyaan diajukan, karena saya tidak bisa lebih setuju dengan menggunakan perpustakaan efek samping Saga untuk logika bisnis seperti ini. Reduksi & Pembuat Tindakan adalah untuk transisi negara. Alur kerja tidak sama dengan fungsi transisi negara. Alur kerja melewati transisi, tetapi bukan transisi itu sendiri. Redux + React tidak memiliki ini sendiri - inilah mengapa Redux Saga sangat berguna.
Atticus
4
Terima kasih, saya mencoba melakukan yang terbaik untuk membuat redux-saga populer untuk alasan ini :) terlalu sedikit orang berpikir saat ini redux-saga hanya pengganti untuk thunks dan tidak melihat bagaimana redux-saga memungkinkan alur kerja yang kompleks dan terpisah
Sebastien Lorber
1
Persis. Actions & Reducers adalah bagian dari mesin negara. Kadang-kadang, untuk alur kerja yang rumit, Anda perlu sesuatu yang lain untuk mengatur mesin negara yang tidak secara langsung bagian dari mesin negara itu sendiri!
Atticus
2
Tindakan: Muatan / peristiwa ke keadaan transisi. Reduksi: Fungsi transisi negara. Komponen: Antarmuka pengguna yang mencerminkan keadaan. Tetapi ada satu bagian utama yang hilang - bagaimana Anda mengelola proses banyak transisi yang semuanya memiliki logika sendiri yang menentukan transisi mana yang akan dilakukan selanjutnya? Redux Saga!
Atticus
2
@ brbrdo jika Anda membaca dengan hati-hati jawaban saya, Anda akan melihat bahwa batas waktu notifikasi benar-benar ditangani yield call(delay,timeoutValue);: itu bukan API yang sama tetapi memiliki efek yang sama
Sebastien Lorber
25

Repositori dengan proyek sampel

Saat ini ada empat proyek sampel:

  1. Menulis Async Code Inline
  2. Mengekstrak Pencipta Tindakan Async
  3. Gunakan Redux Thunk
  4. Gunakan Redux Saga

Jawaban yang diterima luar biasa.

Tetapi ada sesuatu yang hilang:

  1. Tidak ada proyek sampel yang dapat dijalankan, hanya beberapa cuplikan kode.
  2. Tidak ada kode sampel untuk alternatif lain, seperti:
    1. Redux Saga

Jadi saya membuat repositori Hello Async untuk menambahkan hal-hal yang hilang:

  1. Proyek yang dapat dijalankan. Anda dapat mengunduh dan menjalankannya tanpa modifikasi.
  2. Berikan kode sampel untuk lebih banyak alternatif:

Redux Saga

Jawaban yang diterima sudah menyediakan cuplikan kode sampel untuk Async Code Inline, Async Action Generator dan Redux Thunk. Demi kelengkapan, saya memberikan cuplikan kode untuk Redux Saga:

// actions.js

export const showNotification = (id, text) => {
  return { type: 'SHOW_NOTIFICATION', id, text }
}

export const hideNotification = (id) => {
  return { type: 'HIDE_NOTIFICATION', id }
}

export const showNotificationWithTimeout = (text) => {
  return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}

Tindakannya sederhana dan murni.

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

Tidak ada yang spesial dengan komponen.

// sagas.js

import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))
}

// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}

export default notificationSaga

Sagas didasarkan pada Generator ES6

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

Dibandingkan dengan Redux Thunk

Pro

  • Anda tidak berakhir di neraka panggilan balik.
  • Anda dapat menguji aliran asinkron Anda dengan mudah.
  • Tindakan Anda tetap murni.

Cons

  • Itu tergantung pada Generator ES6 yang relatif baru.

Silakan merujuk ke proyek runnable jika potongan kode di atas tidak menjawab semua pertanyaan Anda.

Tyler Long
sumber
23

Anda dapat melakukan ini dengan redux-thunk . Ada panduan dalam dokumen redux untuk tindakan async seperti setTimeout.

Fatih Erikli
sumber
Hanya pertanyaan lanjutan yang cepat, ketika menggunakan middleware, applyMiddleware(ReduxPromise, thunk)(createStore)inilah cara Anda menambahkan beberapa middleware (koma terpisah?) Karena sepertinya saya tidak dapat mengaktifkan thunk.
Ilja
1
@Ilja Ini seharusnya bekerja:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier
22

Saya akan merekomendasikan juga melihat pola SAM .

Pola SAM menganjurkan untuk memasukkan "next-action-predicate" di mana tindakan (otomatis) seperti "pemberitahuan menghilang secara otomatis setelah 5 detik" dipicu setelah model telah diperbarui (model SAM ~ keadaan peredam + toko +).

Pola menganjurkan untuk mengurutkan tindakan dan mutasi model satu per satu, karena "status kontrol" dari model "mengontrol" tindakan mana yang diaktifkan dan / atau secara otomatis dieksekusi oleh predikat aksi berikutnya. Anda tidak bisa memprediksi (secara umum) keadaan apa sistem akan sebelum memproses suatu tindakan dan karenanya apakah tindakan Anda selanjutnya diharapkan akan diizinkan / mungkin.

Jadi misalnya kodenya,

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

tidak akan diizinkan dengan SAM, karena fakta bahwa tindakan hideNotification dapat dikirim tergantung pada model yang berhasil menerima nilai "showNotication: true". Mungkin ada bagian lain dari model yang mencegahnya menerimanya dan oleh karena itu, tidak ada alasan untuk memicu tindakan hideNotification.

Saya akan sangat menyarankan agar menerapkan predikat tindakan selanjutnya yang tepat setelah pembaruan toko dan status kontrol baru model dapat diketahui. Itu cara teraman untuk menerapkan perilaku yang Anda cari.

Anda dapat bergabung dengan kami di Gitter jika Anda mau. Ada juga panduan memulai SAM yang tersedia di sini .

Jean-Jacques Dubray
sumber
Saya hanya menggores permukaan sejauh ini, tetapi saya sudah senang dengan pola SAM. V = S( vm( M.present( A(data) ) ), nap(M))itu indah. Terima kasih telah berbagi pemikiran dan pengalaman Anda. Saya akan menggali lebih dalam.
@tor, terima kasih! ketika saya menulisnya pertama kali, saya memiliki perasaan yang sama. Saya telah menggunakan SAM dalam produksi selama hampir satu tahun sekarang, dan saya tidak dapat memikirkan saat di mana saya merasa saya perlu perpustakaan untuk mengimplementasikan SAM (bahkan vdom, meskipun saya bisa melihat kapan itu bisa digunakan). Hanya satu baris kode, itu dia! SAM menghasilkan kode isomorfik, tidak ada ambiguitas di mana cara menangani panggilan async ... Saya tidak dapat memikirkan waktu di mana saya berpikir, apa yang saya lakukan?
metaprogrammer
SAM adalah pola Rekayasa Perangkat Lunak yang sebenarnya (baru saja menghasilkan Alexa SDK dengannya). Ini didasarkan pada TLA + dan upaya untuk membawa kekuatan dari pekerjaan luar biasa itu kepada setiap pengembang. SAM mengoreksi tiga perkiraan yang (sudah banyak) digunakan semua orang selama beberapa dekade: - tindakan dapat memanipulasi keadaan aplikasi - penugasan setara dengan mutasi - tidak ada definisi yang tepat tentang apa langkah pemrograman itu (mis. Adalah langkah a = b * ca , apakah 1 / baca b, c 2 / hitung b * c, 3 / tetapkan a dengan hasil tiga langkah yang berbeda?
metaprogrammer
20

Setelah mencoba berbagai pendekatan populer (pembuat aksi, thunks, saga, epos, efek, custom middleware), saya masih merasa mungkin ada ruang untuk perbaikan sehingga saya mendokumentasikan perjalanan saya di artikel blog ini, Di mana saya menempatkan logika bisnis saya di aplikasi Bereaksi / Redux?

Sama seperti diskusi di sini, saya mencoba kontras dan membandingkan berbagai pendekatan. Akhirnya itu membuat saya memperkenalkan perpustakaan redux-logic baru yang mengambil inspirasi dari epos, sagas, custom middleware.

Ini memungkinkan Anda untuk mencegat tindakan untuk memvalidasi, memverifikasi, mengotorisasi, serta menyediakan cara untuk melakukan async IO.

Beberapa fungsi umum hanya dapat dideklarasikan seperti debouncing, pelambatan, pembatalan, dan hanya menggunakan respons dari permintaan terbaru (takeLatest). redux-logic membungkus kode Anda menyediakan fungsionalitas ini untuk Anda.

Itu membebaskan Anda untuk mengimplementasikan logika bisnis inti Anda sesuka Anda. Anda tidak harus menggunakan observable atau generator kecuali Anda mau. Gunakan fungsi dan panggilan balik, janji, fungsi async (async / tunggu), dll.

Kode untuk melakukan notifikasi 5s sederhana adalah sesuatu seperti:

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

Saya memiliki contoh pemberitahuan yang lebih maju dalam repo saya yang berfungsi mirip dengan apa yang dijelaskan oleh Sebastian Lorber di mana Anda dapat membatasi tampilan hingga N item dan memutar melalui item yang antri. contoh pemberitahuan redux-logika

Saya punya berbagai contoh live redux-logic jsfiddle serta contoh lengkap . Saya terus bekerja pada dokumen dan contoh.

Saya ingin mendengar tanggapan Anda.

Jeff Barczewski
sumber
Saya tidak yakin bahwa saya suka perpustakaan Anda tetapi saya suka artikel Anda! Bagus, bung! Anda telah melakukan cukup banyak pekerjaan untuk menghemat waktu orang lain.
Tyler Long
2
Saya membuat contoh proyek untuk redux-logic di sini: github.com/tylerlong/hello-async/tree/master/redux-logic Saya pikir ini adalah perangkat lunak yang dirancang dengan baik dan saya tidak melihat adanya kerugian besar dibandingkan dengan yang lain alternatif.
Tyler Long
9

Saya mengerti bahwa pertanyaan ini agak lama tapi saya akan memperkenalkan solusi lain menggunakan redux-observable alias. Epik.

Mengutip dokumentasi resmi:

Apa yang bisa diamati-redux?

Middleware berbasis RxJS 5 untuk Redux. Buat dan batalkan tindakan async untuk membuat efek samping dan lainnya.

Epik adalah inti primitif yang dapat diamati kembali.

Ini adalah fungsi yang mengambil aliran tindakan dan mengembalikan aliran tindakan. Tindakan dalam, tindakan keluar.

Dengan kata-kata yang lebih banyak atau lebih sedikit, Anda dapat membuat fungsi yang menerima tindakan melalui Stream dan kemudian mengembalikan aliran tindakan baru (menggunakan efek samping yang umum seperti timeout, penundaan, interval, dan permintaan).

Biarkan saya memposting kode dan kemudian menjelaskan sedikit tentangnya

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

Kode kunci untuk menyelesaikan masalah ini semudah pie seperti yang Anda lihat, satu-satunya hal yang tampak berbeda dari jawaban lainnya adalah fungsi rootEpic.

Butir 1. Seperti halnya saga, Anda harus menggabungkan epos untuk mendapatkan fungsi tingkat atas yang menerima aliran tindakan dan mengembalikan aliran tindakan, sehingga Anda dapat menggunakannya dengan pabrik middleware createEpicMiddleware . Dalam kasus kami, kami hanya perlu satu sehingga kami hanya memiliki rootEpic kami sehingga kami tidak harus menggabungkan apa pun, tetapi itu baik untuk mengetahui fakta.

Poin 2. RootEpic kami yang menangani logika efek samping hanya membutuhkan sekitar 5 baris kode yang luar biasa! Termasuk fakta yang cukup banyak deklaratif!

Butir 3. Penjelasan baris demi baris rootEpic (dalam komentar)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

Saya harap ini membantu!

cnexans
sumber
Bisakah Anda menjelaskan apa metode api spesifik lakukan di sini, seperti switchMap?
Dmitri Zaitsev
1
Kami menggunakan redux-observable di aplikasi React Native kami di Windows. Ini adalah solusi implementasi yang elegan untuk masalah yang kompleks, sangat tidak sinkron, dan memiliki dukungan fantastis melalui saluran Gitter dan masalah GitHub mereka. Lapisan kompleksitas tambahan hanya layak jika Anda sampai pada masalah yang sebenarnya dimaksudkan untuk menyelesaikannya, tentu saja.
Matt Hargett
8

Kenapa harus begitu sulit? Itu hanya logika UI. Gunakan tindakan khusus untuk mengatur data notifikasi:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

dan komponen khusus untuk menampilkannya:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

Dalam hal ini pertanyaannya harus "bagaimana Anda membersihkan negara lama?", "Bagaimana cara memberitahu komponen bahwa waktu telah berubah"

Anda dapat menerapkan beberapa tindakan TIMEOUT yang dikirim pada setTimeout dari suatu komponen.

Mungkin tidak apa-apa untuk membersihkannya setiap kali pemberitahuan baru ditampilkan.

Lagi pula, harus ada setTimeouttempat, kan? Mengapa tidak melakukannya dalam suatu komponen

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

Motivasinya adalah bahwa fungsi "notifikasi memudar" benar-benar menjadi perhatian UI. Jadi itu menyederhanakan pengujian untuk logika bisnis Anda.

Tampaknya tidak masuk akal untuk menguji bagaimana penerapannya. Masuk akal untuk memverifikasi kapan pemberitahuan akan habis. Dengan demikian lebih sedikit kode untuk rintisan, tes lebih cepat, kode pembersih.

Vanuan
sumber
1
Ini harus menjadi jawaban teratas.
mmla
6

Jika Anda ingin penanganan batas waktu pada tindakan selektif, Anda dapat mencoba pendekatan middleware . Saya menghadapi masalah serupa untuk menangani tindakan berdasarkan janji secara selektif dan solusi ini lebih fleksibel.

Katakanlah Anda pembuat tindakan Anda terlihat seperti ini:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

batas waktu dapat menampung banyak nilai dalam tindakan di atas

  • dalam ms - untuk durasi batas waktu tertentu
  • true - untuk durasi waktu habis yang konstan. (ditangani di middleware)
  • undefined - untuk pengiriman segera

Implementasi middleware Anda akan terlihat seperti ini:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

Anda sekarang dapat merutekan semua tindakan Anda melalui lapisan middleware ini menggunakan redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Anda dapat menemukan beberapa contoh serupa di sini

Yash
sumber
5

Cara yang tepat untuk melakukan ini adalah menggunakan Redux Thunk yang merupakan middleware populer untuk Redux, sesuai dengan dokumentasi Redux Thunk:

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

Jadi pada dasarnya itu mengembalikan fungsi, dan Anda dapat menunda pengiriman Anda atau meletakkannya dalam kondisi kondisi.

Jadi sesuatu seperti ini akan melakukan pekerjaan untuk Anda:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}
Alireza
sumber
4

Sederhana saja. Gunakan paket trim-redux dan tulis seperti ini di componentDidMounttempat lain dan bunuh componentWillUnmount.

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}
Mohmmad Ebrahimi Aval
sumber
3

Redux sendiri adalah pustaka verbose yang cantik, dan untuk hal-hal seperti itu Anda harus menggunakan sesuatu seperti Redux-thunk , yang akan memberikandispatch fungsi, sehingga Anda dapat mengirim penutupan pemberitahuan setelah beberapa detik.

Saya telah membuat perpustakaan untuk mengatasi masalah seperti verbositas dan kompabilitas, dan contoh Anda akan terlihat seperti berikut:

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

Jadi kami membuat tindakan sinkronisasi untuk menampilkan pemberitahuan di dalam tindakan async, yang dapat meminta beberapa info latar belakang, atau memeriksa nanti apakah pemberitahuan ditutup secara manual.

Bloomca
sumber