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;
Top comments (4)
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 aJwtMiddleware
which automatically adds JWT to each request, I also useRefreshTokenMiddleware
which refreshes jwt when it's expired, I sometimes useMockMiddleware
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:
the return type of
post.list()
isPromise<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.Do you happen to have any examples on this approach? Seems interesting.
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:
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))
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.