Skip to content

Instantly share code, notes, and snippets.

@neurosnap
Created November 8, 2022 14:15
Show Gist options
  • Save neurosnap/302796385ddfea7bbda84d54598f0728 to your computer and use it in GitHub Desktop.
Save neurosnap/302796385ddfea7bbda84d54598f0728 to your computer and use it in GitHub Desktop.

saga-toolkit

The goal of this project is to enhance redux-saga core with opinionated ways to use the library.

features

  • helpers for making http requests (e.g. fetch effect)
  • helpers for setting up redux-saga
  • better types with yield delegate
  • helpers to make it easier to create sagas

helpers for making http requests

fetch is the dominate way to make HTTP requests both in the browser and in node. In this project we provide some helpers to make it easier to interact with the fetch api.

before:

function* fetchUser(action: { payload: { id: string } }) {
  const resp = yield call(fetch, `/users/${action.payload.id}`);
  if (!resp.ok) {
    return;
  }
  const data = yield call([resp, 'json']);
  console.log(data);
}

after:

function* fetchUser(action: { payload: { id: string } }) {
  const resp = yield fetch(`/users/${action.payload.id}`);
  if (!resp.ok) {
    return;
  }
  const data = yield call([resp, 'json']);
  console.log(data);
}

This seems like a small change, but the added benefit is with this effect, we can automatically abort the fetch when the saga is cancelled.

Since fetching JSON from an http request is so common, we will also provide a helper for that:

function* fetchUser(action: { payload: { id: string } }) {
  const resp = yield fetchJson(`/users/${action.payload.id}`);
  // `resp` has a different type than `fetch` but we try to emulate it for ease
  // of use.
  if (!resp.ok) {
    return;
  }
  console.log(resp.data);
}

helper for setting up redux-saga

Setting up redux-saga is not terribly difficult but I almost always have to read documentation on setting it up for the first time. It's also confusing figuring out the correct way to create a root saga. Similarly to combineReducers we want to provide a combineSagas function that will create a fault-tolerant root saga.

function* watchOne() {}
function* watchTwo() {}

const rootSaga = combineSagas({
  watchOne,
  watchTwo,
});

const sagaMiddleware = createSagaMiddleware();
const store = createStore(() => {}, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);

All the independent sagas will be combined in such a way that if they throw an error it will not effect any other saga. Further we will also automatically restart the failed sagas.

better types with yield delegate

Typescript is so critical to modern JS codebases now. redux-saga tries its best to provide the best types that are possible but because of fundamental limitations to typescript (see this ts issue for details), they do not handle generators in the way we are using them.

There's a loophole that works well enough that we would like to promote it as a first-class citizen in this project.

import { call } from 'saga-toolkit';

function* fetchUser(action: { payload: { id: string } }) {
  const resp = yield* call(fetch, `/users/${action.payload.id}`);
  if (!resp.ok) {
    return;
  }
  const data = yield* call([resp, 'json']);
  console.log(data);
}

helpers to make it easier to create sagas

Creating sagas involves quite a bit of boilerplate. Usually you would create an effect function and then a watcher function to listen for events:

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

function* watchFetchUser() {
  yield takeEvery('FETCH_USER', fetchUser);
}

This seems simple enough but when you have dozens or even a hundred sagas, it quickly becomes quite a burden.

Instead, we created a set of helper functions to make it slightly easier to create effects.

import { effectWith } from 'saga-toolkit';

function* fetchUser(action: { payload: { id: string } }) {
  // ...
}

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

function* createUser(action: { payload: { name: string } }) {
  // ...
}

const fxEvery = effectWith(takeEvery);
const { sagas, actions } = fxEvery({
  fetchUser,
  fetchAllUsers,
  createUser,
});

The sagas object will create a map of the sagas that can be passed into combineSagas. The actions object contains a map between action type and action creator. The action payload is automatically inferred from the payload used in the saga function. So if you call createUser it will expect { name: string } as its paramter.

const rootSaga = combineSagas({
  ...sagas,
});
store.dispatch(actions.createUser({ name: 'bob' }));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment