Redux Saga: The Future is 1975

WTF is a clock?

(This is the first post in a three-part introduction to Redux Saga. The second post is here, and the third post is here. We'll be building an abstract clock, which you can see live here.)

The React/Redux combo is, in my opinion, the best frontend tech stack available today. These two libraries are particularly symbiotic, since React can be a pure function of state => view, only caring about producing the right presentational content based on state, and Redux is a pure function of action => state serving as the one source and controller of state in your app. Yet even with this space-age tech stack, you may eventually notice that the responsibilities of your redux and react code become muddled–yes, muddled–with a new kind of wandering vagrant: code that controls the flow of your app. I'm going to call this "control flow" and assume that I'm the first person to ever come up with that term.1 In practice, the most difficult control flow code most often involves asynchronous programming, that is, code that coordinates itself with the timing of other processes.

  • "Once the POST request comes back, go to the next page or show an error."
  • "When the device power level reaches 5%, show a warning."
  • "After ten seconds, hide the modal."
  • "When my manager comes around, switch from Facebook to JIRA."

In some apps, this "homeless" code will start to overtake what was once a respectable neighborhood, pissing in alleyways and sleeping on park benches while you look the other way and go right on implementing new features like a dummy.

Fighting The Homeless

The javascript library Redux Saga is a middleware for Redux that offers a solution to the problem: isolate async control flow within generators that yield descriptions of what "effects" should occur at any given point in the flow.
Saga provides, free of charge, a well-constructed home for your app's async code. So it's basically Jimmy Carter. It makes heavy use of generators, therefore we need to know a little bit about how to use generators before moving on to Redux Saga itself. In this post, I'll cover the reasoning behind and benefit of using Redux Saga, and then I'll teach you the necessary background knowledge you need in order to start learning it. So what in the name of Dan Abramov (PBUH) is a "generator," and why do we need them?

Stroll on over to the wiki entry on generators and you will see this nugget:

A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time.... In short, a generator looks like a function but behaves like an iterator.

I feel like I learned nothing from that. Also of note:

"Generators first appeared in CLU (1975),[5] were a prominent feature in the string manipulation language Icon (1977) and are now available in Python,[6] C#,[7] Ruby, the upcoming version of ECMAScript (Harmony/ES6) and other languages. In CLU and C#, generators are called iterators, and in Ruby, enumerators."

So the general programming concept is quite old and well known, but only recently has it made its way into ECMAScript. But why should we care about this in Javascriptland?

A Brief History of Time...in JavaScript

Our runtime environment in javascript provides us, the hapless programmer, blissfully unaware of the browser's internals, with only one single-threaded event loop. All our code executes in the same thread, and thus we must always follow the golden rule, hammered into our brains, of never, never blocking execution since this blocks all execution and effectively halts our app while the code is blocking. This is no bueno. So we've had various solutions for handling async while not blocking, and all of them thus far have been some form of registering callbacks. To be sure, this is an indispensable tool that every JS dev must know inside and out. If you don't, you should go learn about callbacks and promises before continuing. Or you can learn generators first like a total madman.

Keeping in mind the golden rule of never pausing execution, let's delve into ES6's implementation of generators. As you can read on the highly esteemed 2ality Blog,

Generators, a new feature of ECMAScript 6, are functions that can be paused and resumed. This helps with many applications: iterators, asynchronous programming, etc.

What the f***?! He just said that generators "are functions that can be paused and resumed"!! We are allowed, nay, encouraged, to pause and resume within a generator! This is a complete departure from that "never block" mindset. We now have a programming construct that allows us to write code that can be paused and resumed. This is why generators are a new milestone in the evolution of async programming in JS.

A Crash Course in ES6 Generators

You don't need to have a Ph.D. in Generatorology to start being productive with Redux Saga, but you should know the basics. What follows are the absolute essentials of generators in ES6:

  • A generator function starts with function* ("function star") and then looks like a normal function:
function* genFunc () {  
  // code goes here 
}
  • A generator function returns a generator

  • A generator is not a function, damn it, it's just a special object returned by the generator function (see previous point): let genObj = genFunc()

  • We can pause a generator in the middle of its function by using yield

  • DON'T FORGET THE STAR, OK? function* example () { yield 'scheiße' }

  • We don't use the new keyword when invoking a generator function because it isn't really a constructor. We can call the generator function all damn day and it will just give us brand new generator objects, paused at the beginning of their function body until we call next:

function* genFunc () { yield 'hi' }  
> let genObj1 = genFunc()
> let genObj2 = genFunc()
> genObj1.next()
{ value: "hi", done: false }
> genObj1.next()
{ value: undefined, done: true }
> genObj2.next()
{ value: "hi", done: false }
  • As you see above, when you call the next method on a generator like this, genObj.next(), it executes until the next yield and then returns whatever that yield expression evaluates to in the form of { value: any, done: boolean }, where "value" is the yielded value and "done" tells us whether the generator has anything left to yield

  • Next time it runs, it will continue from this point. But you aren't listening because you're too busy picturing Dan Abramov's hair.

  • If you run it to completion, it will return { value: undefined, done: true } on every subsequent next()

  • We can send values into the generator by passing them as an argument to next and assign them to variables:

function* test () {  
  while (true) { 
    console.log('FIRST STEP')
    var x = yield 1
    console.log('SECOND STEP')
    yield x * 2
  }
}

> let gen = test()
> gen.next()
// executes until the first yield, sends out its value and waits
FIRST STEP  
{ value: 1, done: false }
gen.next(29)  
// sends in 29 to the first yield, which is waiting, assigns it to x, then executes until the next yield and sends out its value
SECOND STEP  
{ value: 58, done: false }
gen.next(2)  
// paused at the second yield, we execute up until the first yield again, the value passed to next gets thrown away since we didn't assign it to anything
FIRST STEP  
{ value: 1, done: false }
gen.next()  
// sends in no value to the paused first yield (undefined), and executes until the second yield, sending out the result of its operand expression
SECOND STEP  
{ value: NaN, done: false }
  • So next can be thought of as three steps: 1. send a value to the currently suspended yield 2. return the operand of the next yield 3. suspend on that yield

Alright, that was a lot to digest, but now you know how sending values into a generator works. You may want to slowly walk through the big ugly code snippet up there again to make sure you understand things clearly. And make sure you memorize the three steps that happen when you call next. You should be able to recite these in your sleep. Not only is this key to understanding generators and Redux Saga, but it becomes incredibly useful once we start writing tests for saga code. Two final points:

  • You can only yield while in the generator function, not in a callback. For example, this doesn't work:
function* genFunc () { [1, 2].map(x => yield x) }  
  • You can use yield* to pass control to another generator:
function* foo () {  
  yield* bar()
  yield 3
}

function* bar () {  
  yield 1
  yield 2
}

> let gen = foo()
> gen.next()
{ value: 1, done: false }
> gen.next()
{ value: 2, done: false }
> gen.next()
{ value: 3, done: false }
> gen.next()
{ value: undefined, done: true }

I encourage you to re-read through the bullet points above a few times, and then play with generators in your browser's javascript console. There are yet a few more nuances of generators which you will inevitably learn if you build a complex app that uses saga, but if you feel somewhat comfortable with the basic concepts above, then you are prepared to delve awkwardly into your first Redux Saga app. Head on over to the next post and let's start working on some code.

  1. See https://msdn.microsoft.com/en-us/library/mt674892.aspx "Control Flow in Async Programs"

comments powered by Disqus