Using redux-saga To Simplify Your Growing React Native Codebase

Some of the most fragile, embarrassing, and unreadable code you will write is flow control. — Beowulf
So What’s the Problem Here Anyway?
Look. You know how it goes.
Just call an API and grab the data. Simple right?

Oh, but it might timeout after 30 seconds. So trap for that.
And we should also handle server errors because the backend team is using PHP (zing!).
Then once we get the data back, let’s hand it off to the Redux reducers. But let’s also update a few cached data points first. And fetch a few images so the UI is a bit snappier once the user starts scrolling.

But before all that, put up a loading screen. And once we finish, dismiss it. Oh, and update our usage tracking metrics too.

What a mess.
What Are Sagas?
By giving flow control a safe haven, sagas allow you to remove much clutter and confusion from the rest of your app.
They help organize the chaos of actions & reactions that litter your codebase.
Whether you use promises, callbacks, try/catch blocks, or good ol’ if statements, sagas help you turn these stories into instructions.
Testable, mockable, declarative instructions.
What’s Wrong With What I’m Doing Already?
If you’re new to sagas (but not Redux), you probably have your action creators doing some of your flow control via redux-thunk. It might be hard to test, but you know what? It works.
// an action creator with thunking
function createRequest () {
return function (dispatch, getState) {
dispatch({ type: 'REQUEST_STUFF' })
someApiCall(function(response) {
// some processing
dispatch({ type: 'RECEIVE_STUFF' })
}
}
}
You probably have other bits located in components.
function onHandlePress () {
this.props.dispatch({ type: 'SHOW_WAITING_MODAL' })
this.props.dispatch(createRequest())
}
And you’ve used the redux state tree (and reducers) to act as a signalling system to chain stuff together.
Code is everywhere.
Sagas can be a way out of that sprawl so you trigger your flows with one action.
function onHandlePress () {
this.props.dispatch(createRequest())
}
And whatever steps are involved, are centralized within the saga.
Still a bit fuzzy right?
What Do They Look Like?
Sagas are implemented as generator functions. Functions with benefits.
function *hello() {
}
The * is pronounced superstar. Fact.
The series of steps that make up your saga are yielded along the way.
function *hello() {
yield 'step 1 — cut a hole in a box'
yield 'step 2 ...'
}
Inside the redux-saga toolbox is a few useful things you can yield. Some are generic in nature (call), some are Redux-specific (put and take) and some of them are wacky thread-like things (fork, join, cancel, race).
Remember that completely contrived example from earlier? Well, portions of it might look something like this:
function *hello() {
yield take('BEGIN_REQUEST')
yield put({ type: 'SHOW_WAITING_MODAL' })
const response = yield call(myApiFunctionThatWrapsFetch)
yield put({ type: 'PRELOAD_IMAGES', response.images })
yield put({ type: 'USAGE_TRACK', activity: 'API_CALL'})
yield put({ type: 'HIDE_WAITING_MODAL' })
}
Ok, I just ambushed you with a lot there. Yes the yield is ugly. Yes, the * is weird. Without that odd syntax you’re left with 6 1-liners that do this:
- Wait for a BEGIN_REQUEST action to come through Redux.
- Dispatch a SHOW_WAITING_MODAL action through Redux.
- Call the API and get back a response.
- Dispatch an action through Redux for someone to start downloading some images.
- Seeing a pattern here?
- Yep, I bet you are.
Not bad eh?
We’ve just described a nice flow.
How Do You Run Them?
Sagas are Redux middleware. When you configure your Redux store, your sagas are injected into the store via the sagaMiddleware() function.
Cool, but that didn’t answer your question did it?
Let’s go back to our *hello() saga for a second. Let’s pretend that we injected that into our store with sagaMiddleware([hello]).
Once the Redux store springs to life, hello gets executed.
But, do you remember line 1?
yield take('BEGIN_REQUEST')
Execution will now stop. And wait. The non-blocking kind. Patiently. Lurking. Until the right message flows through Redux.
And when it does, execution will continue along until the next yield. And so-on. When the function is done, the saga is over.
You can turn sagas into daemons by wrapping them in endless loops.
function *foreverAlone () {
while (true) {
yield take('HI')
yield put({ type: 'WAVE' })
}
}
Sagas accept a parameter too. A getState function. That’s one way to get information from your Redux state tree.
How Do You Test Them?
Well, turns out, put, call, take and all the other redux-saga commands are instructions. They’re functions that return an object.
{ TAKE: 'HI' }
When your saga runs within the Redux sagaMiddleware, they perform as you’ve read. But when run in a unit test, they just return instructions. Which you can test!
Here’s an example:
test('foreverAlone', (t) => {
const saga = foreverAlone() let actual = null
let expected = null // test step 1
actual = saga.next().value
expected = take('HI')
t.deepEqual(actual, expected, 'waits for hi action') // test step 2
actual = saga.next().value
expected = put({ type: 'WAVE' })
t.deepEqual(actual, expected, 'dispatch a wave action')
}
Your generator gets stepped through one yield at a time by calling next(). next() returns an object with a .value that shows you what was yielded.
next() can also accept parameters. This way you can inject mocked data into your generators to test things branching logic paths (like if statements and try/catch blocks).
Epilogue
For me, settling in on redux-saga allowed me to simplify my whole actions layer. Action creators are mostly one-liners now. Pure functions.
My flow control layer is now a series of a dozen or so (and counting) sagas (running as daemons) anxiously waiting for their time to shine. They also have test coverage.
When I converted to redux-saga, suddenly my:
- components became simpler
- action creators pared down to next-to-nothing
- reducers didn’t change at all
- flow control was now isolated & testable
Check out the redux-saga library on Github. The README is beautiful. It covers much more than I do here.
Thanks to Yassine Elouafi for creating such a wonderful library. And thanks to Dan Abramov for Redux and his tweet which introduced me to redux-saga.
Be sure to read Jack Hsu’s article on Sagas. Also Riad Benguella has a post about this topic as well. Both are great reads.
Have questions? Comments? I’m skellock on Twitter. Or visit us at Infinite Red.