Skip to content

Instantly share code, notes, and snippets.

@nomrik
Last active April 19, 2023 20:18
Show Gist options
  • Save nomrik/6bfb6d814ad6eb5d1b15c3a4fe0df320 to your computer and use it in GitHub Desktop.
Save nomrik/6bfb6d814ad6eb5d1b15c3a4fe0df320 to your computer and use it in GitHub Desktop.
API requests flow with Redux Saga

API requests flow

Introduction

In Redux, API requests are considered side effects, outside the regular Redux flow. Thus, they are normally handled by a middleware. Out of the many libraries available, one of the most popular is Redux Saga. Redux Saga has two main advantages over the other options:

  • It is built around JavaScript's generator functions, which make it possible to write async logic in a way that is easy to reason about.
  • It is easy to test.
  • When using it, all of the Redux actions are always just plain objects, unlike other middleware libraries which require async actions to be functions. That way it is possible to keep the standard Redux Flow, dispatching just regular actions. If the action is supposed to trigger async logic, the middleware will handle it.

This document is meant to describe the way Redux Saga is incorporated into this project, and how API requests are made. It is recommended to be familiar with the basics of Redux Saga before reading this. We will go over Wiring up Redux Saga,

API requests infrastracture and configuration

The HTTP requests themselves are done with axios, as it is an HTTP library easy to configure, supports the Promise API that Redux Saga needs, with helpful abstractions over the JavaScript new fetch function.

The first part of the file is all of axios defaults configurations, such as baseURL, headers, etc. One important note is the use of an environment variable:

// Configuring the baseURL with an environment variable
axios.defaults.baseURL = process.env.REACT_APP_BASE_URL

This variable is defined in the .env file that is located in the root of the project.

// .env
REACT_APP_BASE_URL=https://base.url.com/api

This value can be overriden with an .env*.local file (which is not checked into git(, in case of a need to use a different backend during development.

// .env.development.local
REACT_APP_BASE_URL=https://my.custom.url.com/api

Please read the section about environment variables in create-react-app for a detailed explanation.

The second part of api.js is the requests functions. Each one of those function should get any parameters it needs (if any), perform the request and return the Promise from it. For example, the following function will return the Promise that is created by sending an HTTP request ${baseURL}/images?count=10.

// core/services/api.js

export function fetchImages({count}) {
    return axios.get('/images', {
        params: {
            count
        }
    })
}

Wiring up Redux Saga

In a similar way to the reducers, we have one rootSaga that is started during the store configuration, after Redux Saga has been applied as a middleware to our store.

// configureStore.js

import {createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga'

import rootReducer from 'core/reducers/rootReducer';
import rootSaga from 'core/sagas/rootSaga';

const sagaMiddleware = createSagaMiddleware();
const middlewares = [
    sagaMiddleware
];

const configureStore = () => {
    const store = createStore(rootReducer, applyMiddleware(...middlewares))
    sagaMiddleware.run(rootSaga);
    return store;
};

export default configureStore;

rootSaga is a saga that all its job is to run other sagas that are imported from the different modules of the app.

import {all} from 'redux-saga/effects';
import apiExampleSaga from './apiExampleSaga';

export default function *rootSaga() {
    yield all([
        apiExampleSaga()
    ]);
}

We will now dive into apiExampleSaga later, and explain what's going on inside each individual saga.

The actions

For each API call we need to make, we will have to create 4 action types. This sounds like a lot, but this is what we need to be able to be fully aware of the status of the request. The first action is dispatched from the UI components, while the other 3 are dispatched from within the saga, as we'll see shortly. In our example, it will look like this:

// core/actionTypes/apiExampleActionTypes.js

export default {
	// Dispatched from a component when there is a need to load new images
    LOAD_IMAGES: 'LOAD_IMAGES',
    
    // Dispatched from the saga
    // To signal the store that a request is currently undergoing (e.g. to display a loading spinner)
    LOAD_IMAGES_REQUEST: 'LOAD_IMAGES_REQUEST',
    // To signal a success of a request (e.g. to load the new images into the store)
    LOAD_IMAGES_SUCCESS: 'LOAD_IMAGES_SUCCESS',
    // To signal an error has occured during the request (e.g. to display an error popup)
    LOAD_IMAGES_FAILURE: 'LOAD_IMAGES_FAILURE'   
}

NOTE: The naming convention is also important - for there are different parts of the app that rely on it, as we'll see later in this document.

The saga

We will now explain how each individual saga is built to handle the request. After the explanation there will be a full code example to illustrate the process. We have 2 relevant "sub-sagas" in play in this process: The worker saga, and the watcher saga.

The worker saga is responsible for making the actual request, and dispatching the relevant actions. The usual flow of the worker saga will be:

  • Dispatch a REQUEST action to signal the start of the request.
  • Calling a function from api.js to make the actual HTTP request.
  • Get the data from the request.
  • Dispatch a SUCESS action with this data to signal a sucess of the request.
  • Dispatch a FAILURE action if anything went wrong during this process.

The watcher saga is responsible for intercepting the relevant actions (that are usually dispatched from the UI component), and activating the worker saga.

// core/sagas/apiExampleSaga.js

import {put, takeEvery, call, all} from 'redux-saga/effects';
import apiExampleActionTypes from '../actionTypes/apiExampleActionTypes';
import apiExampleActionCreators from '../actionCreators/apiExampleActionCreators';
import {fetchImages} from '../services/api';

// Worker saga
function *onLoadImagesSaga() {
    try {
        yield put(apiExampleActionCreators.loadImagesRequest());
        const response = yield call(fetchImages, {count: 10});
        const images = response.data;
        yield put(apiExampleActionCreators.loadImagesSuccess(images))
    } catch (e) {
        yield put(apiExampleActionCreators.loadImagesFailure(e.toString()));
    }
}

// Watcher saga - watch for every action of type LOAD_IMAGES, and trigger onLoadImages saga

function *watchLoadImagesSaga() {
    yield takeEvery(apiExampleActionTypes.LOAD_IMAGES, onLoadImagesSaga)
}

// Run all the watcher sagas

export default function *apiExampleSaga() {
    yield all([
        watchLoadImagesSaga()
    ])
}

That's it! All you have to do now is just trigger a LOAD_IMAGES action from anywhere (e.g. from a componentDidMount method), and it will trigger this process.

The reducers

To keep things straightforward, we are separating the data reducers from the status reducers, thus keeping the request status and data separate. This is powerful, allowing us to keep the data model in the store simple, and also keep all the loading and errors information of all our requests in one place. Please read This article that explains the concept very well.

The reducers (which can be found in reducers/isLoadingReudcer.js and reducers/errorReducer.js) are modeled in the same way as described there, with slight modifications. Together with those, there are also 2 selector factory functions that are used in case of a need to determine the status of a specific request (or multiple requests), called createLoadingSelector and createErrorSelector. They are both located in core/selectors/utils.js. They both get an array of action types as an argument, and return a reselect selector function (a function that gets the state and calculates some sort of value). That selector functions return a slightly different value from each one:

  • createLoadingSelector - if one of the requests is loading, returns true. Otherwise returns false.
  • createErrorSelector - if one of the requests has an error flag on it, returns the first error message it found. Otherwise, returns an empty string. NOTE: there might be a need to return the entire error array later on

For example, in my UI component mapStateToProps function, this is how I would know if my images are still loading:

// ApiExampleContainer.js

import {createLoadingSelector} from 'core/selectors/utils'

const mapStateToProps = (state) => ({
	isLoading: createLoadingSelector([apiEcampleTypes.LOAD_IMAGES])(state)
})

Finally, the components

The convenient aspect of this approach, is that the component writing is just normal, plain, Redux and React writing. Let's say I want to build a component that displays the images I fetched from my server. All I need to do is just dispatch a LOAD_IMAGES action whenever I need the new images (through a function in mapDispatchToProps), and get them from the state in mapStateToProps.

@ronnie-mathew-gmail
Copy link

Thanks Nice and Clear. Would you be able to share the entire code? Is there a repo that I can clone to see the entire code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment