Redux Saga - Asynchronous Side Effects for Redux
Redux Saga - Asynchronous Side Effects for Redux

In this video you will learn what is Redux-Saga, why do we need it and how to use it together with Redux inside React application on a real example.

What is Saga?

The first question is what is Redux-Saga and why do we need it? Actually this is the additional library that we can use together with Redux and we need it to make asynchronous actions.

And actually you might know that the most popular way to implement async actions is redux-thunk. This is the easier way to do asynchronous actions inside Redux. Redux-Saga is something similar but more advanced and more configurable.

If you previously worked with Angular and NgRx and you used there effects it will be super similar.

The main idea of Redux-Saga is to implement effects. We dispatch some actions and then our code inside Redux-Saga reacts on our action. Typically it will be an asynchronous call. At the end of this call we can call dispatch again with the success action.

To understand it better let's implement Redux-Saga on the real example.

Project

So what project do we have? Here is a small React application just with hooks. We have 3 users which we fetch from the API. We can add new user and it will be added with an API call to the list.

React hooks

To implement an API I'm using a json-server package. It is just a Node package to implement fake API in a matter of minutes.

First of all we must create db.json file.

{
  "users": [
    {
      "id": 1,
      "name": "Jack"
    },
    {
      "id": 2,
      "name": "John"
    },
    {
      "id": 3,
      "name": "Mike"
    }
  ]
}

And we can start our API.

json-server --watch db.json --port 3004

Now we can open localhost:3004/users and get back our list of users. And we will use this data to fetch data from the API through Redux-saga.

Now let's check our project. We have just a single component where everything happens.

const Users = () => {
  const [inputValue, setInputValue] = useState("");
  const [users, setUsers] = useState([])

  const addUser = () => {
    const newUser = {
      id: +Math.random().toFixed(4),
      name: inputValue
    }
    setUsers([...users, newUser])
    setInputValue('')
  }

  useEffect(() => {
    axios.get('http://localhost:3004/users').then(response => {
      setUsers(response.data)
    })
  }, []);

  return (
    <div>
      <div>Total: {users.length}</div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={addUser}>Add user</button>
      </div>
      <div>
        {users.map((user, index) => (
          <div key={index}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};

We have here users array inside a state. On initialize we fetch our users and write them inside state. When we add a user to just push it to the array.

Now the goal of this post is to configure Redux together with Redux-Saga and to refactor this feature in it.

Installation

First of all we must install all packages.

yarn add redux react-redux redux-saga

Typically we just use redux as a state management and react-redux as bindings to React. But here additionally for side effects we installed redux-saga.

It is important to understand. Saga is not a new way to write Redux code. It just solves async part of Redux.

Now let's start with configuring our Redux. Here I already wrote standard Redux configuration.

const reducers = combineReducers({});

const middleware = [];

const store = createStore(
  reducers,
  composeWithDevTools(applyMiddleware(...middleware))
);

export default store;

Here we have quite standard configuration. We have default reducers, a middleware array which is empty for now and redux-devtools.

As you can see here we have a code to configure redux-devtools extension so we must install this package also.

yarn add @redux-devtools/extension

Now we want to add Redux-Saga inside.

import createSagaMiddleware from "redux-saga";
import { watcherSaga } from "./sagas/rootSaga";

const reducers = combineReducers({});

const sageMiddleware = createSagaMiddleware();
const middleware = [sageMiddleware];

const store = createStore(
  reducers,
  composeWithDevTools(applyMiddleware(...middleware))
);

sageMiddleware.run(watcherSaga);

export default store;

We created sageMiddleware by calling createSagaMiddleware function and provided it in the array of our middlewares.

Also later we called sageMiddleware.run where we provided watcherSaga. We didn't create watcherSaga yet but the idea is that inside we will define all actions on which redux-saga must react.

Now let's create rootSaga file.

// src/redux/sagas/rootSaga.js
export function* watcherSaga() {
}

We defined here a function with a star symbol which is actually a generator.

Let's check if it's working. As you can see in browser we are getting an error that we didn't define reducers.

Reducer error

This is actually correct so let's do that now. We need to create users reducer where we will store our users data and loading state.

// src/redux/reducers/users.js
const initialState = {
  data: [],
  isLoading: false,
  error: null,
};

const reducers = (state = initialState, action) => {
  return state
};

export default reducers;

Our reducer is ready now we need to register it in our combineReducers function.

// src/redux/configureStore.js
import usersReducer from "./reducers/users";

const reducers = combineReducers({
  users: usersReducer,
});

Now in browser we don't have any errors and our users reducer is available for us.

Basic Redux devtools

Fetching users

Now we want to implement fetching of the users. This is why we must create action types. And again this is just a plain Redux, no saga yet.

// src/redux/actionTypes.js
export const GET_USERS_START = "GET_USERS_START";
export const GET_USERS_FAILURE = "GET_USERS_FAILURE";
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS";

As it is an asynchronous operation we define 3 action types. Now we need to create our actions.

// src/redux/actions/getUsers.js
import {
  GET_USERS_FAILURE,
  GET_USERS_START,
  GET_USERS_SUCCESS,
} from "../actionTypes";

export const getUsers = () => ({ type: GET_USERS_START });
export const getUsersSuccess = (payload) => ({
  type: GET_USERS_SUCCESS,
  payload,
});
export const getUsersFailure = (payload) => ({
  type: GET_USERS_FAILURE,
  payload,
});

Here we created 3 actions for start, success and failure. In success and failure we also provided payload to put data inside.

Now we need to update our reducers and change the state accordingly.

const reducers = (state = initialState, action) => {
  switch (action.type) {
    case GET_USERS_START: {
      return { ...state, isLoading: true };
    }
    case GET_USERS_SUCCESS: {
      return { ...state, isLoading: false, data: action.payload };
    }
    case GET_USERS_FAILURE: {
      return { ...state, isLoading: false, error: action.payload };
    }
    default: {
      return state;
    }
  }
};

On start we change isLoading to true. On success to write fetched users to data property and on error we save our error inside.

Adding Redux-Saga

Now we can start with adding Redux-Saga. For this instead of axios call we want to dispatch start action.

const Users = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(getUsers())
  }, [dispatch])
}

Again this is plain Redux where we get a dispatch function inside our component and call our action.

Get users start action

As you can see in browser our action is successfully dispatched.

Now we must tell Saga to react on this dispatch. When we dispatch getUsers action Saga must fetch data and then dispatch on it's own success or failure.

For this we must define on what action Saga must react.

// src/redux/sagas/rootSaga.js
import { GET_USERS_START } from "../actionTypes";
import { takeEvery } from "redux-saga/effects";
import { getUsersHandler } from "./handlers/getUsers";

export function* watcherSaga() {
  yield takeEvery(GET_USERS_START, getUsersHandler);
}

Here we define that for every GET_USERS_START action that is dispatched Saga must call getUsersHandler.

Now let's define this handler.

// src/redux/sagas/handlers/getUsers.js
import { getUsersFailure, getUsersSuccess } from "../../actions/getUsers";
import { call, put } from "redux-saga/effects";
import getUsersRequest from "../requests/getUsers";

export function* getUsersHandler() {
  try {
    const response = yield call(getUsersRequest);
    yield put(getUsersSuccess(response.data));
  } catch (error) {
    yield put(getUsersFailure(error.message));
  }
}

getUsersHandler is also a generator where we call getUsersRequest which is just a promise and dispatch either getUsersSuccess or getUsersFailure. To dispatch an action in Saga we use yield put() construction.

The only thing that we are missing is getUsersRequest.

// src/redux/sagas/requests/getUsers.js
import axios from "axios";

export default () => axios.get("http://localhost:3004/users");

As you can see this is just a promise which we get from the Axios.

Get users saga

As you can see in browser we get not only start action but also a success. So every time when we dispatch getUsersStart action Saga listens to this dispatch and makes an API call. If it is successful then it dispatches success action.

This is exactly how you can do API requests with Saga. So essentially Saga is something outside of the Redux which can react on Redux actions and dispatch something when needed.

You can easily test handlers and requerst in isolation without writing Redux code at all.

Now we can change the code of getting users.

const Users = () => {
  const users = useSelector((state) => state.users.data);
  ...
};

So now instead of reading users from local state we read them from Redux state. And again this code doesn't have anything to do with Saga. The goal of Saga i just to dispatch some actions.

Creating a user

Now let's implement creating of the user really fast. First of all we need action types.

// src/redux/actionTypes.js
export const CREATE_USER_START = "CREATE_USER_START";
export const CREATE_USER_FAILURE = "CREATE_USER_FAILURE";
export const CREATE_USER_SUCCESS = "CREATE_USER_SUCCESS";

After this we need create actions.

// src/redux/actions/createUser.js
import {
  CREATE_USER_FAILURE,
  CREATE_USER_START,
  CREATE_USER_SUCCESS,
} from "../actionTypes";

export const createUser = (payload) => ({ type: CREATE_USER_START, payload });
export const createUserSuccess = (payload) => ({
  type: CREATE_USER_SUCCESS,
  payload,
});
export const createUserFailure = (payload) => ({
  type: CREATE_USER_FAILURE,
  payload,
});

Now let's react on these 3 actions in our reducer.

const reducers = (state = initialState, action) => {
  switch (action.type) {
    case CREATE_USER_START: {
      return { ...state, isLoading: true };
    }
    case CREATE_USER_SUCCESS: {
      const updatedUsers = [...state.data, action.payload];
      return { ...state, isLoading: false, data: updatedUsers };
    }
    case CREATE_USER_FAILURE: {
      return { ...state, isLoading: false, error: action.payload };
    }
    default: {
      return state;
    }
  }
};

Here we just push new user information in our users array.

Now we start to implement Saga for create user. First of all let's write request.

// src/redux/sagas/requests/createUser.js
import axios from "axios";

const createUser = (name) => {
  return axios.post("http://localhost:3004/users", { name });
};

export default createUser;

And also a handler.

// src/redux/sagas/handlers/createUser.js
import { call, put } from "redux-saga/effects";
import { createUserSuccess, createUserFailure } from "../../actions/createUser";
import createUserRequest from "../requests/createUser";

export function* createUserHandler(action) {
  try {
    const response = yield call(createUserRequest, action.payload);
    yield put(createUserSuccess(response.data));
  } catch (error) {
    yield put(createUserFailure(error.message));
  }
}

And we must create on dispatching of createUser.

import { createUserHandler } from "./handlers/createUser";

export function* watcherSaga() {
  ...
  yield takeEvery(CREATE_USER_START, createUserHandler);
}

The only thing that is left is to update our component.

const Users = () => {
  ...
  const addUser = () => {
    dispatch(createUser(inputValue));
    setInputValue("");
  };
}

As you can see in browser everything is working as expected and we can save users through API in database.

But now you might think that Redux-saga is too complicated. But actually it is quite scalable and it is suitable for big production applications. But here we just talked about basics of Redux-saga. Inside we have quite a lot of advanced stuff like concurrency, fork mode, non-blocking calls and much much more.

Redux-Saga is really a versatile tool inside Redux ecosystem.

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done