Async requests in redux-saga

Handle Async request in react-saga

react

06-April-2020

Hi, We have talked about redux in previous article, check it out, where I have tried to explain the best way to write, understand redux. if you don’t know redux, it’s state management for react, where it comes more handy to share your state among any component inside your application instead of passing it from parent to children and then ends up with this:

mess

What Are you going to do

We are going to build application where redux is been used with redux-saga to handle our fetch requests, we will build crud-users app.

CRUD: Create, Read, Update, Delete

I will go through every package that are we going to use with its purpose difenition:

1.redux: Our main state management. 2.react-redux: To inject state in the components. 3.redux-saga: To handle fetch-requests by using generator functions. 4.reduxsauce: To define our types and actions, easily manage our types. 5.json-server: Get a full fake REST API with zero coding in less than 30 seconds (seriously😎). 6.seamless-immutable: To make your state immutable.

For an api we will use jsonplaceholder

Install

To start we will use create-react-app to install, but ….? we will eject it directly which is something you might do depending on your project. I’m doing it because I like that 😃 I will call my project crud-users, feel free with naming. I’m using yarn, but you can use npm as well if you prefer that too:

yarn add -g create-react-app

create-react-app crud-users

cd crud-users && yarn eject

now it’s ready to go remove everything in the src directory without index.js, serviceWorker.js. if you're interested to build Progressive web apps, you might enable serviceWorker in index.js by calling register function of unregister // default : serviceWorker.unregister() // change to: serviceWorker.register()

1. Setup API

We will use json-server because it offers simplicity, no need to configuration and also you can set your response from real api to it which what we are going to do it.

Installing first json-server globally: yarn add -g json-sever Make file in your project directory called: db.json. You’re free with naming. now we will fetch users by api

https://jsonplaceholder.typicode.com/users/ This is will return json response with 10 users copy them to your db.json.

In package.json: set this command to run json-server under scripts.

"watch-db": "json-server --watch db.json --port 4000"

now run it:

yarn watch-db

and you will get link: http://localhost:4000/users, with 10 users leave this running and let’s go to the next step.

2. Routing

For routing we will use react-router-dom, to handle our navigation route between pages.

✍️. you might use react-router-redux which is better way to handle our routing actions.

we will set three pages: A. Home: /, B. Users : /users C. Add user: /add-user

create a file under Containers/routes.jsx:

import React from 'react';
import Home from './Home';
import UsersContainer from './UsersContainer';
import AddUserContainer from './AddUserContainer';

export const routes = {
  Home: {
    path: '/',
    exact: true,
    component: Home
  },
  Users: {
    path: '/users',
    exact: true,
    component: UsersContainer
  },
 addUser: {
   path: '/add-user',
   exact: true,
   component: AddUserContainer
 }
}

create a file in the same directory called Routing.jsx

import React from 'react';
import { Route } from 'react-router-dom';

import { routes } from './routes';

const Routing = () => {
  return (
    <div>
      {Object.keys(routes).map((route, key)=> (
        <Route {...routes[route]} key={key+1}/>
      ))}
   </div>
  )
}


export default Routing;
3. Redux
yarn add redux react-redux redux-logger seamless-immutable reduxsauce

Install these packages. Then make file in this path : src/redux/UserRedux.js In this file we will define our Types and Actions that used by redux

import Immutable from 'seamless-immutable';
import { createActions, createReducer } from 'reduxsauce';

// state =>user State
const INITIAL_STATE = Immutable({
    fetching: false,
    users: [],
    fetchingError: null
})

// Actions
const { Actions, Creators } = createActions({
	fetchUsersStart: null,
	fetchUsersSuccess: ['users'],
	fetchUsersFailure: ['error'],
	resetFetchError: null
})

export const UserTypes = Types;
export default Creators;

export const fetchUsersStart = state => state.merge({
	fetching: true,
	users: [],
	fetchingError: null
})
export const fetchUsersSuccess = (state, { users }) => state.merge({
	fetching: false,
	users,
	fetchingError: null
})
export const fetchUsersFailure = (state,{ error }) => state.merge({
	fetchingError: error,
	fetching: false
})
export const resetFetchError = (state) => state.merge({ fetchingError: null })

// Reducer
export const reducer = createReducer(INITIAL_STATE, {
	[Types.FETCH_USERS_START]: fetchUsersStart,
	[Types.FETCH_USERS_SUCCESS]: fetchUsersSuccess,
	[Types.FETCH_USERS_FAILURE]: fetchUsersFailure,
	[Types.REST_FETCH_ERROR]: resetFetchError
})

And than create a file where you can create your store:

src/redux/index.js

import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { createLogger } from 'redux-logger'

import { reducer as UserReducer } from './UserRedux'

export default (() => {
	const middlewares = []
	const enhances = []

	const rootReducer = combineReducers({
		users: UserReducer
	})

	// create logger middleware
	const logger = createLogger()
	middlewares.push(logger)

	// combine all middlewares
	enhances.push(applyMiddleware(...middlewares))

	const store = createStore(rootReducer, compose(...enhances))

	return store
})()
4. Setup CRUD API

We have json-server running, now what we want to do is setting our API to receive functions where we could get Users, Updating them, delete them, and also add new user to them. Create a file under this source: src/utils/apiConfig.js

const headers = {
	Access: 'application/json',
	'Content-Type': 'application/json'
}
const url = 'http://localhost:4000/users'


export const api = {
	GET_ALL: () => fetch(url).then(res => res.json()),

	GET: id => fetch(url+id).then(res => res.json()),

	POST: (id, data) => fetch(url+id, {
		method: 'post',
		body: JSON.stringify(data),
		headers,
	}).then(res => res.json()),

	DELETE: id => fetch(url+id, {
		method: 'delete',
		headers,
	}).then(res => res.json()),

	PUT: (id, updatedItem) => fetch(url+id, {
		method: 'put',
		headers,
		body: JSON.stringify(updatedItem)
	}).then(res => res.json())
}

now with this, we will move to setup out Saga 🙂.

5. Saga

install: yarn add redux-saga

explaination above every line of code

Before we start, you need to know that saga works with generator functions.

. If you’re not familiar with generator functions, it is not so difficult! you need to understand that with generator function, you add * with function definitions and inside this function you must use yield keyword, like return async call …

Create folder under src directory with name saga (feel free with naming). We will create a file with name UserSaga.js inside this folder :

import { put, call } from 'redux-saga/effects' 

// import Actions from 'redux' files where we can trigger automatically any action
import { UserActions } from '../redux/UserRedux'

// function genetator
export function* fetchUsers(api){
	// Call api with yield keyword that returns res.json()
	const res = yield call(api.GET_ALL)

	if(Array.isArray(res) && res.length !==0) {
		// check if response is an array, specific for this api.
		yield put(UserActions.fetchUsersSuccess(res))
	} else {
		// Otherwise put my anything we return from the server as error.
		// trigger failure with thid error
		const error = res
		yield put(UserActions.fetchUsersFailure(error))
	}
}

Now If we are going to use more than userSaga, than you might better to set them all together and run them, checkout this in src/saga/index.js this will export all our sagas together, we can run them as root. so make file with src/saga/index.js and write in it this:

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

import { fetchUsers } from './UserSaga'
import { UserTypes } from '../redux/UserRedux'

import { api as apiConfig } from '../utils/apiConfig'

export default (() => {
	return function* rootSaga(){
		yield all([
		          	// fetching
		          	/**
		          	* takeLatest takes: 
		          		1. ActionsTypes: which will trigger our types
		          		2. function generator or any function to make the trigger;
		          		3. any: which are any args you want to pass as extra;

		          		** docs: https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
		          		**/
		         	takeLatest(UserTypes.FETCH_USERS_START, fetchUsers, apiConfig)
		         ])
	}
})()

now in src/redux/index.js we need run our sagas: checkout new code with // added comments

import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { createLogger } from 'redux-logger'

import { reducer as UserReducer } from './UserRedux'
//* added
//start
import createSagaMiddleware from 'redux-saga'
//end

export default ((rootReducer, rootSaga) => {
	const middlewares = []
	const enhances = []
	//* added
	// start
	const sagaMiddleware = createSagaMiddleware()
	middlewares.push(sagaMiddleware);
	// end

	// create logger middleware
	const logger = createLogger()
	middlewares.push(logger)

	// combine all middlewares
	enhances.push(applyMiddleware(...middlewares))

	const store = createStore(rootReducer, compose(...enhances))
	//* added
	// start
	sagaMiddleware.run(rootSaga)
	return store
})()
6. Connect

Now we are ready for fetching users, and use them inside our components. Connecting with our components will be by using package react-redux yarn add react-redux now update src/index.js with this:

import React from 'react';
import ReactDOM from 'react-dom';
import * as ServiceWorker from './ServiceWorker';

import Routing from './containers/Routing'
//added
import Provider from 'react-redux'
import createStore from './redux'

const store = createStore

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


ServiceWorker.register();

If you navigate to Routing.jsx there we will fetch users by using redux-saga

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

import { UserActions } from '../redux/UserRedux'

class Routing extends Component {
	componentDidMount() {
		const { fetchUsersStart } = this.props

		fetchUsersStart()
	}

	render() {
		const { users, fetching } = this.props

		return (
		        <ul>
		        	{!fetching && users.length !==0
		        		&& users.map(user => <li key={user.id}>{user.name}</li>)
		        	}
		        </ul>
		    )
	}
}

const mapStateToProps = state => ({
	fetching: state.users.fetching,
	users: state.user.users // ugly, I know
})

const mapDispatchToProps = dispatch => ({
	fetchUsersStart: () => dispatch(UserActions.fetchUsersStart())
})
export default connect(mapStateToProps, mapDispatchToProps)(Routing)

if you have already knowledge about redux, this is a typically redux injecting props.where you have connect from react-redux as higher order component will takes two parameters one is the state, and other is dispatch.

Run
yarn start
or
npm run start

For styling: it is up to you, I’m not really concentrating in styling in this tutorial, the main concept is using redux-saga, and that’s why I didn’t use it a main point

This is for fetching users and the same will be for delete, add, update Users. For that checkout my repo for rest of code.

I have built a full CRUD app with redux-saga. I have updated my code with all features, what I would recommend that you try it first by own and than check it with my code in Github.

Code YouTube


Published on CodeReview by Mustafa Alroomi on 06-April-2020