Skip to content

jds work

Redux Sagas A Year in

React, Redux, Programming2 min read

We've been using Redux Sagas for quite some time on our client side application. We chose sagas over thunks because of how explicit they are when it comes to side effects for various state changes and good patterns with them can do well to scale with large applications.

Overview

It's been about a year and half since we began rewriting our main frontend application from Backbone.js to React. Pretty straight away we decided to use redux-sagas for our redux middleware. There's plenty of reasons why applications wouldn't need such powerful tooling for state management but state management seems to have hit new levels of complexity in that, state management isn't necesarily about state anymore, but a small (rather dumb) data store. Given that keeping our side effects manageable as the code base grows is a huge concern for us.

Best practices we've found

There are a few things to avoid and some things we found really valuable in using sagas. These aren't meant to be anecdotally perscriptive, but rather pieces of encouragement to be intentional about how you write your side effects and what they can control

Don't Use Sagas for Component Interfaces

This may seem like an obvious point but it's important to recognize nonetheless as it's easy to begin to rely on Sagas for this type of behavior. It's important to understand the flow of the UI and what side effects need to happen. Whenever you work on a saga first ask the question, "Is this a UI side effect? Or a UI linear path?"

Watchers & Workers

A common pattern among sagas is to use the watcher/worker pattern. This is great because you know exactly what is listening for what change and makes your code a lot easier to understand for someone new coming into the mix.

ex-1
1export function* attemptCreateAction(action) {
2 try {
3 const response = yield call(actionsListService.createAction, action.params);
4 const prevPoints = yield select(state => state.player.points);
5 yield put(createActionSuccess({ ...response, prevPoints }));
6 yield put(propertyRecentActivityRequest());
7 yield put(propertyLeaderboardRequest());
8 if (action.params.rsvp) {
9 yield put(showFeedbackBanner({ message: 'Response recorded.' }));
10 }
11 } catch (e) {
12 yield put(showFeedbackBanner({ message: e.error, variant: 'alert' }));
13 yield put(createActionFailure(e));
14 }
15}
16
17export function* watchCreateActionRequest() {
18 yield takeLatest(CREATE_ACTION_REQUEST, attemptCreateAction);
19}

As you can see, it's clearly define what generator function is doing what, one is watching then handing off the work to be done to the worker function. There's an anti-pattern in here which optionally puts a sperate UI side effect and we'll talk about that next.

Side Effects in Side Effects

In ex-1 you can see that depending on the action type we're dispatching another action which causes a UI update. I haven't really figured out why I personally don't like this other than I just don't. I think this stems from an issue withour underlying architecture and model/domain structure. Avoid these types of conditionals if you can, they tend to make a saga less clear and if something isn't written right, unintentional effects can happen. These are also hard to test in isolation.

Integration Testing > Unit Testing for Sagas

We've been using redux-saga-test-plan as our library of choice for testing. This allows us to test the entire grouping of behavior from the actions to the reducers and all the saga based side effects. This is a wonderful way to test these. The initial learning curve is quite high if you've never used redux-sagas before, but it's definitely worth it for piece of mind as you make changes to your application.

Conclusion

No matter what your opinions are it's important to be intentional about things. These practices work for us because of the structure of our domains but they aren't for every application. I'm also well aware the Redux team discourages the use of redux-sagas but we've been burned by side-effect heavy code before and we want to ensure we take every precaution to keep things as maintainable as possible.