Saga middleware for Redux to handle side effects using ES6 generators
—
High-level helper effects built on top of basic effects for common patterns like handling every action, latest action, throttling, and debouncing.
Spawns a saga on each action dispatched to the Store that matches the pattern. Allows concurrent handling of actions.
/**
* Spawn saga on each matching action (allows concurrency)
* @param pattern - Action pattern to watch for
* @param worker - Saga function to spawn for each action
* @param args - Additional arguments passed to worker (action is always last)
* @returns ForkEffect that never completes (runs forever)
*/
function takeEvery<P extends ActionPattern>(
pattern: P,
worker: (action: ActionMatchingPattern<P>) => any
): ForkEffect<never>;
function takeEvery<P extends ActionPattern, Fn extends (...args: any[]) => any>(
pattern: P,
worker: Fn,
...args: HelperWorkerParameters<ActionMatchingPattern<P>, Fn>
): ForkEffect<never>;
/**
* Watch channel for messages
* @param channel - Channel to watch
* @param worker - Function to handle each message
* @returns ForkEffect
*/
function takeEvery<T>(
channel: TakeableChannel<T>,
worker: (item: T) => any
): ForkEffect<never>;Usage Examples:
import { takeEvery, call, put } from "redux-saga/effects";
function* fetchUser(action) {
try {
const user = yield call(api.fetchUser, action.payload.userId);
yield put({ type: 'USER_FETCH_SUCCEEDED', user });
} catch (error) {
yield put({ type: 'USER_FETCH_FAILED', error: error.message });
}
}
function* watchUserRequests() {
// Spawn fetchUser on every USER_FETCH_REQUESTED action
// Multiple requests can run concurrently
yield takeEvery('USER_FETCH_REQUESTED', fetchUser);
}
// With additional arguments
function* saveData(apiClient, action) {
yield call([apiClient, 'save'], action.payload);
}
function* watchSaveRequests() {
const apiClient = yield getContext('apiClient');
yield takeEvery('SAVE_REQUESTED', saveData, apiClient);
}Spawns a saga on each action, but automatically cancels any previous saga if it's still running. Only the latest saga runs.
/**
* Spawn saga on each action, cancel previous if running (latest wins)
* @param pattern - Action pattern to watch for
* @param worker - Saga function to spawn
* @param args - Additional arguments passed to worker
* @returns ForkEffect that never completes
*/
function takeLatest<P extends ActionPattern>(
pattern: P,
worker: (action: ActionMatchingPattern<P>) => any
): ForkEffect<never>;
function takeLatest<P extends ActionPattern, Fn extends (...args: any[]) => any>(
pattern: P,
worker: Fn,
...args: HelperWorkerParameters<ActionMatchingPattern<P>, Fn>
): ForkEffect<never>;
function takeLatest<T>(
channel: TakeableChannel<T>,
worker: (item: T) => any
): ForkEffect<never>;Usage Examples:
import { takeLatest, call, put } from "redux-saga/effects";
function* searchUsers(action) {
try {
const results = yield call(api.searchUsers, action.payload.query);
yield put({ type: 'SEARCH_SUCCEEDED', results });
} catch (error) {
yield put({ type: 'SEARCH_FAILED', error: error.message });
}
}
function* watchSearchRequests() {
// Cancel previous search if user types quickly
// Only the latest search request will complete
yield takeLatest('SEARCH_REQUESTED', searchUsers);
}Spawns a saga on an action, then blocks until that saga completes before accepting new actions of the same pattern.
/**
* Spawn saga and block until completion before accepting new actions
* @param pattern - Action pattern to watch for
* @param worker - Saga function to spawn
* @param args - Additional arguments passed to worker
* @returns ForkEffect that never completes
*/
function takeLeading<P extends ActionPattern>(
pattern: P,
worker: (action: ActionMatchingPattern<P>) => any
): ForkEffect<never>;
function takeLeading<P extends ActionPattern, Fn extends (...args: any[]) => any>(
pattern: P,
worker: Fn,
...args: HelperWorkerParameters<ActionMatchingPattern<P>, Fn>
): ForkEffect<never>;
function takeLeading<T>(
channel: TakeableChannel<T>,
worker: (item: T) => any
): ForkEffect<never>;Usage Examples:
import { takeLeading, call, put } from "redux-saga/effects";
function* processPayment(action) {
try {
const result = yield call(api.processPayment, action.payload);
yield put({ type: 'PAYMENT_SUCCEEDED', result });
} catch (error) {
yield put({ type: 'PAYMENT_FAILED', error: error.message });
}
}
function* watchPaymentRequests() {
// Ignore additional payment requests while one is processing
// Prevents double payments from rapid clicking
yield takeLeading('PROCESS_PAYMENT', processPayment);
}Spawns a saga on an action, then ignores subsequent actions for a specified time period while the saga is processing.
/**
* Spawn saga and ignore actions for specified time period
* @param ms - Milliseconds to throttle (ignore subsequent actions)
* @param pattern - Action pattern to watch for
* @param worker - Saga function to spawn
* @param args - Additional arguments passed to worker
* @returns ForkEffect that never completes
*/
function throttle<P extends ActionPattern>(
ms: number,
pattern: P,
worker: (action: ActionMatchingPattern<P>) => any
): ForkEffect<never>;
function throttle<P extends ActionPattern, Fn extends (...args: any[]) => any>(
ms: number,
pattern: P,
worker: Fn,
...args: HelperWorkerParameters<ActionMatchingPattern<P>, Fn>
): ForkEffect<never>;
function throttle<T>(
ms: number,
channel: TakeableChannel<T>,
worker: (item: T) => any
): ForkEffect<never>;Usage Examples:
import { throttle, call, put } from "redux-saga/effects";
function* saveUserPreferences(action) {
try {
yield call(api.savePreferences, action.payload);
yield put({ type: 'PREFERENCES_SAVED' });
} catch (error) {
yield put({ type: 'PREFERENCES_SAVE_FAILED', error: error.message });
}
}
function* watchPreferenceChanges() {
// Save preferences but throttle to max once per 2 seconds
// Prevents excessive API calls when user changes multiple settings
yield throttle(2000, 'PREFERENCES_CHANGED', saveUserPreferences);
}Spawns a saga only after actions stop being dispatched for a specified time period. Resets the timer on each new action.
/**
* Spawn saga after actions stop for specified time period
* @param ms - Milliseconds to wait after last action
* @param pattern - Action pattern to watch for
* @param worker - Saga function to spawn
* @param args - Additional arguments passed to worker
* @returns ForkEffect that never completes
*/
function debounce<P extends ActionPattern>(
ms: number,
pattern: P,
worker: (action: ActionMatchingPattern<P>) => any
): ForkEffect<never>;
function debounce<P extends ActionPattern, Fn extends (...args: any[]) => any>(
ms: number,
pattern: P,
worker: Fn,
...args: HelperWorkerParameters<ActionMatchingPattern<P>, Fn>
): ForkEffect<never>;
function debounce<T>(
ms: number,
channel: TakeableChannel<T>,
worker: (item: T) => any
): ForkEffect<never>;Usage Examples:
import { debounce, call, put } from "redux-saga/effects";
function* performAutoSave(action) {
try {
yield call(api.autoSave, action.payload.document);
yield put({ type: 'AUTO_SAVE_SUCCEEDED' });
} catch (error) {
yield put({ type: 'AUTO_SAVE_FAILED', error: error.message });
}
}
function* watchDocumentChanges() {
// Auto-save document 1 second after user stops typing
yield debounce(1000, 'DOCUMENT_CHANGED', performAutoSave);
}
function* searchSuggestions(action) {
const suggestions = yield call(api.getSearchSuggestions, action.payload.query);
yield put({ type: 'SUGGESTIONS_LOADED', suggestions });
}
function* watchSearchInput() {
// Fetch suggestions 300ms after user stops typing
yield debounce(300, 'SEARCH_INPUT_CHANGED', searchSuggestions);
}Creates an effect that attempts to call a function multiple times with delays between attempts on failure.
/**
* Retry function call with delay between attempts on failure
* @param maxTries - Maximum number of attempts
* @param delayLength - Milliseconds to wait between attempts
* @param fn - Function to retry
* @param args - Arguments to pass to function
* @returns CallEffect that resolves with successful result or throws final error
*/
function retry<Fn extends (...args: any[]) => any>(
maxTries: number,
delayLength: number,
fn: Fn,
...args: Parameters<Fn>
): CallEffect<SagaReturnType<Fn>>;Usage Examples:
import { retry, call, put } from "redux-saga/effects";
function* fetchUserWithRetry(action) {
try {
// Retry up to 3 times with 2 second delays
const user = yield retry(3, 2000, api.fetchUser, action.payload.userId);
yield put({ type: 'USER_FETCH_SUCCEEDED', user });
} catch (error) {
// All retries failed
yield put({ type: 'USER_FETCH_FAILED', error: error.message });
}
}
function* uploadFileWithRetry(action) {
try {
// Retry upload 5 times with 5 second delays
const result = yield retry(
5,
5000,
api.uploadFile,
action.payload.file,
action.payload.options
);
yield put({ type: 'UPLOAD_SUCCEEDED', result });
} catch (error) {
yield put({ type: 'UPLOAD_FAILED', error: error.message });
}
}Helper effects are high-level APIs built using lower-level effects:
takeEvery = fork(function*() { while(true) { const action = yield take(pattern); yield fork(worker, action); } })takeLatest = Like takeEvery but with cancellation of previous tasktakeLeading = Like takeEvery but with blocking until completionthrottle = takeEvery with timing-based action filteringdebounce = takeEvery with timer reset on each actionThese helpers provide common async patterns while maintaining full saga capabilities like cancellation and error handling.
Install with Tessl CLI
npx tessl i tessl/npm-redux-saga