Redux Saga: Hello, World!

Simplified Redux Saga Boilerplate

(This is the second post in a three-part series on Redux Saga. The first post is here, and the third and final post is here.)

In this post, we'll walk through the structure of a basic React/Redux/Redux Saga app and the reasoning behind it.

I've created a boilerplate version of the final app as a starting point so that we don't have to go through the chore of setting up the development toolchain for the app from scratch since that isn't the point of this series. (I'm assuming you have a basic working knowledge of React, Redux and the typical dev tools that accompany them.) That said, I will still highlight a few things briefly so that you have a basic understanding of the dependencies and configuration, and also because you might be a complete and utter n00b. (It's OK if you're a n00b.) If you don't care about this, scroll down to the picture of Patrick Stewart and Ian McKellen and Elmo.

First step, clone the repo and install the dependencies:

git clone https://github.com/granmoe/redux-saga-clock-tutorial.git  
cd redux-saga-clock-tutorial  
npm i  

Now crack open the project in your favorite editor, and let's look at what we've got.

Everything is pretty standard in our package.json, but note that we must have babel-polyfill in case any browsers we are targeting don't support ES2015 generators. And babel-polyfill must be imported before redux-saga in our app.

You'll also notice that I've got a slew of ESLint deps in the package.json because I find it to be an invaluable dev tool.

Here's our babel config, found in the .babelrc:

{
  "presets": ["es2015", "react", "stage-2"]
}

I've decided I want to be cool and use es2015, react, and stage-2 and up features in my code.

I'd walk you through the .eslintrc, but I don't want to put you to sleep.

That leaves a very minimal webpack config and index.html, which I don't think anyone here is incredibly excited about.

That was BORING AS F***. Let's move on to the interesting stuff.

Kicking Things Off

Patrick Stewart and Ian McKellen and Elmo

Our app's entry point is main.jsx:

import 'babel-polyfill' // generator support  
import React from 'react'  
import ReactDOM from 'react-dom'  
import { Provider } from 'react-redux'

import App from 'app.jsx'  
import initStore from 'store'

const store = initStore()

ReactDOM.render(  
  <Provider store={ store }>
    <App />
  </Provider>,
  document.getElementById('app')
)

Here we pull in some deps (including the babel-polyfill mentioned earlier), import our root component, import our redux store config, instantiate the store, and use ReactDOM to render the root component into our "app" div wrapped with the Provider class so that any react components in our component tree will have access to our store instance.

Check out store.js and we can see how to set up our store to work with the saga middleware:

import { applyMiddleware, createStore } from 'redux'  
import rootReducer, { rootSaga } from 'duck'  
import createSagaMiddleware from 'redux-saga'

export default function () {  
  const sagaMiddleware = createSagaMiddleware()

  const store = createStore(
    rootReducer,
    applyMiddleware(sagaMiddleware)
  )

  sagaMiddleware.run(rootSaga)

  return store
}

First we create an instance of the middleware with createSagaMiddleware. Next, we pass our root reducer and our middleware into the call to createStore, which gives us our redux store instance. Then, and only then, will we pass our app's root saga into the saga middleware. This must be done after instantiating the redux store. Don't even try doing it before. rootSaga is the top-level generator that delegates to all of our other generators. (More on that in a minute.)

Show Me Some Code Already, You Jerk

So now we have the fun stuff. Our app code basically lives in two files. "app.jsx" is a react component that will return some markup to be rendered based on the app state and fire actions on DOM events. "duck.js" contains plain object actions and a reducer function that together describe how to modify state. It also contains all of our control flow code, stuff that describes the process of our app. If you're familiar with the standard ducks pattern, our only modification to that is to include saga code in the duck. Let's get working on the duck module.

We're going to make a controllable clock. Let's start by determining what is the minimum state needed to represent our clock. The only question we need to ask about our app's state at any given moment is, "What time is it?" All we really need for this is one field which will store the time as a single number. Now how will we need to modify this state? Well, we are going to make a clock that the user can run forward, backward, pause, and reset. This means our field representing the time can increment, decrement, do nothing, and reset to zero. Doing nothing doesn't require a state change, so we are left with increment, decrement and reset. And we'll keep track of time in terms of milliseconds, thus I'm calling our field in the app state "milliseconds."

As I said, I'm using the ducks pattern for the redux code in this demo, and if you don't like it, then you can go ahead and split it all up into three files like a dummy.

Let's look at the first part of duck.js, up until the line that says // saga actions. Here it is:

import { takeLatest } from 'redux-saga'

const initialState = {  
  milliseconds: 0
}

export default function reducer (currentState = initialState, action) {  
  switch (action.type) {
    case 'reset-clock':
      return {
        ...currentState,
        milliseconds: 0
      }
    case 'increment-milliseconds':
      return {
        ...currentState,
        milliseconds: currentState.milliseconds + 100
      }
    case 'decrement-milliseconds':
      if (!currentState.milliseconds) { return currentState }

      return {
        ...currentState,
        milliseconds: currentState.milliseconds - 100
      }
    default:
      return currentState
  }
}

// actions
export const resetClock = () => ({ type: 'reset-clock' })  
export const incrementMilliseconds = () => ({ type: 'increment-milliseconds' })  
export const decrementMilliseconds = () => ({ type: 'decrement-milliseconds' })  

Notice how simple this is. We have our initial state which just has our one field. Then we have a reducer which will actually handle actions and create the new state with the appropriate modification based on the action type. Finally, we are exporting some plain object actions that can be called from elsewhere in the app. (We also have an import for saga that we'll get to in a second.) Ah, example apps are always so clean...so...clean...

Now we need to describe the flow of our app. As a process, what states can the app (or a clock) be in? Another way to ask this question is: What can our app be doing at any given moment? Our clock can be running forward, running backwards, or paused. In order to transition between these, we need three actions: start the clock, rewind the clock, pause the clock.

Look at the remainder of our duck module, starting at the // saga actions line. We've created three actions, and our root saga is starting a new dummy process each time it receives one of these. The dummy process just prints out the action name for now. Later on, we'll start a process to either increment, decrement or idle the clock based on the action type. Here's the saga code from duck.js:

// saga actions
export const startClock = () => ({ type: 'start-clock' })  
export const pauseClock = () => ({ type: 'pause-clock' })  
export const rewindClock = () => ({ type: 'rewind-clock' })

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

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

The actions (I should be saying "action creators" to follow strict terminology, but whatever) should look familiar as they are just like any other redux actions. However, these actions do not get handled in our reducer. It can be nice to keep these actions that only trigger sagas near the saga code where they are used, and to avoid mixing responsibilities between these kinds of actions and actions that modify state. Obviously, our saga actions can still be imported in components and bound to our store instance via connect(), as you'll see in a moment.

Now to explain the weird saga stuff in this file. You remember that rootSaga was passed to the saga middleware, right? Honestly, you probably don't, but that's OK. Every time we fire an action, that action is pushed into every generator we passed into the saga middleware via sagaMiddleware.run(generator). This means that every generator has a chance to respond to the action. In our case, rootSaga does nothing until it gets a matching action type. We are using the takeLatest helper from Redux Saga to accomplish this. This will take any action that matches the array of action types we're passing it and start a new handleClockAction process, passing the action to it. The takeLatest also means that if another matching action is received while handleClockAction is still running, the current handleClockAction will be cancelled before the new one is started. handleClockAction is essentially started in the background ("forked"), allowing rootSaga to keep working and receive the next matching action even if handleClockAction is still running.

Also, note that we use the yield keyword. Recall that yield is used to both send out and pull in values in a generator. Anytime we yield a Redux Saga helper or effect, we are communicating with the Saga middleware. In our example above, we're waiting for an action of the matching type to be sent in to our saga. We'll get into more depth on that later.

I hope that made at least a little bit of sense. I think you'll understand more clearly from testing out the code. So let's see how the user interacts with this madness by looking at our React component.

"app.jsx" is a very simple react component at this point. Let's read through it from the bottom up.

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)

By using the connect higher order component, we can grab our one field from the store state and pass it to the component as props. We also pass an object with all of our action creators here. Redux binds these to our store instance to make sure they are dispatched correctly when we call them in our component.

In our render, we are returning a <div> which first has an SVG (this will be critical later on). The SVG has some event handlers that will dispatch our state-modifying actions. Next we have a <p> that shows the current time based on our app state. Lastly, we have some <button>s that are wired up to our saga actions.

With all of this code in place, we can run the app and verify that our basic structure is solid and all actions are firing correctly.

Does It Work?

Go back to your terminal, and run npm start. Now go to localhost:8080 and open up the devtools in your browser, and check the javascript console. When you click the buttons, you'll see our saga actions logged out. Now try the click, mouse leave, and double-click actions on the SVG itself. You'll see the milliseconds text update accordingly.

screenshot

Hell yeah. We've created a boilerplate Redux Saga app structure, and learned how to use takeLatest. And that has allowed us to...log some stuff to the console! Woo!

In the next post, we'll finish the implementation of our clock and really get into some cool shit.

comments powered by Disqus