Redux Saga — The simple (opinionated) way

Image for post
Image for post

Redux-Saga is just a redux middleware library, that handles side effects in your application by leveraging an ES6 features called Generators. You can write asynchronous code that looks synchronous.

Don’t worry if you don’t know anything about Generators. A generator function is like a book: you read a few pages, you close the book, and when you open it again, you will resume from the page you left off.

So far, you may have used redux-thunk for handling side-effects. What I like about sagas is that you can write clean and easy to understand code. It might seem like it’s a lot of boilerplate code, but it is very easy to maintain it and add new things.

Installation

The file structure could look like this:

src/
actions/
components/
containers/
reducers/
sagas/
index.js

I think you already know how to setup a simple React project and I will jump directly to setting up redux-saga.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension';
// I will explain this reducers and sagas imports later
import reducers from './reducers';
import sagas from './sagas';
// Create the saga middleware
const
sagaMiddleware = createSagaMiddleware();
const initialState = {};

// Create the application store
const
store = createStore(
reducers,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(sagas);render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);

Creating Action Types and Creators

As you may already know, you need to create actions that will be dispatched when something happens in your application and the state should change. An action has two things: type and payload.

For creating action types and creators and writing less code overall, I will use reduxsauce. I will explain everything I do providing relevant examples.

Every side effect that includes an API call has 3 potential states: REQUEST, SUCCESS, FAILURE. (luckily they all have the same number of letters)

Let’s take an entity like Product and create some basic actions for it.

import { createActions } from 'reduxsauce';export const { Types, Creators } = createActions({
// This will be used to reset the state
reset: null,
getProductsRequest: null,
getProductsSuccess: ['products'],
getProductsFailure: ['error'],
updateProductRequest: ['productId', 'product'],
updateProductSuccess: ['product'],
updateProductFailure: ['error'],
});

The keys of the object will become keys of the Creators. They will also become the keys of the Types after being converted to SCREAMING_SNAKE_CASE.

The values will control the flavour of the action creator. When null is passed, an action creator will be made that only has the type.

getProductsSuccess: ['products'] created the action type GET_PRODUCTS_SUCCESS and a payload object that will have this “products” key. When dispatching this action, whatever you will pass as the parameter will be the value of products key. (e.g. the array of products)

updateProductRequest: ['productId', 'product'] created the action type UPDATE_PRODUCT_REQUEST and a payload object that will have the keys productId and product. When dispatching this action, the first parameter will be passed as the value of key productId and the second one as the value of key product.

Reducers

In this part we will talk about reducers, how to create them and how to handle every action type case with the help of reduxsauce. As an alternative, you can always go back to switch case statements, but I think that this approach means less code and pretty easy to follow.

I will provide the content first and explain everything afterwards:

import { createReducer, resettableReducer } from 'reduxsauce';
import { Types } from '../actions';

const INITIAL_STATE = {
products: [],
selectedProduct: {},
loading: false,
error: false,
};
// Handle every action case here: key is the action type, value is the handler
const HANDLERS = {
// GET Products
[Types.GET_PRODUCTS_REQUEST]: state => ({
...state,
loading: true,
error: false,
}),
[Types.GET_PRODUCTS_SUCCESS]: (state, { products }) => ({
...state,
products,
loading: false,
error: false,
}),
[Types.GET_PRODUCTS_FAILURE]: (state, { error }) => ({
...state,
loading: false,
error,
}),

// UPDATE Product
[Types.UPDATE_PRODUCT_REQUEST]: state => ({
...state,
loading: true,
error: false,
}),
[Types.UPDATE_PRODUCT_SUCCESS]: (state, { product }) => ({
...state,
selectedProduct: product,
loading: false,
error: false,
}),
[Types.UPDATE_PRODUCT_FAILURE]: (state, { error }) => ({
...state,
loading: false,
error,
}),
};

export const productReducer = resettableReducer(
Types.RESET,
createReducer(INITIAL_STATE, HANDLERS)
);

From ../actions we import only the Types.

Setup an initial state, in this case we have an empty array of products, an empty object as the selectedProduct and loading & error states on false.

The HANDLERS part: As you might already know, every action type case should be handled and this is what we are doing here. Usually, you would do it in a switch case, but here is a quite easier alternative. Consider the HANDLERS object like this:

» the key is the action case (e.g. GET_PRODUCTS_REQUESTS)
» the value is the function that handles that specific case, where the parameters are state and payload. In particular, I used destructuring for the payload and only access what I need from it.

Note that we access the action types like this: Types.NAME_OF_ACTION

Finally, we create and export the reducer. It is wrapped with resettableReducer function, that receives the action type that will trigger the state to reset (become INITIAL_STATE) and a reducer. (This is optional)
Alternatively, this could be exported in this way:

export const productReducer = createReducer(INITIAL_STATE, HANDLERS);

But in this case, you won’t be able to reset the state and assume you don’t even need it.

Here is the place where you import all your reducers and combine them.

import { combineReducers } from 'redux';
import { userReducer } from './user';
import { productReducer } from './product';
// import { entityReducer } from './entity';

const reducers = combineReducers({
user: userReducer,
product: productReducer,
// entity: entityReducer
});

export default reducers;

The best part, Sagas

As with reducers, you should group your sagas in the same manner.
An important note is that for this particular example, I kept the API logic (that used axios) in a single file. You are free to use whatever you like and design the code accordingly.

import axios from 'axios';

const create = (baseURL = 'http://localhost:1337') => {
const api = axios.create({
baseURL,
timeout: 10000,
});

// PRODUCTS API calls
const
getProducts = () => api.get('/products');
const updateProduct = (productId, product) =>
api.put(`/products/${productId}`, product);

return {
// A list of the API functions

getProducts,
updateProduct,
};
};

export default {
create
};
import { call, put, takeEvery } from 'redux-saga/effects';
import { Types, Creators } from '../actions';
import API from '../api';
// This has been explained in the optional part
const api = API.create();

export function* getProducts() {
try {
const response = yield call(api.getProducts);
yield put(Creators.getProductsSuccess(response.data));
} catch (error) {
yield put(Creators.getProductsFailure(error));
}
}

export function* updateProduct({ productId, product }) {
try {
const response = yield call(api.updateProduct, productId, product);
yield put(Creators.updateProductSuccess(response.data));
} catch (error) {
yield put(Creators.updateProductFailure(error));
}
}

export const productSagas = [
takeEvery(Types.GET_PRODUCTS_REQUEST, getProducts),
takeEvery(Types.UPDATE_PRODUCT_REQUEST, updateProduct),
];

A generator function is declared by adding an * after the function keyword (e.g. function* funcName()).

The parameter received is the payload object. In the updateProduct case it is destructured into productId and product.

The pattern for the API call with yield call: yield call(apiFunction, param1, param2, ...) => apiFunction(param1, param2, …)

In the end, we export an array productSagas. takeEvery(Types.GET_PRODUCTS_REQUEST, getProducts) means that for every action with type GET_PRODUCTS_REQUEST that will be dispatched, the function getProducts will be triggered.

Here we are going to import and yield for all the sagas.
You need to do this for all the new sagas you’ll create.

import { all } from 'redux-saga/effects';
import { productSagas } from './product';
import { userSagas } from './user';

/* ------------ Connect Types to Sagas ------------ */
export default
function* root() {
yield all([...userSagas, ...productSagas]);
}

Usage

const mapDispatchToProps = dispatch => ({
getProducts: () => dispatch(Creators.getProductsRequest()),
updateProduct: (id, product) =>
dispatch(Creators.updateProductRequest(id, product)),
});

And then, just use it from props.

const dispatch = useDispatch()
const getProducts = () => dispatch(Creators.getProductsRequest())
const updateProduct = (productId, product) => dispatch(Creators.updateProductRequest(productId, product))

Final Thoughts

Reduxsauce provides more customization, but in most cases what I used here should be enough. I see it as syntactic sugar, easy to understand once you read the small documentation.
I like how I can structure my code using redux-saga and have a clean code overall. This is something that I rarely achieved using redux-thunk. Generators are cool once you understand them, but you don’t need to know too much in order to use redux-saga as your redux middleware.

I hope everything was explained in a simple manner. Leave me your thoughts and clap if you like my first article on Medium.

Written by

Full-Stack Software Developer (React/Node) @ Betfair Romania Development

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store