The goal of this project is to enhance redux-saga
core with opinionated ways
to use the library.
- 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
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);
}
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.
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);
}
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' }));