I was recently given a technical test which required the use of Redux-Saga. Prior to this I’d had some experience using Redux but sagas were a new concept to me. After a run through of the beginner tutorial in the docs and a look from some example code I attempted the test was some success. This blog will attempt to solidify what I have learnt by building a small project.
What even is Redux Saga?
Redux-Saga is a Redux middleware that attempts to manage side affects in a simple and testable way. It takes advantage of generator functions which were made possible in JavaScript ES6, to enable asynchronous code which can be tested as easily as synchronous code.
The Project
The project we’re building is a React app which renders a randomly generated picture of a cat alongside a quote every time the user clicks on a button.
See the finished app here:
Getting Started
This repo is available here if you want to code along, clone it down and run npm install. This will give us a base React app with some additional styling. Alternatively, set up a new React app by running npx create-react-app catsandquotesand implement your own styling. Once the React has finished setting up move into the directory and start the app cd catsandquotes && npm start.
Once you have a React app up and running install the dependencies with the following:
npm i redux react-redux redux-saga
These are all of the dependencies we will need for this project.
Actions
mkdir src/store && touch src/store/actions.js
Let’s start with the actions, as these will be frequently referred to throughout the app. Start by creating a store directory inside src and inside this create an actions.js file.
The contents of this file is shown below. We have three actions API_REQUEST API_SUCCESS and API_FAILURE, by declaring these as constants we protect ourselves against typos later on. We also create three corresponding helper functions which return our actions formatted for correctly for Redux to consume.
Reducer
touch src/store/reducer.js
The reducer is going to manage the application state. It will be responsible for setting the initial state, as well as updating and returning state. We’ll start by creating a reducer.js file inside the store directory, importing our actions and setting the initial state:
import { API_REQUEST, API_SUCCESS, API_FAILURE } from './actions';
const initialState = {
catImageUrl: '',
quoteText: '',
fetching: false,
error: null
};
Then we set up the reducer itself. We have three options, plus the default which returns the state unchanged.
API_REQUEST: any time we make a request to the API we call the API_REQUEST action which sets fetching to true, and error to null (in case there is a previous error still in state).
API_SUCCESS: if our API call is successful we call the API_SUCCESS action which resets our fetching state to false sets the catImageUrl and quoteText returned from the API’s.
API_FAILURE: should there be an error with the API call, the API_FAILURE action will reset fetching to false and return the error message.
Saga
touch src/store/saga.js
Now onto the crux of the project, the saga! This will be responsible for making our API calls and handling the success or failure of this.
Add the following imports to the top of the file, we’ll take a closer look at call, put and takeLatest further down.
import { apiSuccess, apiFailure, API_REQUEST } from './actions';
import { call, put, takeLatest } from 'redux-saga/effects';
We’ll start by writing our API request functions, I’m using the thecatapi.com for the cat images and ron-swanson-quotes.herokuapp.com for the quotes. We’re using simple async/await functions for this.
const catFetch = async () => {
const res = await fetch('https://api.thecatapi.com/v1/images/search');
const data = await res.json();
return data[0].url;
};
const quoteFetch = async () => {
const res = await fetch('https://ron-swanson-quotes.herokuapp.com/v2/quotes');
const data = await res.json();
return data[0];
};
Next we have our API saga function. This is a generator function which is going to do all of the heavy lifting for us. We define a generator function by adding an asterisk (*) at the end of the function keyword. It’s worth noting here that we cannot define generators with the arrow function syntax.
function* apiSaga() { ... }
We wrap the saga in a try-catch block to enable us to easily handle any errors that may arise.
try { ... } catch (error) { ... }
Inside the try block we perform the API fetches then call the API_SUCCESS action.
try {
const catImageUrl = yield call(catFetch);
const quoteText = yield call(quoteFetch);
const payload = { catImageUrl, quoteText };
yield put(apiSuccess(payload));
}
Here the first line is calling the catFetch function and saving the return value to a const.
“The yield keyword is used to pause and resume a generator function” — MDN Web Docs. This tells our saga to pause whilst we perform the asynchronous API call and continue when we have a response.
call is part of the Redux-saga API. It “creates an Effect description that instructs the middleware to call the function” — Redux Saga Docs. Simply, it tells our saga to call the catFetch function.
The second line is the same as the first but calling the quotes API. And the third line creates a payload object using ES6 object shorthand.
The final line of our try block uses the Redux-saga put method which “instructs the middleware to schedule the dispatching of an action to the store.” — Redux Saga Docs. We’re telling the saga to call the Redux API_SUCCESS action with payload from the API calls.
catch (error) {
yield put(apiFailure(error));
}
If there is an error without API fetches we call the Redux API_FAILURE action and pass the error as the payload.
export function* rootSaga() {
yield takeLatest(API_REQUEST, apiSaga);
}
The final part of out saga file is the rootSaga generator. The root saga is responsible for starting all of our sagas (in our case we only have one) and allows us to export just one saga. We would see the real benefit of this if we had multiple sagas being defined and exported.
Notice that we are using takeLatest here, this “forks a saga on each action dispatched to the Store that matches pattern. And automatically cancels any previous saga task started previously if it's still running.” — Redux Saga Docs. It prevents the same saga from being multiple times simultaneously, by cancelling any previous instances every time it is called.
Full code for src/store/saga.js below:
Creating a Store
touch src/store/index.js
It’s time to bring all of these elements together to build and export our Redux Store. We start with our imports, the reducer and rootSaga we previously created and the rest we’ll cover when we implement them.
import createSagaMiddleware from 'redux-saga';
import { createStore, compose, applyMiddleware } from 'redux';
import { reducer } from './reducer';
import { rootSaga } from './saga';
If you don’t already have Redux DevTools installed on your browser head over to extension.remotedev.io. These will greatly help with debugging, and give a great insight into the Redux process in your app.
const reduxtools =
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const sagaMiddleware = createSagaMiddleware();
The first line checks if DevTools are installed, and if so invokes them. The second line calls on the createSagaMiddleware function to create a Redux middleware and connect our saga to the Redux Store.
export const store = createStore(
reducer,
compose(applyMiddleware(sagaMiddleware), reduxtools)
);
sagaMiddleware.run(rootSaga);
Finally it’s time to create our Store and start our saga middleware. createStore first takes in our reducer and secondly takes an enhancer. We want to pass in two enhancers — our middleware, and the devtools, so we can use the Redux compose function two pass in multiple options. Inside compose we pass the Redux applyMiddleware function which will connect our saga to the Redux Store.
The final line here calls run on our saga middleware and passes in our saga.
Full code for src/store/index.js below:
Bringing it all together
The final thing we need to do is connect our Redux Store to our React app.
First we update src/index.js by importing Provider from react-redux and the Store we just created. Wrap our App component with the Redux Provider and pass in the store we created.
The final part of the puzzle is adding Redux to our App component. We’ll use React Hooks to set Redux in our app.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { apiRequest } from './store/actions';
Start by importing useEffect from ‘react’ — this is the hooks equivelant of componentDidMount, useSelector and useDispatch from ‘react-redux’ and our apiRequest action function.
The hooks implementation of Redux is much cleaner and more precise than it previously was. We can bring in our state and dispatch in just two lines:
const { catImageUrl, quoteText } = useSelector(state => state);
const dispatch = useDispatch();
const handleClick = () => dispatch(apiRequest());
The first line uses ES6 syntax to extract catImageUrl and quoteText from the state object provided by useSelector. The second line set up our Redux dispatch function. The last line passes our apiRequest action to the dispatch function inside a handleClick function.
useEffect(() => {
dispatch(apiRequest());
}, [dispatch]);
return (
<div className="container">
<h1>Cats + Quotes</h1>
<div className="row">
<img src={catImageUrl} alt="A cat" />
</div>
<blockquote>{quoteText}</blockquote>
<button onClick={handleClick}>Gimme more...</button>
</div>
);
We’ll also pass the dispatch to useEffect to make sure an API request is made as soon as we load the page. Finally, we return the contents of the App component, passing the handleClick function to our ‘more’ button so the user can load a new picture and quote.
The full App component is shown below.
That’s our Cats and Quotes app complete. Some missing features that I will look to work on in future include handling errors inside the app, as well as some testing of both the React app and the sagas.
Thanks for reading…
Top comments (1)
Thanks for this tutorial, it was really helpful!