How to integrate Redux and Redux Saga in your React Native project
By: Alexandre Jr
Tags: React Native, Redux, Redux Saga

Flux architecture

A way to organize changes in global states of our application. Global states that are shared across multiple components.

Middleware are interceptors. I can intercept my action, make an external action inside the middleware that will have some action. In this case, stock check. After this check is complete, it will pass the answer to Reducer. To do this check and everything else, let's use Redux Saga.

Setting store

I create with typescript template my project. I did the default file cleanup.

I start installing:

$ npx react-native init MyApp --template react-native-template-typescript $ yarn add redux react-redux

After that inside the src, I start creating a store folder. Inside it is where all the Redux configuration inside my application will be.

store = global redux state. Information that will be available between all application components.

// will be called once and will create my store

import { createStore } from 'redux';



const store = createStore(() => []);



export default store;

In my App.tsx file I import the provider from within my react-redux. It will stay around for the entire application.

It serves to provide the values contained within a context for all components within it.

import React from 'react';

import { Provider } from 'react-redux';



import store from './store';



function App() {

return (

<Provider store={store}>

<h1>Hi, hello ! </h1>

</Provider>

);

}



export default App;



// from that moment on, all my components inside the provider will have access to my store

To start viewing, I'll create the components folder and inside the Catalog.tsx file.

I create a simple functional component with the text "Catalog" inside and import it into my App.tsx.

To access the store within my Catalog component, I'm going to use the useStore variable. BUT this is not the best approach. The useStore returns a lot, so to get around this the best option is to use the useSelector.

It takes a function as a parameter and this function has state as its first parameter.

import React from "react";

import { useSelector } from "react-redux"; // very cool because I can granulate my search within the store



const Catalog: React.FC = () => {

const catalog = useSelector(state => state); console.log(catalog); // at this point return an empty []

return (

<h1>Catalog</h1>

)

};



export default Catalog;

📎 getState() => return the state of the application. At this point it will return me an empty array because that's what I filled inside my store.

Using the Reducer

I also need to be in control of taking this state and breaking it down into smaller state portions.

For example, if my application controls several global states, I have to have within Redux a way to be able to isolate these states in order to treat them.

Ex: In an online store the cart by itself is an isolated state, it should not influence any other part of the application. For example, if i have a state that stores an authenticated user in the application, that state should be totally isolated from the cart state because they don't relate in ant way.

And then inside Redux I have a concept called REDUCERS.

I'm going to create the modules folder inside my store folder. Within this folder, each folder will represent a module, a feature isolated from the application. I then create the cart folder and inside reducer.ts

The reducer is then the hub of a module within my redux application. It determines that information will be contained in this cart module.

It's a function that returns the data contained in my state:

function cart() {

return []

}



export default cart;

If I save this and go back to my store folder index, import my cart and place it inside an object I will see that the return it gives me inside redux (console.log(catalog)) in the component I will see that inside it I have an object and inside it a function.

import { createStore } from 'redux';



import cart from './modules/cart/reducer';



const store = createStore(() => ({

cart,

}));



export default store;

Inside the modules folder I will create the rootReducer.ts that will serve to combine several reducers into a single state available within my application.

import { combineReducers } from 'redux';



import cart from './cart/reducer';



export default combineReducers({

cart

});

I save this and inside the store folder's index.ts instead of passing all the function I had, I'll just pass:

import { createStore} from 'redux';



import rootReducer from './modules/rootReducer';



const store = createStore(rootReducer);



export default store;

Save that and now the return is no longer a function but the empty array that I put inside my cart reducer.

Now inside this cart I'll be able to manipulate the information that will be available there, for example, inside the cart I'll have the products, shipping price, all the information necessary to keep the data there inside my cart and display these data to the user.

The most important information at this first moment is the question of the products themselves, which are the products that are added in the cart.

Initial Status of Reducer

Within Redux all reducers must follow a format. In this case, informing the items that I will have in my cart.

Inside the Cart folder I make a file called types.ts and that's where I'll put my interfaces:

export interface iProduct {

id: number;

title: string;

price: number;

}



export interface ICartItem {

products: object;

quantity: number;

}



export interface ICartState {

items: ICartItem[];

}

And now I transform my function into a constant with arrow function so I can define the type of this variable

import { Reducer } from "react";

import { ICartState } from "./types";



const INITIAL_STATE: ICartState = {

items: []

}



const cart: Reducer<ICartState> = () => {

return INITIAL_STATE;

}



export default cart;

That way I already have my initial state. And now I'm going to start working with actions: the user can perform actions within my state, Add new products, change the quantity, etc.

Creating Json Server

In order to make the application work better, it would be interesting to have an API that returns a product catalog. As I don't have this API available, I will use a JSON-based api with server.json

At the root of my project I create the server.json file and inside I put my products json:

{

"products": [

{

"id": 1,

"title": "Cool Coat",

"price": 290.9

},

{

"id": 2,

"title": "Cheap Glasses",

"price": 20.9

},

{

"id": 3,

"title": "Fashion Shoes",

"price": 500

}

],

"stock": [

{

"id": 1,

"quantity": 3

},

{

"id": 2,

"quantity": 0

},

{

"id": 3,

"quantity": 1

}

]

}

Now I add a module:

$ yarn add json-server -D

And in package.json I create a new script and after I run the server and from that moment I have an API for products and stock.

"server": "json-server server.json -p 3333"



// terminal

$ yarn server



yarn run v1.22.4

$ json-server server.json -p 3333



\\{^_^}/ hi!



Loading server.json

Done



Resources

<http://localhost:3333/products>

<http://localhost:3333/stock>



Home

<http://localhost:3333>

Now that I created the API I need to create a way to consume. Inside src, I'll create the services folder and inside the api.ts file and then install axios inside the application.

import axios from 'axios';



const api = axios.create({

baseURL: "<http://localhost:3333>"

})



export default api;



// from that moment i already have API access

Now in my Catalog component I'll start loading the api items.

import React, { useState, useEffect } from "react";

import { iProduct } from "../store/modules/cart/types";

import api from "../services/api";



const Catalog: React.FC = () => {

const [catalog, setCatalog] = useState<iProduct[]>([]);



useEffect(() => {

api.get("products").then((response) => {

setCatalog(response.data);

});

}, []);



return (

<main>

<h1>Catalog</h1>



{catalog.map((product) => (

<article key={product.id}>

<strong>{product.title}</strong> {" - "}

<span>{product.price}</span> {" "}



<button type="button"> Buy </button>

</article>

))}

</main>

);

};



export default Catalog;

Working with Actions

Every time I need a user action and it's going to run something inside the application and it changes the state of Redux, like adding a new item to the cart, I'll use a Redux concept called actionCreator or actions.

Inside my Cart module I will create a file called actions.ts and each action that can happen inside the cart will be a function, ex: addProductToCart()

All actions must return the type. It identifies each of the actions.

payload: any additional information I need to add the product to the cart. Usually it's all the parameters I get there from my component.

// actions.ts



import { IProduct } from './types';



export function addProductToCart(product: IProduct) {

return {

type: 'ADD_PRODUCT_TO_CART',

payload: {

product,

}

}

}

And now there in my component when he clicks the buy button I want to trigger that action.

import React, { useState, useEffect, useCallback } from "react";

import { useDispatch } from "react-redux";



import { IProduct } from "../store/modules/cart/types";

import api from "../services/api";

import { addProductToCart } from "../store/modules/cart/actions";



const Catalog: React.FC = () => {

const dispatch = useDispatch()

const [catalog, setCatalog] = useState<IProduct[]>([]);



useEffect(() => {

api.get("products").then((response) => {

setCatalog(response.data);

});

}, []);



const handleAddProductToCart = useCallback((product: IProduct) => {

dispatch(addProductToCart(product))

},

[dispatch])



return (

<main>

<h1>Catalog</h1>



{catalog.map((product) => (

<article key={product.id}>

<strong>{product.title}</strong> {" - "}

<span>{product.price}</span> {" "}



<button

type="button"

onClick={() => handleAddProductToCart(product)}

>

Buy

</button>

</article>

))}

</main>

);

};



export default Catalog;

To start changing my items state I need to make some changes to my reducer.

Reducer is a function that takes two parameters: state and action

Im my reducer I will listen to the actions that are fired based on their type and payload.

import { Reducer } from "react";

import { ICartState } from "./types";



const INITIAL_STATE: ICartState = {

items: []

}



const cart: Reducer<ICartState> = (state, action) => {

return INITIAL_STATE;

}



export default cart;

Changing State

Inside my Reducer I will use the switch, which is based on the type of my action. In it I will perform one action OR another.

And then I create a case for each type of action.

obs: if I don't set my state = INITIAL_STATE at first it will come as undefined. Putting it is as we define it, an empty [].

Immer lib helps to avoid verbosity when making state changes to information that is immutable.

import produce from "immer";

produce => I will produce a new state from a draft of the previous state. The first parameter is state, and the second is a function whose first parameter is a draft.

This draft has the same format as my state, the only difference is that the changes within this draft can be the same way I would without the concept of immutability.

At the end it compares my draft with my state and makes the changes automatically.

All business rules should stay in the reducer, actions should never have the responsibility to search the state or anything like that.

Setting up Redux Saga

It's a redux middleware, an interceptor.

That is, I can perform an action between the action and the reducer.

$ yarn add redux-saga

And inside my store index.ts I start configuring my Redux Saga.

import { createStore, applyMiddleware } from 'redux';

import { composeWithDevTools } from 'redux-devtools-extension';

import createSagaMiddleware from 'redux-saga';



import rootReducer from './modules/rootReducer';



import { ICartState } from './modules/cart/types';



export interface IState {

cart: ICartState;

}



const sagaMiddleware = createSagaMiddleware();



const middlewares = [sagaMiddleware];



const store = createStore(

rootReducer,

composeWithDevTools(

applyMiddleware(...middlewares)

),

);



sagaMiddleware.run()



export default store;

And inside my Cart module I create a sagas.ts file and that file will be my saga settings related to this module itself.

I start by exporting all that comes from inside redux-saga/effects. Which is a function that takes an array into it.

Every time I call the addProductToCart action before I actually add the product to the cart I want to check if the quantity I want to order is available is stock. That is, this checkProductStock function will intercept my action.

import { all, takeLatest } from "redux-saga/effects";



function checkProductStock() {



};



export default all([

takeLatest('ADD_PRODUCT_TO_CART', checkProductStock)

]);

takeLatest => if the check of the previous action has not finished yet and the person clicks but again, it will cancel the previous actions and take only the last one

I have takeEvery too, so I would get all the clicks

takeLeading => take only the first

In the same place where I have the rootReducer I will create the rootSaga.

import { all } from "redux-saga/effects";



import cart from './cart/sagas';



export default function* rootSaga() {

return yield all([

cart,

])

}

I save this and go to my index.ts

// where will I pass my Saga functions

sagaMiddleware.run(rootSaga)

Obtaining cart quantity

import { IState } from './../../index';

import { all, takeLatest, select } from "redux-saga/effects";

import { addProductToCart } from "./actions";





type CheckProductStockRequest = ReturnType<typeof addProductToCart>





function* checkProductStock({ payload }: CheckProductStockRequest) {

const { product } = payload;





const currentQuantity: number = yield select((state: IState) => {



return state.cart.items.find(item => item.product.id===product.id)?.quantity ?? 0

});



console.log(currentQuantity)

};



export default all([

takeLatest('ADD_PRODUCT_TO_CART', checkProductStock)

]);

Dividing the actions

So far it's not because Redux Saga is intercepting the "ADD_PRODUCT_TO_CART" action that it's not being heard by the reducer and/or making the changes it needs.

The product is being added to the cart even when checking stock. But what we want is that the product is only added to the cart, if there is stock.

When I have an action in Redux that can succeed or fail, the best thing to do is separate the action into 3:

import { IProduct } from './types';





export function addProductToCartRequest(product: IProduct) {

return {

type: 'ADD_PRODUCT_TO_CART_REQUEST',

payload: {

product,

}

}

}



export function addProductToCartSuccess(product: IProduct) {

return {

type: 'ADD_PRODUCT_TO_CART_SUCCESS',

payload: {

product,

}

}

}



export function addProductToCartFailure(productId: number) {

return {

type: 'ADD_PRODUCT_TO_CART_FAILURE',

payload: {

productId,

}

}

}

Now there in my CatalogItem I'll fix my import bringing now the addProductToCartRequest.

In the saga I do the same thing. And in takeLatest I should hear the "ADD_PRODUCT_TO_CART_REQUEST" action.

Now on my reducer before I was listening to 'ADD_PRODUCT_TO_CART' but now I'm going to hear 'ADD_PRODUCT_TO_CART_SUCCESS' because I just want to add this product to the cart if the stock check passes.

Finishing stock call

What I'm going to do now is the API call that will return what quantity available I have stock for that specific product.

In my Sagas.ts file I will complete with:

function* checkProductStock({ payload }: CheckProductStockRequest) {

const { product } = payload;



const currentQuantity: number = yield select((state: IState) => {



return state.cart.items.find(item => item.product.id ===product.id)?.quantity ?? 0

});



const availableStockResponse: AxiosResponse<IStockResponse> = yield call(api.get, `stock/${product.id}`)





if (availableStockResponse.data.quantity > currentQuantity) {

yield put(addProductToCartSuccess(product))

} else {

yield put(addProductToCartFailure(product.id))

}

};

call() method takes some parameters:

The first is what asynchronous action I want to perform. It could be any promise

action I want to perform. It could be any Second parameter are the parameters that api.get receives

Inside the generic <> of the AxiosResponse type I pass what is the return of my call. Hence the creation of the interface.

put() method => compares to dispatch. Serves to trigger an action.

At this point we are already doing the stock check ! I can only add to my cart the amount I have available.

But I still don't have any visual feedback that, that product is out of stock.

In my reducer I'll create another case for failure.

case "ADD_PRODUCT_TO_CART_FAILURE": {

break;

}

Signaling out of stock

In order for me to be able to signal in my view that a product is out of stock, what I will need to do is note in my state which products are out of stock.

In my types.ts file inside ICartState I will create an information called failedStockCheck: number[]; which will store the id of the products that failed the stock check.

Now on my reducer:

const INITIAL_STATE: ICartState = {

items: [],

failedStockCheck: [],

};



case "ADD_PRODUCT_TO_CART_FAILURE": {

draft.failedStockCheck.push(action.payload.productId)



break;

}

And now inside my CatalogItem.js



const hasFailedStockCheck = useSelector<IState, boolean>(state => {

return state.cart.failedStockCheck.includes(product.id)

})



{ hasFailedStockCheck && <span style={{ color: 'red'}}>Lack of stock.</span>}

That way we will already have the return on the screen if these is product out of stock.

Creating action types

At this point I have several variables that repeat in many places! So if I ever need to change something, I'll have to change it in several different places. But I will solve this

In my types.ts file I'm going to export an enum (it's basically a property that has a value):

export enum ActionTypes {

addProductToCartRequest = 'ADD_PRODUCTS_TO_CART_REQUEST',

addProductToCartSuccess = 'ADD_PRODUCTS_TO_CART_SUCCESS',

addProductToCartFailure = 'ADD_PRODUCTS_TO_CART_FAILURE',

}

Now I go to my actions and change the types

import { IProduct, ActionTypes } from './types';





export function addProductToCartRequest(product: IProduct) {

return {

type: ActionTypes.addProductToCartRequest,

payload: {

product,

}

}

}



export function addProductToCartSuccess(product: IProduct) {

return {

type: ActionTypes.addProductToCartSuccess,

payload: {

product,

}

}

}



export function addProductToCartFailure(productId: number) {

return {

type: ActionTypes.addProductToCartFailure,

payload: {

productId,

}

}

}

And on my reducer and sagas too.

So abstract my actions types into variable ! 😃