Skip to main content

Let's learn redux-saga

Greetings!

In a previous article, I talked about redux architecture with reactjs a bit. In this article, I talk about handling side effects with redux-saga.

The complete code for this article is here.

What is redux-saga?

Redux saga is a side effect manager using ES6 generators. Therefore it gives the added benefit of easy testability.

What do we build?

To demonstrate the architecture, let's create a simple GitHub user application where we fetch GitHub handle for a given username.
For example, this gives my GitHub account information.
https://api.github.com/users/slmanju

Initialize the project

create-react-app react-redux-saga-example
cd react-redux-saga-example
npm install redux react-redux reselect
npm install redux-saga
npm install axios

Util to fetch actual data

Let's create a little util for HTTP calls using axios.
import axios from "axios";

const withError = (promise) => promise.then((data) => [null, data]).catch((err) => [err]);

export const getGitHubUser = async (username) => {
  return await withError(axios.get(`https://api.github.com/users/${username}`));
};

GitHub components

These are our components to accept the username and show a user.
import { useState } from "react";

const GitHubInput = ({ handleSubmit }) => {
  const [username, setUsername] = useState("");

  const handleChange = (event) => {
    setUsername(event.target.value);
  };

  const onSubmit = (event) => {
    event.preventDefault();
    setUsername("");
    handleSubmit(username);
  };

  return (
    <div>
      <form>
        <label>Name: </label>
        <input
          type="text"
          name="username"
          value={username}
          onChange={handleChange}
        />
        <button onClick={onSubmit}>Add</button>
      </form>
    </div>
  );
};

export { GitHubInput };
import './GitHub.css';

const GitHubUser = ({ github }) => {
  return (
    <div className="container">
      <div className="img-container">
        <img src={github.image} className="img" />
      </div>
      <div className="data">
        <div>{github.name}</div>
        <div>{github.bio}</div>
        <a href={github.url} target="_blank">{github.username}</a>
      </div>
    </div>
  );
};

export { GitHubUser };

Actions, reducers, and selectors

Let's quickly define these. I assume you have an understanding of these.
In addition to fetching calls, I have added a loader as well to indicate that data is loading. Other than that, this is a typical file.
One special thing to note here is we do not include FETCH_USER_REQUESTED in the reducer. That is because it will be handled as an effect in the saga.
import { createSelector } from "reselect";

export const FETCH_USER_REQUESTED = "github/fetch";
export const FETCH_USER_SUCCESS = "github/fetchSuccess";
export const FETCH_USER_FAILED = "github/fetchFailed";
export const LOADING = "github/loading";

export const fetchUser = (username) => {
  return {
    type: FETCH_USER_REQUESTED,
    username,
  };
};

export const fetchUserSuccess = (user) => {
  return {
    type: FETCH_USER_SUCCESS,
    user,
  };
};

export const fetchUserFailed = () => {
  return { type: FETCH_USER_FAILED };
};

const initialState = {
  loading: false,
  users: [],
};

export const githubReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_USER_SUCCESS:
      const { user } = action;
      return {
        loading: false,
        users: [
          ...state.users,
          {
            id: user.id,
            username: user.login,
            name: user.name,
            url: user.html_url,
            bio: user.bio,
            image: user.avatar_url,
          },
        ],
      };
    case FETCH_USER_FAILED:
      return {
        ...state,
        loading: false,
      };
    case LOADING:
      return {
        ...state,
        loading: true,
      };
  }
  return state;
};

const selectGitHub = (state) => state.githubReducer;
export const selectUsers = createSelector(selectGitHub, (state) => state.users);
export const selectLoading = createSelector(
  selectGitHub,
  (state) => state.loading
);

Configure store

Here is the configuration for the store. We are yet to create the saga configuration though. Saga is applied as a middleware to the store.
import {
  applyMiddleware,
  combineReducers,
  legacy_createStore as createStore,
} from "redux";
import createSagaMiddleware from "redux-saga";

import { githubReducer } from "./github";
import githubSaga from "./github-saga";

const sagaMiddleware = createSagaMiddleware();
const rootReducer = combineReducers({ githubReducer });
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(githubSaga);

export default store;

Provider in index

Now, as usual, we configure the Provider for the store.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

App

Let's visit the App.jsx as well. There is nothing special in this as well. Note that we are dispatching fetchUser function within this.
import { useDispatch, useSelector } from "react-redux";
import "./App.css";
import { GitHubUser } from "./github/GitHubUser";
import { GitHubInput } from "./github/GitHubInput";
import { fetchUser, selectLoading, selectUsers } from "./store/github";

function App() {
  const users = useSelector(selectUsers);
  const loading = useSelector(selectLoading);
  const dispatch = useDispatch();

  const onAddUser = (username) => {
    dispatch(fetchUser(username));
  };

  return (
    <div className="App">
      <h3 className="title">GitHub Users</h3>
      <GitHubInput handleSubmit={onAddUser} />
      {loading && <div>Loading...</div>}
      <hr />
      {users.map((user) => (
        <GitHubUser github={user} key={user.id} />
      ))}
    </div>
  );
}

export default App;
Up to now, we saw the usual stuff. Nothing special other than configuring saga. It is time to write the actual saga effect.

Saga effect

Now, let's have a look at the saga effect.
import { call, put, takeLatest } from "redux-saga/effects";
import { getGitHubUser } from "../utils/github-utils";
import {
  fetchUserFailed,
  fetchUserSuccess,
  FETCH_USER_REQUESTED,
  LOADING,
} from "./github";

function* fetchUser(action) {
  yield put({ type: LOADING });
  const [error, user] = yield call(getGitHubUser, action.username);
  if (!error) {
    yield put(fetchUserSuccess(user.data));
  } else {
    yield put(fetchUserFailed());
  }
}

function* githubSaga() {
  yield takeLatest(FETCH_USER_REQUESTED, fetchUser);
}

export default githubSaga;
There are a couple of things to pay attention to here. These come with testability in mind with the support of generators. Instead of direct calls like dispatch or HTTP calls, this gives us easy unit testability.
  • call - creates a plain object describing the function call. The redux-saga middleware takes care of executing the function call and resuming the generator with the resolved response
  • put - creates a plain JavaScript Object to instruct the middleware that we need to dispatch some action, and let the middleware perform the real dispatch
  • takeLatest - starts a new saga task in the background. automatically cancels any previous saga task started previously if it's still running
That is for today. Not many explanations though.

Happy learning guys ☺

References

https://redux-saga.js.org/
https://redux-saga.js.org/docs/introduction/GettingStarted/
https://redux-saga.js.org/docs/basics/DeclarativeEffects

Comments