Asynchronous JavaScript , then Asynchronous React . It is now time for Asynchronous Redux.
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.
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.
//components/Example.js
const Example = (props) => {
//...
const dispatch = useDispatch;
//...
dispatch({ type: "a type", payload: something });
};
//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);
};
//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;
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
.This folder contains all logic for redux including reducers, actions, constants, and the store definition/setup.|- 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;
booksReducer
in ./src/redux/reducers/index.js
: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:
src/redux/constants/book.constants.js
:// 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";
src/redux/actions/book.actions.js
, depending on whether you use axios
or not, add the correct part: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 });
}
};
./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) {
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!
cart.actions.js
, cart.constants.js
, and cart.reducers.js
.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
src/
called apiService.js
. All of the connections to the backend API will go through this API service. Think about it like a city gate.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?
api
from apiService
, then you can use it like an instance of axios
: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;