Switcharoo: create a react-redux-saga app

Background

My team uses react, redux and redux-saga for frontend. I had never used redux-saga (a redux middleware) before I joined this team last month and needed to figure out how it works. Reading and taking down notes is important, but sometimes writing and testing codes are more intuitive to learn.

So I created a very simple react, redux-saga app to switch logo on button click. It utilizes unsplash api.

switcharoo

What is Redux-Saga?

It is a redux middleware which uses ES6 generators to execute asynchronous logic. It is a good way to avoid callback hell.

0. Prerequisite

Go to Unsplash to apply for an API access key.

1. Getting started

Start a react app and install necessary libs

create-react-app switcharoo
cd switcharoo
yarn add axios redux redux-saga react-redux

2. Create folders and files

root
|-- .gitignore
|-- README.md
|-- package.json
|-- yarn.lock
|-- public
|   |-- favicon.ico
|   |-- index.html
|   |-- manifest.json
|   |-- rawr.ico
|-- src
    |-- App.css
    |-- App.js
    |-- directoryList.md
    |-- index.css
    |-- index.js
    |-- logo.svg
    |-- serviceWorker.js
    |-- __tests__
    |   |-- sagas.test.js
    |-- api
    |   |-- .config.js
    |   |-- index.js
    |-- redux
    |   |-- actions.js
    |   |-- constants.js
    |   |-- reducer.js
    |-- sagas
        |-- index.js

3. Use unsplash api

Create a config file under api folder. Don’t forget to add it to your .gitignore! Then, write a function to fetch a random image via Unsplash API.

/* src/api/.config.js */

export const ACCESS_KEY =
  "your_key_here";


/* src/api/index.js */

import axios from 'axios';
import { ACCESS_KEY } from './.config.js';

const unsplash = axios.create({
  baseURL: 'https://api.unsplash.com',
  headers: {
    Authorization: `Client-ID ${ACCESS_KEY}`
  }
});
export const searchPhoto = () => unsplash.get('/photos/random');

4. Time to churn out the usual codes for redux

/* src/redux/constants.js */

export const FETCH_IMAGES_CALLED = "FETCH_IMAGES_CALLED";
export const FETCH_IMAGES_SUCCESS = "FETCH_IMAGES_SUCCESS";
export const FETCH_IMAGES_FAILURE = "FETCH_IMAGES_FAILURE";


/* src/redux/actions.js */

import {
  FETCH_IMAGES_CALLED,
  FETCH_IMAGES_SUCCESS,
  FETCH_IMAGES_FAILURE
} from "./constants";

export const fetchImages = () => {
  return {
    type: FETCH_IMAGES_CALLED
  };
};
export const fetchImagesSuccess = image => {
  return {
    type: FETCH_IMAGES_SUCCESS,
    image
  };
};
export const fetchImagesFailure = error => {
  return {
    type: FETCH_IMAGES_FAILURE,
    error
  };
};


/* src/redux/reducer.js */

import {
  FETCH_IMAGES_CALLED,
  FETCH_IMAGES_SUCCESS,
  FETCH_IMAGES_FAILURE
} from "./constants";

const initialState = {
  fetching: false,
  img: null,
  error: null
};
export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_IMAGES_CALLED:
      return { ...state, fetching: true, error: null };
    case FETCH_IMAGES_SUCCESS:
      return { ...state, fetching: false, img: action.image };
    case FETCH_IMAGES_FAILURE:
      return { ...state, fetching: false, error: action.error };
    default:
      return state;
  }
};

5. Time for the new part! Redux-Saga!

/* src/sagas/index.js */

import { takeLatest, call, put } from "redux-saga/effects";
import { searchPhoto } from "../api/index";
import { FETCH_IMAGES_CALLED } from "../redux/constants";
import { fetchImagesSuccess, fetchImagesFailure } from "../redux/actions";

export function* watcher() {
  // when this action is called, a callback generator function is run
  yield takeLatest(FETCH_IMAGES_CALLED, worker);
}
function* worker() {
  try {
	// instructs middleware to call a function or another saga
	const response = yield call(searchPhoto);
	const image = response.data.urls.regular;
	// instructs middleware to dispatch an action to the Store
    yield put(fetchImagesSuccess(image));
  } catch (error) {
    yield put(fetchImagesFailure(error));
  }
}
export function* testWorker() {
  // this will be used only for unit test purpose
  try {
    yield call(searchPhoto);
    yield put(fetchImagesSuccess({ status: 200 }));
  } catch (error) {
    yield put(fetchImagesSuccess({ error }));
  }
}

6. Finishing touch

/* App.js */

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
import { fetchImages } from './redux/actions';

const mapStateToProps = state => {
  // called every time the store state changes
  return {
    fetching: state.fetching,
    img: state.img,
    error: state.error
  };
};
const mapDispatchToProps = dispatch => {
  // used for dispatching actions to the store
  // this is the only way to trigger a state change
  return {
    onRequest: () => dispatch(fetchImages())
  };
};

export default connect(
  // connect a React component to a Redux store
  mapStateToProps,
  mapDispatchToProps
)(App);

class App extends Component {
  render() {
    const { fetching, img, onRequest, error } = this.props;
    return (
      <div className="App">
        <header className="App-header">
          <img src={img || logo} className="App-logo" alt="logo" />
          <h1 className="app-title">Switcharoo</h1>
        </header>
        {fetching ? (
          <button className="button" disabled>
            Fetching...
          </button>
        ) : (
          <button onClick={onRequest}>NEW PIC</button>
        )}
        {error && <p style={{ color: 'red' }}>Error!</p>}
      </div>
    );
  }
}

Testing

import { call, put } from 'redux-saga/effects';
import { searchPhoto } from '../api';
import { testWorker } from '../sagas';
import { fetchImagesSuccess } from '../redux/actions';

describe('test worker', () => {
  it('handles a successful image fetch', () => {
    const init = testWorker();
    expect(init.next().value).toEqual(call(searchPhoto));
    expect(init.next().value).toEqual(put(fetchImagesSuccess({ status: 200 })));
  });
});

Conclusion

Code looks cleaner because logic is less scattered and a callback hell doesn't exist anymore. Also, it seems easier to unit test now. You can see the complete code here.

Reference

Redux-Saga Official Doc Redux-saga tutorial for beginners and dog lovers

GitHubGitHubLinkedin