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.