React, Redux, and Sagas

At first redux and redux-saga may feel quite complicated and may seem like a lot of work to get one api call to work. And yes, it is quite a few lines of code, but what you get in return is a stable system for handling your backend calls synchronously or asynchronously, giving you full control without having to loop through complicated and ugly callback functions.

The Principle

What you get with redux and redux-saga is a store that contains all of your data at the root of your application. This can then be accessed from anywhere you want it. This is achieved through something called actions and reducers.

If you consider of the term “saga” you may think of stories you were told as a child.  The story of Jack and Jill is a good example:

Jack and Jill went up the hill
To fetch a pail of water
Jack fell down and broke his crown,
And Jill came tumbling after

Jack and Jill going up the hill to fetch some water may be considered the action, the the pail of water is a piece of your store being called through your api. Unfortunately, in this case, something goes wrong in the saga at this point: Jack falls down. Or to put it in our terms, the call failed, sending Jack down the hill breaking his crown and Jill, for some reason or other, picks up on that action and comes tumbling after.

This may not be a perfect simile, but it sort of explains the jist. Read on and see if you can spot it.

Pieces of the puzzle…

The Store

As mentioned, the store is at the root of the application and contains all of the data from your api calls and any other data that you wish to place there.

Let’s say that Jack and Jill were fetching water so that their mother could prepare some tea and cookies. All of the pieces needed for the story to work, or for mum to be able to make tea and cookies is stored in the store.

The store keeps track of the state of the application. For example, at the beginning of the story the pail of water is empty, and Jack and Jill are not going up the hill. Later, however, mum gives her children the action to fetch water, then the pail is filled and Jack and Jill are on the top of the hill. The store keeps track of all of this.

Here is an example of what a store could look like:

import 'babel-polyfill';
import { createStore, applyMiddleware, compose } from 'redux';
import { createLogger } from 'redux-logger'
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
import reducer from './redux';

export default function createStoreWithMiddleware(history, data) {
  const sagaMiddleware = createSagaMiddleware();
  const loggerMiddleware = createLogger();
  const middleware = [sagaMiddleware, loggerMiddleware];

  const store = createStore(reducer, data, compose(
    applyMiddleware(...middleware)
  ));

  sagaMiddleware.run(rootSaga);

  return store;
}

Notice that the store imports the rootSaga and reducers and uses them both to build the store.

The store is initiated with your provider thusly:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from "react-redux";
import store from "./business_logic/store";
import App from './App';
import './styles.css';

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

This is so that your entire application will be aware of the store.

The Actions

An action is pretty much just a string that gets sent to the store which a saga event listener picks up on when it “hears” it. Much in the same way that Jill falls down the hill because something went wrong with Jack.

As a standard there are three types of actions. There is the starter event, for example “GET_SOMETHING“, the success event, “GET_SOMETHING_SUCCEEDED“, and the fail event, “GET_SOMETHING_FAILED“.

Here is a practical example of an actions event set:

export function fetchWater(child1, child2) {
  return { type: "FETCH_WATER", child1: "Jack", child2: "Jill"};
}
export function fetchWaterSucceeded() {
  return { type: "FETCH_WATER_SUCCEEDED" };
}
export function fetchWaterFailed() {
  return { type: "FETCH_WATER_FAILED" };
}

The Reducers

A reducer is a function which tells the store what state properties should be updated on a particular event. For example, if Jack and Jill managed to get the water down the hill, a success action would be triggered and the water would be saved to wherever they were bringing it.

This corresponds to the set of action events:

export default function(state = {
  waterPailContents: [],
  fetching: false,
  fetched: false, 
}, action) {
  switch (action.type) {
    case "FETCH_WATER": {
      return { ...state, fetching: true, fetched: false, waterPailContents: null }
    }
    case "FETCH_WATER_SUCCEEDED": {
      return { ...state, fetching: false, fetched: true, waterPailContents: action.waterPailContents }
    }
    case "FETCH_WATER_FAILED": {
      return { ...state, fetching: false, fetched: false, waterPailContents: null, error: action.message }
    } 
    default: return state;
  }
}
Combine reducers

If we’d had more actions and reducers, such as one for fetching flour, we would need to combine them. This would typically be done in an index file in the actions_reducers folder.

import { combineReducers } from 'redux';

import water from './water';
import flour from './flour';

export default combineReducers({
  water,
  flour
});

The Saga

It is inside the saga that you make your actual api call and where you apply any other logic that needs to be applied to your data before it goes into the store.

Sagas use generator functions which execute your code synchronously. It is the yield command that allows this, since the code will not continue to execute until the command has been resolved.

There are two types of functions used in sagas, there are the watcher functions, and the worker functions.

This is what a typical watcher function looks like:

export function* watchGetWater() {
  yield takeEvery('FETCH_WATER', getWater);
}

This function, much like Jack and Jill were doing, listens to see if the action is fired, and then executes the function getWater.

The getWater function in this case is a worker function.

function* getWater(action) {
  try { 

    const response = yield call(axios.get, '/api/getmywater?child1=' + action.child1 + '&child2=' + action.child2);
    yield put({type: 'FETCH_WATER_SUCCEEDED', waterPailContents: response.data});
    console.log('Mum is happy and in a little while everybody gets tea and cookies! :D ');

  } catch (err) {

    yield put({type: 'FETCH_WATER_FAILED', message: err.message});
    console.log('Jack falls down and breaks his crown!!!');

    yield put({type: 'INVOKE_CONSEQUENT_ERROR', message: err.message});
    console.log('Oh no! Jill tumbles down the hill!!!');

  }
 }
A root saga

Creating a root saga is imperative. It combines the watcher functions so that the one store can keep track of them all.

import { watchGetWater } from './waterSaga';
import { watchGetFlour } from './flourSaga';

export default function* rootSaga() {
  yield [
    watchGetWater(),
    watchGetFlour()
  ]
};

Bind Action Creators

Now we’re getting to the part where we tie it all together in our application.

Let’s say that Mum is our App. We have several components, we have a Teapot for example, and a MixingBowl, and so on and so forth. The Teapot and the MixingBowl are not very smart, all they know is that they need water in order to function, but they have no idea that Jack and Jill can fetch water. They don’t even hear when Mum calls out the window while Jack and Jill are playing in the back yard and tells them to fetch water. Only Mum knows about that because she is a smart component.

It is the Mum component that keeps track of everything. She knows what actions need to be made in order to make tea and cookies and she can send the commands on to her children.

This is what Mum might look like:

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { fetchWater, fetchFlour } from './business_logic/redux/ingredients'
import { combineIngredients, putInOven } from './business_logic/redux/bakeCookies'

import Teapot from './components/Teapot';
import MixingBowl from './components/MixingBowl';

function mapStateToProps(store) {
  return {
    waterPailContents: store.ingredients.waterPailContents,
    flour: store.ingredients.flour,
    ingredients: store.cookies.ingredients,
    oven: store.cookies.oven
  };
}

function mapDispatchToProps(dispatch) {
  return { actions:bindActionCreators({
    fetchWater,
    fetchFlour,
    combineIngredients,
    putInOven
  }, dispatch) };
}

class App extends Component {

  constructor(props) {
    super(props);
    this.state = { childOne:"Jack", childTwo:"Jill" };
  }

  callFetchWater() {
    this.props.actions.fetchWater(this.state.childOne, this.state.childTwo);
  }

  render() {

    return (
      <div className="App">
        <h1>Welcome to Mum's kitchen</h1>
        <section className="content">
          <p>We are making tea and cookies.</p>
        </section>
        <button onClick={this.callFetchWater}>Tell Jack and Jill to get water</button>
      </div>
    );
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Mum);
What’s going on here is that Mum imports all of the actions/reducers needed to make tea and cookies…
import { fetchWater, fetchFlour } from './business_logic/redux/ingredients'
import { combineIngredients, putInOven } from './business_logic/redux/bakeCookies'
She then connects them…
function mapStateToProps(store) {
  return {
    waterPailContents: store.ingredients.waterPailContents,
    flour: store.ingredients.flour,
    ingredients: store.cookies.ingredients,
    oven: store.cookies.oven
  };
}

function mapDispatchToProps(dispatch) {
  return { actions:bindActionCreators({
    fetchWater,
    fetchFlour,
    combineIngredients,
    putInOven
  }, dispatch) };
}

...

export default connect(mapStateToProps, mapDispatchToProps)(Mum);
and when we hit the button the callFetchWater function is invoked wish fires the action:
callFetchWater() {
  this.props.actions.fetchWater(this.state.childOne, this.state.childTwo);
}

She had no idea that Jack would fall down or that Jill would come tumbling after, but handling such an event is an article for another day.

To wrap up…

These are the steps required to get a whole saga going:

  1. Create a set of actions – send arguments to the starter action
  2. Create a reducer for those actions – get arguments from “actions” and add them to the success or fail function
  3. Add the reducer to the combined reducers if you have one, usually the reducer index file
  4. Create a watcher saga function
  5. Create a worker function – values are accessed through the “actions” argument
  6. Add the watcher function to the saga index file
  7. Add the action function created in the first step to your root app component, or the “smart” component where you want it to be stored
  8. Map the action to the dispatcher (in mapDispatchToProps)
  9. Make sure to map the state produced by the reducer to the app’s properties (in mapStateToProps)
  10. Invoke the function wherever you need it, use this.props.actions.whateverYourActionFunctionIsCalled

Find more information

For more information, check out these great video series on redux and redux-sagas:

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *