How do you handle asynchronous requests in ReactJS?
Tags: javascript, react, redux, typescript
Published: 2019-11-23 13:56:39

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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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;
```