You can throttle a sequence of dispatched actions by using a handy built-in throttle helper. For example, suppose the UI fires an INPUT_CHANGED action while the user is typing in a text field.
By using this helper the watchInput won't start a new handleInput task for 500ms, but in the same time it will still be accepting the latest INPUT_CHANGED actions into its underlaying buffer, so it'll miss all INPUT_CHANGED actions happening in-between. This ensures that the Saga will take at most one INPUT_CHANGED action during each period of 500ms and still be able to process trailing action.
Debouncing
To debounce a sequence, put the built-in delay helper in the forked task:
In the above example handleInput waits for 500ms before performing its logic. If the user types something during this period we'll get more INPUT_CHANGED actions. Since handleInput will still be blocked in the delay call, it'll be cancelled by watchInput before it can start performing its logic.
Example above could be rewritten with redux-saga takeLatest helper:
import { call, takeLatest, delay } from'redux-saga/effects'function*handleInput({ input }) {// debounce by 500msyielddelay(500)...}function*watchInput() {// will cancel current running handleInput taskyieldtakeLatest('INPUT_CHANGED', handleInput);}
Retrying XHR calls
To retry a XHR call for a specific amount of times, use a for loop with a delay:
In the above example the apiRequest will be retried for 5 times, with a delay of 2 seconds in between. After the 5th failure, the exception thrown will get caught by the parent saga, which will dispatch the UPDATE_ERROR action.
If you want unlimited retries, then the for loop can be replaced with a while (true). Also instead of take you can use takeLatest, so only the last request will be retried. By adding an UPDATE_RETRY action in the error handling, we can inform the user that the update was not successfull but it will be retried.
The ability to undo respects the user by allowing the action to happen smoothly first and foremost before assuming they don't know what they are doing. GoodUI The redux documentation describes a robust way to implement an undo based on modifying the reducer to contain past, present, and future state. There is even a library redux-undo that creates a higher order reducer to do most of the heavy lifting for the developer.
However, this method comes with overhead because it stores references to the previous state(s) of the application.
Using redux-saga's delay and race we can implement a basic, one-time undo without enhancing our reducer or storing the previous state.
import { take, put, call, spawn, race, delay } from'redux-saga/effects'import { updateThreadApi, actions } from'somewhere'function*onArchive(action) {const { threadId } = actionconstundoId=`UNDO_ARCHIVE_${threadId}`constthread= { id: threadId, archived:true }// show undo UI element, and provide a key to communicateyieldput(actions.showUndo(undoId))// optimistically mark the thread as `archived`yieldput(actions.updateThread(thread))// allow the user 5 seconds to perform undo.// after 5 seconds, 'archive' will be the winner of the race-conditionconst { undo,archive } =yieldrace({ undo:take(action =>action.type ==='UNDO'&&action.undoId === undoId), archive:delay(5000) })// hide undo UI element, the race condition has an answeryieldput(actions.hideUndo(undoId))if (undo) {// revert thread to previous stateyieldput(actions.updateThread({ id: threadId, archived:false })) } elseif (archive) {// make the API call to apply the changes remotelyyieldcall(updateThreadApi, thread) }}function*main() {while (true) {// wait for an ARCHIVE_THREAD to happenconstaction=yieldtake('ARCHIVE_THREAD')// use spawn to execute onArchive in a non-blocking fashion, which also// prevents cancellation when main saga gets cancelled.// This helps us in keeping state in sync between server and clientyieldspawn(onArchive, action) }}