Skip to content

Instantly share code, notes, and snippets.

@hbarcelos
Last active September 4, 2020 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hbarcelos/9880a469ed36edf3d7c98b6b80756651 to your computer and use it in GitHub Desktop.
Save hbarcelos/9880a469ed36edf3d7c98b6b80756651 to your computer and use it in GitHub Desktop.
Reducing boilerplate of watcher sagas in redux-saga

How to use

import createWatcherSaga, { TakeType } from './create-watcher-saga.js';

// Act on every counter increment
const watchIncrementSaga = createWatcherSaga({ takeType: TakeType.every }, incrementSaga, 'counter/increment');
// Discard previous user fetches and act only on the latest
const watchFetchUser = createWatcherSaga({ takeType: TakeType.latest }, fetchUserSaga, 'user/fetch');
// Validate sign-up e-mail while the user is typing, but wait the user to have stopped typing for 2 seconds to not flood the server. 
const watchValidateSignUpEmailSaga = createWatcherSaga({ takeType: TakeType.debounce, timeout: 2000 }, validateSignUpEmailSaga, 'user/validateSignUp');
// Fetch exchange rates of the same pair of currencies at most once during 10 seconds.
const watchFetchRatesSaga = createWatcherSaga(
  { 
    takeType: TakeType.throttleByKey, 
    timeout: 10000, 
    selector: action => `${action.payload.from}-${action.payload.to}`
  }, 
  fetchRatesSaga,
  'currency/fetchRates'
);
import {
call,
cancelled,
debounce,
delay,
fork,
take,
takeEvery,
takeLatest,
takeLeading,
throttle,
} from 'redux-saga/effects';
export const TakeType = {
every: 'every',
latest: 'latest',
leading: 'leading',
throttle: 'throttle',
debounce: 'debounce',
throttleByKey: 'throttleByKey',
};
export default function createWatcherSaga({ takeType = TakeType.every, additionalArgs = [], timeout, selector }, saga, pattern) {
if ([TakeType.throttle, TakeType.debounde].includes(takeType)) {
if (timeout === undefined) {
throw new Error('Cannot use TakeType.throttle without specifying a timeout');
}
}
if (takeType === TakeType.throttleByKey) {
if (timeout === undefined) {
throw new Error('Cannot use TakeType.throttleByKey without specifying a timeout');
}
if (selector === undefined) {
throw new Error('Cannot use TakeType.throttleByKey without specifying a selector');
}
}
const factory = sagaFactoryByType[takeType];
if (!factory) {
throw new Error(`Unknown take type "${takeType}". Should be one of ${Object.keys(TakeType)}.`);
}
const watcherSaga = factory({ pattern, saga, additionalArgs, timeout, selector });
const sagaName = saga.displayName ?? saga.name ?? '<anonymous>';
return Object.defineProperty(watcherSaga, 'name', { value: `watcher(${sagaName})` });
}
const sagaFactoryByType = {
every: ({ pattern, saga, additionalArgs }) =>
function* watcherSaga() {
yield takeEvery(pattern, saga, ...additionalArgs);
},
latest: ({ pattern, saga, additionalArgs }) =>
function* watcherSaga() {
yield takeLatest(pattern, saga, ...additionalArgs);
},
leading: ({ pattern, saga, additionalArgs }) =>
function* watcherSaga() {
yield takeLeading(pattern, saga, ...additionalArgs);
},
throttle: ({ timeout, pattern, saga, additionalArgs }) =>
function* watcherSaga() {
yield throttle(timeout, pattern, saga, ...additionalArgs);
},
debounce: ({ timeout, pattern, saga, additionalArgs }) =>
function* watcherSaga() {
yield debounce(timeout, pattern, saga, ...additionalArgs);
},
throttleByKey: ({ timeout, selector, pattern, saga, additionalArgs }) =>
function* watcherSaga() {
const set = new Set();
while (true) {
const action = yield take(pattern);
const id = selector(action);
const throttled = set.has(id);
try {
if (!throttled) {
set.add(id);
yield fork(function* () {
yield delay(timeout);
set.delete(id);
});
yield call(saga, action, ...additionalArgs);
}
} finally {
if (yield cancelled()) {
set.delete(id);
}
}
}
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment