DEV Community

loading...

How do you handle asynchronous requests in ReactJS?

potouridisio profile image Ioannis Potouridis ・3 min read

Hey! πŸ‘‹

With React Suspense being far from reality (stable) I wanted to make a

short article to show you how I currently handle my asynchronous requests

in ReactJS, and hopefully exchange opinions in the comment section.

I know that there are a lot of hooks for fetching resources out there.

I'm a big fan of hooks but I don't find this approach much versatile yet.

To start with, I create a wrapper function for fetch usually in a

helpers.ts file.

// helpers.ts

/**
 *
 * @param {RequestInfo} input
 * @param {RequestInit} [init]
 * @returns {Promise<T>}
 */
export async function createRequest<T>(
  input: RequestInfo,
  init?: RequestInit
): Promise<T> {
  try {
    const response = await fetch(input, init);
    return await response.json();
  } catch (error) {
    throw new Error(error.message);
  }
}

Nothing fancy here.

Where I work, we usually implement filtering, sorting and pagination in

the back-end so most of my API related functions expect key/value pairs

as search parameters. This is how I do it.

// userApi.ts
const { HOST, SCHEME } = process.env; 

const PATH = 'api/v1/users';

export interface User {
  createdAt: string;
  email: string;
  firstName: string;
  id: number;
  lastName: string;
  updatedAt: string;
}

/**
 *
 * @param {Record<string, string>} [init]
 * @returns {Promise<User[]>}
 */
export function fetchUsers(
  init?: Record<string, string>
): Promise<User[]> {
  const searchParams = new URLSearchParams(init);
  const QUERY = searchParams.toString();
  const input = `${SCHEME}://${HOST}/${PATH}?${QUERY}`;

  return createRequest<User[]>(input);
}

I recently started using URLSearchParams in order to construct

my query strings, thus being declarative.

Next, I prepare my actions and reducer.

In order to handle my async actions in Redux I create a middleware to

handle async payloads and dispatch separate actions for each state of

the async action. To keep it short, I'll use redux-promise-middleware.

It does exactly that.

With that, here's how the actions.ts file looks.

// actions.ts
import { FluxStandardAction } from "redux-promise-middleware";

import * as userApi from './userApi';

/**
 *
 * @param {Record<string, string> | undefined} [init]
 * @returns {FluxStandardAction}
 */
export function fetchUsers(
  init?: Record<string, string>
): FluxStantardAction {
  return {
    type: 'FETCH_USERS',
    payload: userApi.fetchUsers(init)
  }
}

Remember, our middleware will transform the actions that have async

payload and will dispatch separate fulfilled, rejected and pending actions.

This is how I handle those actions.

// reducer.ts
import { FluxStandardAction } from "redux-promise-middleware";

import { User } from './userApi'; 

export interface UserListState {
  users: User[];
  usersPending: boolean;
}

const initialState: UserListState {
  users: [];
  usersPending: false;
}

/**
 *
 * @param {UserListState} state
 * @param {FluxStandardAction} action
 * @returns {UserListState}
 */
function userList(
  state: UserListState = initialState,
  action: FluxStandardAction
): UserListState {
  switch(action.type) {
    case "FETCH_USERS_FULFILLED":
      return {
        ...state,
        users: action.payload.users,
        usersPending: false
      }
    case "FETCH_USERS_PENDING":
      return {
        ...state,
        usersPending: true
      }
    case "FETCH_USERS_REJECTED":
      return {
        ...state,
        usersPending: false
      }
    default:
      return state;
  }
}

export default userList;

I then create the selector functions in order to extract data

from the Redux store state.

// selectors.ts
import { State } from '../wherever/this/guy/is';
import { User } from './userApi';

/**
 *
 * @param {State} state
 * @returns {User[]}
 */
export function usersSelector({ userList }: State): User[] {
  return userList.users;
}

/**
 *
 * @param {State} state
 * @returns {boolean}
 */
export function usersPendingSelector({ userList }: State): boolean {
  return userList.usersPending;
}

Finally, I create the React component in order to display the users.

// user-list.tsx
import React, { useEffect, useState }  from 'react';
import { useDispatch, useSelector } from 'react-redux'; 

import { fetchUsers } from './actions'; 
import { stringifyValues } from './helpers'; // it does what it says
import { usersSelector, usersPendingSelector } from './selectors';

type Params = Record<string, number | string>;

const initialParams: Params = {
  limit: 10,
  page: 1
};

/**
 *
 * @returns {JSX.Element}
 */
function UserList(): JSX.Element {
  const dispatch = useDispatch();
  const users = useSelector(usersSelector);
  const usersPending = useSelector(usersPendingSelector);
  const [params, setParams] = useState<Params>(initialParams);

  useEffect(() => {
    dispatch(fetchUsers(stringifyValues(params));
  }, [dispatch, params];

  // Nothing fancy to see here except 
  // some handlers that update the params 
  // e.g. setParams(prev => ({ ...prev, page: 2 }));
  // and some conditional rendering.
}

export default UserList;

Discussion

pic
Editor guide
Collapse
cyberhck profile image
Nishchal Gautam

I handle API calls completely differently.
First I don't wrap fetch around something, I use a library @crazyfactory/tinka, it's not popular, but it has functionalities like, adding a default baseUrl, and ability to use middlewares.

I add a few middlewares, like WrapMiddleware which does a .json() on response, I also add a JwtMiddleware which automatically adds JWT to each request, I also use RefreshTokenMiddleware which refreshes jwt when it's expired, I sometimes use MockMiddleware when I just want to play around in frontend and don't want to build backend yet.

Another huge thing I do for production is that, I don't add api calls to my projects, I build a SDK which uses these, so api calls look like this:

const api = Api.getInstance();
const posts = await api.posts.list();
// or:
const response = await api.posts.create({body: ""}); // tserror: title is required

the return type of post.list() is Promise<Post[]>, all API calls are strongly typed.

Now I don't make API calls from components, because they're not supposed to be from there, I make API calls from a side effect layer, I use redux-saga for that.

Collapse
potouridisio profile image
Ioannis Potouridis Author

Do you happen to have any examples on this approach? Seems interesting.

Collapse
cyberhck profile image
Nishchal Gautam

I don't have a running example which you can just clone and start using unfortunately, I do have some projects to point you in that direction:

  • Normally SDKs can be generated if you write your APIs right, just using swagger and using autorest to generate all sdks, every time you update your API, it can automatically publish the sdk to npm.
  • Or you can handwrite your own SDK, like here: github.com/devcurate/sdk (is an example), I've used a lot of automation so I don't have to make a release every time I want to use it.
  • That sdk, accepts an instance of Client which comes from tinka

(I've a project where I didn't create sdk separately, but directly in the project itself, because it had only two endpoints: github.com/cyberhck/EventCheckIn-C...) there I configure everything.

github.com/cyberhck/EventCheckIn-C... here you can see that every time a particular action is dispatched, we perform a side effect.

(side effect can be, when user clicks on a message, clear the message notification icon, or making an api call, (or even make completely different api call according to business logic))

Collapse
potouridisio profile image
Ioannis Potouridis Author

Hey James,

I work on a project with redux-observable and I believe it requires too much boilerplate code for simple async actions.

I prefer the simplicity of promise middleware, I never tried redux-saga though. I need to have a look at that.

I love Apollo but where I work we don't serve GraphQL yet. I had a little experience in a side project though and it was amazing.