Redux Saga: Finishing the Clock Demo App

Implementing Saga Code

This is the third and final post in a three-part series about Redux Saga. In the first post, we warmed ourselves up to the general concepts involved. In the second, we took our relationship with Saga to the next level. We made it real. And we felt like the only developer in the room. We thought it was different this time. We thought...we were special. In this final post of the series, we are going to write the rest of the app. If you haven't already, you can clone the starter repo from this url. If you just want to skip to the final product, go ahead and check out its repo on github and live demo here.

What's my name again?

In the last post, we considered our basic yet kind of non-trivial requirements, to create a rudimentary clock that responds to some user interactions, and for some frakking reason we chose to use a bleeding-edge, space-age tech stack to achieve this. Oh yeah, it was to learn the basics of Redux Saga. Right. Cool. We took a shot of Norseman Vodka (Minneapolis, MN) and chased it with La Croix Pamplemousse flavor carbonated water, which is, of course, the best flavor of La Croix. (Edit: apparently, some Facebook developer agrees with me.) We then cloned our boilerplate repo, took another fine sip of Norseman, and began writing some code.

We determined that our minimum redux state for the app could consist of a single field, "milliseconds," to represent the clock's time. Since our clock needs to run forward, backward and be reset, we created corresponding redux actions to increment, decrement and reset the milliseconds field in state. We then jumped boldly into some saga code, creating a saga that listens for three saga actions (start, pause and rewind) and just logs them out when it receives them. Eventually, our plan is to replace this logging of actions with a process that runs the clock, sending out a decrement action every second, for example, and thus side-effecting the app state in a strictly defined way. Finally, we created a plain-jane React component to let us test drive our redux and saga actions and see that the state updates accordingly and our saga code works as expected.

Now we're going to finish this thing. We want it to look kinda cool so it impresses our friends and is fun to look at and engaging and shit, so we'll use some cool SVG techniques to make the clock draw itself as its time increments. But most importantly, we'll use Saga to make our app sing and dance. Let's start with that.

Implementing the saga

Our duck.js module is pretty minimal, and we actually only need a few more lines of code to finish the saga/redux side of our app. So far, in the "saga" part of our duck, we have this:

export function* rootSaga () {  
  yield takeLatest(['start-clock', 'pause-clock', 'rewind-clock'], handleClockAction)
}

function* handleClockAction ({ type }) {  
  console.log('Pushed this action to handleClockAction: ', type)
}

As we discussed in the previous post, takeLatest will listen for an action type or array of action types and run another process (in our case, handleClockAction) each time an action with a matching type is fired. It will also cancel any running process it had previously started. This is important to remember for later on. Also, takeLatest passes the full action object, which is why we can use that in handleClockAction.

Now let's think about how this thing should work. All the excitement is really going to be in handleClockAction, and we will want to do something different for each of our three saga actions. Let's stub out that basic structure in the code. Replace the single line in handleClockAction with an if/else if block that hits a different branch of the if/else if for each action type and then logs out some arbitrary text to prove that we hit the right code.

I'll give you a second on that...

...still waiting patiently...

..alright, hurry it up!

Done? OK, good. Here's what my handleClockAction looks like:

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    console.log(`Received ${type}. We need to run the clock forward here.`)
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  } else if (type === 'pause-clock') {
    console.log(`Received ${type}. Guess what needs to be done here?`)
  }
}

Now run the app (npm start), go to localhost:8080, open up the javascript console and test out the saga actions (they're wired up to those buttons underneath the SVG). We see the appropriate message logged out for each action. Sweet. So now we just need to figure out what to do when we receive each action.

When we get the start clock action, we want to increment the clock every 100 milliseconds. For that, we can pull in a little helper from saga called "delay." Edit your import from redux-saga so it looks like this:

import { delay, takeLatest } from 'redux-saga'  

Delay is so simple as to be self-explanatory. It takes one argument, number of milliseconds, and then pauses execution for that amount of time, a la yield delay(50). Let's add some logic to the branch of our if/else if that logs to the console every 1000 ms. Remember that you want this to repeat indefinitely. We can use a simple, native javascript control flow structure in order to implement this...I'll let you guess what it is.

No peeking.

Take a minute and try some ideas out.

OK, Here's what I have:

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      console.log(`Received ${type}. We need to run the clock forward here.`)
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  } else if (type === 'pause-clock') {
    console.log(`Received ${type}. Guess what needs to be done here?`)
  }
}

At this point, it's incredibly informative to go to your browser with the javascript console open and play with the saga actions, looking at what gets logged out. You'll see that the start clock action does indeed pause for 100 ms then log something out then repeat indefinitely. Also, notice the very crucial fact that whenever our takeLatest in the root saga receives a new matching action, it cancels the currently running handleClockAction. Since we need to do nothing but cancel any current clock action when the pause clock action is received, guess what we can do? Delete some code! Pause clock will automatically work because takeLatest cancels the current handleClockAction, stopping the clock whether it's running forward or backwards. Make sure you understand this key feature of takeLatest. Make the appropriate changes in handleClockAction. You should now have something like this:

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      console.log(`Received ${type}. We need to run the clock forward here.`)
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  }
}

Now let's get really wild and actually dispatch an action within the while loop. Whoa.

(Side note: we aren't actually dispatching an action, we're just requesting that the middleware do so. But don't worry about that just yet, I'll explain it later.)

To dispatch an action within a saga, we'll use the put side effect. Add the following line to the top of your duck file:

import { put } from 'redux-saga/effects'  

To request that an action be dispatched with put, we just use this simple syntax: yield put({ type: 'hi', data: 'I am an action' }). That means, of course, that we can invoke an action creator that returns a plain object within the put like this yield put(someAction()). (Function arguments are evaluated immediately in javascript.) Hey, remember those redux actions we created way back at the beginning of the app? Let's use one. Go to the while loop in handleClockAction and give put a test drive:

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      yield put(incrementMilliseconds())
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  }
}

Go try it in your browser. Notice that the clock time shown underneath the SVG is actually incrementing. Funk yeah.

To run the clock backwards, we can reuse most of the code from start clock. Go ahead and try it out.

Did you write the code? Is it working? Toil away a bit longer if you need to.

Here's what you should have:

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      yield put(incrementMilliseconds())
    }
  } else if (type === 'rewind-clock') {
    while (true) {
      yield delay(1000)
      yield put(decrementMilliseconds())
    }
  }
}

Now go test out all the saga actions and make sure everything behaves correctly. If it doesn't, that means you're a failure as a developer. Just kidding, of course :-) In all seriousness, if you get stuck at any point on this or any other article on my blog, leave a comment, and I'll be happy to help you out.

This is a great moment to play with the saga code and learn by experimentation. One hint: delay is blocking, while some other effects are not. Try your hand at a for loop. Or maybe change the while condition to be false at some point. We've abandoned the world of promises, and now we can express all of our control flow with these simple, predictable, native javascript structures.

OK, undo any experimental changes in your saga code so that it looks like my code snippet above again.

So with that, our saga code is fully complete and operational. (Note: in the final, completed repo, I've reorganized the code a bit to make it read more clearly.)

Saga's approach to side-effects

OK, now that we've gotten some work done, let's talk about how the code works. The core idea in Redux Saga is this: we don't actually perform side-effects like dispatching to the store in our code. Instead, we yield descriptions of side-effects to the saga middleware, and the middleware performs them. Essentially, we are giving instructions back to Saga telling it what we want to happen. These side-effect descriptions are all plain objects that conform to the standard generator output of { value: any, done: boolean }, like we discussed in the first post of this series. For example, yield put sends a plain object back out to the middleware containing an action object for the middleware to dispatch for us. Defining our async flows this way has some big benefits, one of them being testability. We can simply assert that our sagas are yielding the correct side-effect descriptions back to the middleware at each of their steps. (Keep your eye out for a future post that covers saga testing in-depth.)

That said, we should make one tiny update in the interest of following redux-saga best practices: add call to your imports from redux-saga/effects and change the yield delay(100) to be yield call(delay, 100). Since we want to only yield side-effect descriptions back to the saga middleware, saga provides the call effect to basically convert non-effects into effects. Instead of just yielding to delay itself (a promise), we convert delay to an effect via call and yield that. We should give the same treatment to anything we yield within saga code that isn't already a saga effect (anything not imported from redux-saga/effects). The syntax for call is just the function to invoke and the list of arguments to pass to it. For example: yield call(fetch, url, options). Lastly, note that call can be used for promises, generators or functions.

make this thing not so ugly

We need to create a file that will serve as the config for our app, holding some arbitrary constants. Why not call it "config.js"? Create the file in src/, and paste in these contents:

const MAX_RADIUS = 40

let hands = [  
  { ms: 144000, maxTicks: 1 },
  { ms: 36000, maxTicks: 4 },
  { ms: 12000, maxTicks: 3 },
  { ms: 2000, maxTicks: 6 },
  { ms: 400, maxTicks: 5 },
  { ms: 100, maxTicks: 4 }
]

export const STROKE_WIDTH = MAX_RADIUS / hands.length

hands = hands.map((hand, idx) => {  
  const radius = STROKE_WIDTH * (hands.length - idx)

  return {
    ...hand,
    radius,
    circumference: 2 * Math.PI * radius,
    alpha: 1 - idx / hands.length
  }
})

export const CLOCK_HANDS = hands

export const MINIMUM_MS = 100  

OK, so what's going on here? Let me explain. It's a bunch of constants and calculations that will determine how exactly our SVG looks. Happy? Good. (You can dissect this on your own if you wish, but I won't get into it here since it's a little too off topic.)

OK, so now our saga code is in place and we've defined all of the configuration needed for our SVG. Let's finish off app.jsx with some updates to make the SVG more interesting.

FINISH HIM

Here's what we have currently in app.jsx:

import React from 'react'  
import { connect } from 'react-redux'

import { incrementMilliseconds, decrementMilliseconds, resetClock, startClock, pauseClock, rewindClock } from 'duck'

class Clock extends React.Component {  
  render () {
    const {
      milliseconds,
      incrementMilliseconds,
      decrementMilliseconds,
      resetClock,
      startClock,
      pauseClock,
      rewindClock
    } = this.props

    return (
      <div>
        <svg onClick={ incrementMilliseconds } onDoubleClick={ resetClock } onMouseLeave={ decrementMilliseconds }
          className="clock" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="500">
          <circle cx="50" cy="50" r={ 30 } stroke={ 'rgba(1,1,1,1)' } fill="orange" />
        </svg>
        <p>{ milliseconds }</p>
        <p>
          <button type="button" onClick={ startClock }>Start Clock</button>
          <button type="button" onClick={ pauseClock }>Pause Clock</button>
          <button type="button" onClick={ rewindClock }>Rewind Clock</button>
        </p>
      </div>
    )
  }
}

export default connect(state => ({  
  milliseconds: state.milliseconds
}), ({
  incrementMilliseconds,
  decrementMilliseconds,
  resetClock,
  startClock,
  pauseClock,
  rewindClock
}))(Clock)

We need to pull in our exports from the config and pass them as props to the component. While we're at it, we can delete some unneeded imports from the duck. We're no longer going to call certain actions directly, since they will only be fired within our saga. Update the top of your file like so:

import React from 'react'  
import { connect } from 'react-redux'

import { startClock, rewindClock, pauseClock, resetClock } from 'duck'  
import { CLOCK_HANDS, STROKE_WIDTH } from 'config'

So now we have all the data we need to make pretty circles within our SVG. Essentially, we are going to use a series of concentric circles to represent arbitrary hours/minutes/seconds (but with the arbitrary definitions from our config), and change the "length" of each circle so that it increases from 0 to 100% as time passes. To do the SVG drawing, we use a simple little trick explained in this CSS Tricks blog post.

We're no longer going to pass the milliseconds field from state as props. Instead, on every update to milliseconds, we're going to calculate the position for each clock hand. This is just some boring ol' mathy stuff. Basically, we iterate over each hand, largest to smallest, and put as much time on it as we can, and then repeat the process on the next hand, passing the remaining time. The smallest hand corresponds to our smallest unit of precision, 100 ms (MAXIMUM_MS in config.js). Update your connect call to look like this:

export default connect(state => {  
  const currentTime = state.milliseconds
  let remainingTime = currentTime

  const getTicks = (hands, timeRemaining) => {
    let [hand, ...tailHands] = hands
    hand.ticks = Math.floor(timeRemaining / hand.ms)
    return tailHands.length ? [hand, ...getTicks(tailHands, timeRemaining % hand.ms)] : [hand]
  }

  const hands = getTicks(CLOCK_HANDS, remainingTime)
    .map((hand, idx) => {
      const offset = state.milliseconds >= hand.ms ? 1 : 0
      const position = hand.circumference - ((hand.ticks + offset) / hand.maxTicks * hand.circumference)

      return {
        ...hand,
        position
      }
    })

  return {
    hands
  }
}, ({
  startClock,
  rewindClock,
  resetClock,
  pauseClock
}))(Clock)

Now we need to pull in the new hands prop in our component. Update the destructured assignment of props in the render method:

render () {  
  const {
    hands,
    startClock,
    rewindClock,
    resetClock,
    pauseClock
  } = this.props

For the grand finale, we'll make one final change. We're going update our SVG to start the clock onMouseEnter, rewind onMouseLeave, and pause onClick. Go ahead and plug in those changes.

I'll wait. I need to go get some vodka. Tonight, I'm side-by-side tasting Norseman and Tattersall vodkas. brb

Mmmmmm....de-lish. The Tattersall vodka has a slightly creamy flavor along with a subtle sweetness, and a more prominent "boozy" character. The Norseman is still much smoother, if not overly minimal for some tastes, with its own sweet undertone and crisp bite with a faint hint of citrus.

OK, you got those changes done? Good. Now we're going to create the concentric circles within the SVG. We need to iterate hands and create a <circle/> SVG element for each one, using our calculated values to set the radius, circumference, position and transparency for each hand. If you fancy yourself an SVG wizard, go ahead and make these changes on your own.

Here's our final update. This is what you should be returning from render():

return (  
  <svg onMouseEnter={ startClock } onMouseLeave={ rewindClock } onDoubleClick={ resetClock }
    onClick={ pauseClock } className="clock" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="500">
    { hands.map((hand, index) => {
      const { radius, circumference, position, alpha } = hand
      return (
        <circle key={ index } cx="50" cy="50" r={ radius } stroke={ `rgba(1,1,1,${alpha})` } fill="none"
          strokeWidth={ STROKE_WIDTH } strokeDasharray={ circumference } strokeDashoffset={ position } />
      )
    }) }
  </svg>
)

Shit, I forgot something. Go over to duck.js, and import MINIMUM_MS from config:

import { MINIMUM_MS } from 'config'  

Now replace the 1000 in the delay calls with MINIMUM_MS. The clock is going to tick every 100 ms.

function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(MINIMUM_MS)
      yield put(incrementMilliseconds())
    }
  } else if (type === 'rewind-clock') {
    while (true) {
      yield delay(MINIMUM_MS)
      yield put(decrementMilliseconds())
    }
  }
}

See how easy it is to change control flow logic in saga?

Whip Out Your Clock

Now let's give this thing a whirl. npm start and open up localhost:8080 in your browser.

demo of app

There you have it. A working clock that we can pause, resume, run forwards and backwards and reset. And it's even kind of cool looking.

The End

Thanks for reading this tutorial! We covered the basics of javascript generators and Redux Saga in the context of a small demo app. I hope I've helped you understand how to use Redux Saga, as well as the benefits and reasoning behind it.

If you want to learn more about Redux Saga, here are a handful of great tutorials you can check out. These helped me immensely when I was first starting to learn saga:

I'd also like to credit the authors of the following articles that demystified generators, whose crystal clear, insightful explanations inspired me to write this series, and without which I would probably still be trying to figure out what the fuck a generator is:

Thanks for reading! See you next time.

comments powered by Disqus