Asynchronous JavaScript , then Asynchronous React . It is now time for Asynchronous Redux.

joker

When we call an asynchronous API, there are three crucial states in time: the state before we start the call, the state between the moment a call start and the moment an answer received, and the state after we receive the answer.

STATE(before call) →STATE(during call) →STATE(answer received)

Each of these three states usually will be obtained by dispatching normal actions that will be processed by reducers synchronously.

Generally for any API request you'll want to dispatch at least two different kinds of actions:

An action informing the reducers that the request began.

The reducers may handle this action by toggling a Loading flag in the state. This way the UI knows it's time to show a spinner or in my App I just show "Loading..." .

An action informing the reducers that the request finished successfully.

The reducers may handle this action by merging the new data into the state they manage and resetting Loading. The UI would hide the spinner, and display the fetched data.

Without middleware, Redux store only supports synchronous data flow. Thus, without any middleware, our action creator function must return plain object only.

funcfunc

mvc

Redux Thunk is middleware for Redux. It basically allows us to return function instead of objects as an action.

If Redux Thunk middleware is enabled, any time we attempt to dispatch a function instead of an action object, the middleware will call that function with dispatch method itself as the first argument.

Previously

//components/Example.js

const Example = (props) => {
  //...
  const dispatch = useDispatch;
  //...
  dispatch({ type: "a type", payload: something });
};

With Thunk

//action.js
const action = {};

action.thunkAction = (props) => (dispatch) => {
  dispatch({ type: "dispatch was sent from caller", payload: "hah" });
};

//example.js
const Example = (props) => {
  //...
  const dispatch = useDispatch;
  //...
  dispatch(action.thunkAction);
};

daw

//action.js file

const asyncLogic = (props) => async (dispatch) => {
  dispatch({ type: "start_async_logic", payload });
  try {
    const res = await someAsyncAPI;
    dispatch({ type: "success_async_logic", payload: res });
    console.log("other side effects");
  } catch (error) {
    dispatch({ type: "failure_async_logic", payload: error });
  }
};
export const action = {
  asyncLogic,
};
//reducer.js

const initialState = {
  data: [],
  isLoading: false,
};
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case `start_async_logic`:
      return { ...state, isLoading: true };
    case `success_async_logic`:
      return { ...state, data: action.payload, isLoading: false };
    case `failure_async_logic`:
      return { ...state, isLoading: false };
    default:
      return state;
  }
};

export default reducer;

Here is the offical documentation of Redux-Thunk

Requirements

git clone https://github.com/coderschool/book-store-redux
cd book-store-redux
yarn add redux react-redux redux-thunk redux-devtools-extension
yarn

Run yarn dev to start the server.

Open another terminal, run yarn start to start the React app.

|- src/
    |- ...
    |- redux/
        |- actions/
            |- cart.actions.js
            |- book.actions.js
        |- constants/
            |- cart.constants.js
            |- book.constants.js
        |- reducers/
            |- cart.reducer.js
            |- book.reducer.js
            |- index.js
        |- store.js
// ./src/redux/store.js
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};
const store = createStore(
  rootReducer,
  initialState,
  composeWithDevTools(applyMiddleware(thunk))
);

export default store;
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

import { Provider } from "react-redux";
import store from "./redux/store";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Define booksReducer in ./src/redux/reducers/books.reducer.js:

import * as types from "../constants/books.constants";

const initialState = {
  books: [],
  loading: false,
  readingList: [],
  selectedBook: null,
};

const booksReducer = (state = initialState, action) => {
  const { type, payload } = action;

  switch (type) {
    default:
      return state;
  }
};

export default booksReducer;

This will allow us to silo off different reducers for different pieces of state.

import { combineReducers } from "redux";
import booksReducer from "./books.reducer";

export default combineReducers({
  books: booksReducer,
});

[domain].constants.js

To both reduce typing and mistakes, we'll define constants that hold the names of our actions (such as GET_BOOKS_REQUEST) in [domain].constants.js. Example:

// book.constants.js
export const GET_BOOKS_REQUEST = "BOOK.GET_BOOKS_REQUEST";
export const GET_BOOKS_SUCCESS = "BOOK.GET_BOOKS_SUCCESS";
export const GET_BOOKS_FAILURE = "BOOK.GET_BOOKS_FAILURE";

[domain].actions.js

In ./src/redux/actions/, create book.actions.js:

import * as types from "../constants/books.constants";

// the middleware functions will be here

const bookActions = {};
export default bookActions;

Here is an example of the book list fetching feature that happens on the home page:

// books.constants.js
export const GET_BOOKS_REQUEST = "BOOK.GET_BOOKS_REQUEST";
export const GET_BOOKS_SUCCESS = "BOOK.GET_BOOKS_SUCCESS";
export const GET_BOOKS_FAILURE = "BOOK.GET_BOOKS_FAILURE";
import { toast } from "react-toastify";

import * as types from "../constants/books.constants";

import api from "../../apiService";

const getBooks = (pageNum, limit, query) => async (dispatch) => {
  dispatch({ type: types.GET_BOOKS_REQUEST, payload: null });
  try {
    let url = `${process.env.REACT_APP_BACKEND_API}/books?_page=${pageNum}&_limit=${limit}`;
    if (query) url += `&q=${query}`;

    //without axios
    const res = await fetch(url);
    const data = await res.json();
    //---------------
    //with axios
    const data = await api.get(url);
    //---------------
    dispatch({ type: types.GET_BOOKS_SUCCESS, payload: data.data });
  } catch (error) {
    toast.error(error.message);
    dispatch({ type: types.GET_BOOKS_FAILURE, payload: error });
  }
};
import * as types from "../constants/books.constants";
const initialState = {
  books: [],
  loading: false,
  readingList: [],
  selectedBook: null,
};

const booksReducer = (state = initialState, action) => {
  const { type, payload } = action;

  switch (type) {
    case types.GET_BOOKS_REQUEST:
      return { ...state, loading: true };
    case types.GET_BOOKS_SUCCESS:
      return { ...state, books: payload, loading: false };
    case types.GET_BOOKS_FAILURE:
      return { ...state, loading: false };
    default:
      return state;
  }
};

export default booksReducer;

In HomePage.js, this is the useEffect you can use to fetch data from the API:

useEffect(() => {
  dispatch(bookActions.getBooks(pageNum, limit, query));
}, [dispatch, pageNum, limit, query]);

You will also need to import everything that is needed.

import { useDispatch, useSelector } from "react-redux";
import bookActions from "../redux/actions/books.actions";

Now work on the rest of the features using this same flow!

preview

axios has become undeniably popular among frontend developers. Axios is a promise based HTTP client for the browser and Node.js. Axios makes it easy to send asynchronous HTTP requests to REST endpoints.

Installation

npm i axios
import axios from "axios";

const api = axios.create({
  baseURL: process.env.REACT_APP_BACKEND_API,
  headers: {
    "Content-Type": "application/json",
  },
});

/**
 * console.log all requests and responses
 */
api.interceptors.request.use(
  (request) => {
    console.log("Starting Request", request);
    return request;
  },
  function (error) {
    console.log("REQUEST ERROR", error);
  }
);

api.interceptors.response.use(
  (response) => {
    console.log("Response:", response);
    return response;
  },
  function (error) {
    error = error.response.data;
    console.log("RESPONSE ERROR", error);

    // Error Handling here

    return Promise.reject(error);
  }
);

export default api;

How to use?

import api from "apiService";

// Examples:
const res = await api.post("/auth/login", { email, password });

const res = await api.get(`/blogs?page=${pageNum}&limit=${limit}`);

const res = await api.put(`/blogs/${blogId}`, { title, content, images });

const res = await api.delete(`/blogs/${blogId}`);

api.defaults.headers.common["Authorization"] = AUTH_TOKEN;