Ali Ismael
The Startup
Published in
6 min readSep 3, 2020

--

Handle RESTful Requests Data in React/Redux Apps

Handle RESTful Requests Data in React/Redux Apps

The idea is to handle modeling, fetching, and displaying RESTful requests data (Remote data) in React/Redux apps.

As a web developer, I have to deal with remote sources (e.g., HTTP) in my apps. During working with react, I was following an ordinary behavior by making the HTTP requests and then store the fetched data in the application store (e.g., redux store).

It was a simple data type that maps the API response data. For example, if I fetch a list of users, each user has some properties (id, username, gender, etc…). The datatype that would represent that response is an array of User where User is the data type representing the user details.

type User {
id: string,
username: string,
gender: 'Male' | 'Female'
.
.
.
}
State = {
users: User[]
}

Suppose we want to display a loading placeholder while data is fetching and an empty view if the list of users is empty. The previous implementation (representation of data) doesn’t have much data that we can use to do so. We can add a loading flag to the State. Then in the UI, we check this flag before displaying the data.

type User {
id: string,
username: string,
gender: 'Male' | 'Female'
.
.
.
}
State = {
loading: true,
users: User[]
}

There is no guarantee that you have to check this flag before displaying the list of users. If someone forgot, In slow connectivity, The empty placeholder may appear for seconds while data is loading. The previous implementation does not represent remote data well. So we need to find an explicit data model to represent RESTful requests data well in our application.

The following section inspired by RemoteData in elm.

RemoteData type

RESTful requests have one of four states:

  • We haven’t asked for the data yet.
  • We’ve asked, but we haven’t got a response yet.
  • We got a response, but there was an error.
  • We got a response, and here’s the data you asked.

First, we will wrap our remote data type in a new model called RemoteData

Defining RemoteData type

RemoteData<T, E> where T is the data type and E is the error type respectively.

So our state representation of users list example explained above will be
Here we assume that our error type is a string

State = {
users: RemoteData<User[], string>
}

When we fetch a list of users from an API endpoint, there are four different states users can be:

  • NOT_ASKED - You didn’t ask for the data yet
  • LOADING - You asked for the data but it’s not fetched yet.
  • REJECT - Something went wrong. Here's the error.
  • SUCCESS - Data fetched successfully. Here’s the requested data.

And that’s it, You have now a better representation of remote data that will lead to a better UI.

API request wrapper

Since we now have a good representation of remote data, It’s time to handle the fetching of it and updating the application store. When calling an API endpoint to fetch data, we’ll need to update the application store, and since updating the application store in redux requires dispatching an action to tell reducer to update the store, I’ve created a helper function called api request wrapper based on Axios to do the job of making the HTTP requests and dispatching the reasonable action in each state of the HTTP request (loading, reject, and success).

Defining api request wrapper

api<T, E>(config) where T, E are the types of data and the expected error respectively

api<User[], ErrorResponse>({
method: 'GET',
url: 'users',
baseURL: 'https://jsonplaceholder.typicode.com/',
action: FETCH_USERS,
});

Request Config

In addition to Axios request config there are three more options:

  • action: is the action type that will get dispatched when the request state changed. If it’s not provided no action will get dispatched.
  • onSuccess, onError: are the callbacks to be triggered for the relevant request state.

The possible actions that can get dispatched from api request wrapper are:

  • LOADING - You asked for the data but it’s not fetched yet.
  • REJECT - Something went wrong. Here's the error.
  • SUCCESS - Data fetched successfully. Here’s the requested data.

The attribute that differentiates between loading, reject, and success of an action is the kind attribute.

Here’s some example of loading, reject, and success of fetch users list action.

// Loading
{
action: 'FETCH_USERS',
kind: RemoteKind.Loading,
}
// Reject
{
action: 'FETCH_USERS',
kind: RemoteKind.Reject,
error: 'Something went wrong'
}
// Success
{
action: 'FETCH_USERS',
kind: RemoteKind.Success,
data: [{...}, {...}]
}

Defining Action type

Action<T, E> where T is the data type and E is the error type respectively

Instead of handling those actions by yourself for each HTTP request, api request wrapper will do that for you.

Suppose you want to create an action for fetching users. Here’s an example that shows how the action file would look like with and without using the api request wrapper.

Without using the api request wrapper, Your action file would be:

// action.tsconst FETCH_USERS = 'FETCH_USERS'
const FETCH_USERS_FAILED = 'FETCH_USERS_FAILED'
const FETCH_USERS_FULFILLED = 'FETCH_USERS_FULFILLED'
const fetchUsers = () => (dispatch) => {
dispatch({type: 'FETCH_USERS'})
axios({
method: 'GET',
url: 'posts',
baseURL: 'https://jsonplaceholder.typicode.com/',
})
.then(res => dispatch({
type: FETCH_USERS_FULFILLED,
payload: res.data
})
.catch(err => dispatch({
type: FETCH_USERS_FAILED,
payload: err
})
}

By using the api request wrapper, it will be simpler, Just need to pass an extra param action that will get dispatched when the request state changed.

kind is the attribute differentiates between different request states (loading, reject, and success) of an action.

// actions.tsconst FETCH_USERS = 'FETCH_USERS'
const fetchUsers = () =>
api<User[], ErrorResponse>({
method: 'GET',
url: 'users',
baseURL: 'https://jsonplaceholder.typicode.com/',
action: FETCH_USERS,
});

Fetching data reducer

Let’s catch the dispatched actions and update the application store. I’ve created a reducer that can handle the different actions for any remote data and update the store.

Defining fechingReducer

fetchingReducer<T, E>(actionType) a reducer for managing the state of the remote data.

import { fetchingReducer } from 'remote-data';combineReducers({
users: fetchingReducer<User[], ErrorResponse>(FETCH_USERS),
});
  • actionType: it should be the same as the action passed to the api request wrapper.

Creating a custom reducer to manually update the store (Optional)

You can create your custom reducer, here’s an example:

import { RemoteData, RemoteKind, Action } from 'remote-data';
import { User, ErrorResponse } from '../../models';
import { FETCH_USERS } from './constants';

export type UsersStore = {
users: RemoteData<User[], ErrorResponse>;
};

const initialState: UsersStore = {
users: {
kind: RemoteKind.NotAsked,
},
};

export default (
state: UsersStore = initialState,
action: Action<User[], ErrorResponse>,
): UsersStore => {
if (action.type === FETCH_USERS) {
switch (action.kind) {
case RemoteKind.Loading:
return {
...state,
users: {
kind: RemoteKind.Loading,
},
};

case RemoteKind.Success:
return {
...state,
users: {
kind: RemoteKind.Success,
data: action.data,
},
};

case RemoteKind.Reject:
return {
...state,
users: {
kind: RemoteKind.Reject,
error: action.error,
},
};

default:
return state;
}
}

return state;
};
  • Initialize your state
  • Verify the action type and kind
  • Update your state

action is of type Action<T, E>

Remote Component

The last part is to handle the display of remote data in its different states.
I’ve created a component that takes your remote data and the views that you want to display in each state Loading, Success, and Reject.

<RemoteComponent
remote={{ users }}
loading={UsersLoading}
reject={({ users }) => <UsersError error={users.error} />}
success={({ users }) => <ListUsers users={users.data} />}
/>

I’ve created a small react library that provides:

Install

npm i @alismael/remote-data

Usage

Performing a GET request to fetch the data

import { api } from 'remote-data';
import { Post, ErrorResponse } from '../models';
import { FETCH_POSTS } from './constants';
const fetchPosts = () =>
api<Post[], ErrorResponse>({
method: 'GET',
url: 'posts',
baseURL: 'https://jsonplaceholder.typicode.com/',
action: FETCH_POSTS,
});

Adding a reducer to update the store

import { Reducer } from 'react';
import { combineReducers } from 'redux';
import { fetchingReducer, RemoteData } from 'remote-data';
import { Post, ErrorResponse } from '../../models';
import { FETCH_POSTS } from './constants';
export type PostsStore = {
posts: RemoteData<Post[], ErrorResponse>;
};
const postsReducer: Reducer<PostsStore, any> = combineReducers({
posts: fetchingReducer<Post[], ErrorResponse>(FETCH_POSTS),
});
export default postsReducer;

Displaying your remote data

const PostsLoading = () => <>Loading posts...</>;
const PostsError = ({ err }: { err: ErrorResponse }) => <>{err}</>;
const ListPosts = ({ data }: { data: Post[] }) => <>Here you can use the fetched data</>
type PostsContainerProps = {
fetchPosts: () => Promise<Post[]>;
posts: RemoteData<Post[], ErrorResponse>;
};
const PostsContainer = ({ fetchPosts, posts }: PostsContainerProps) => {
React.useEffect(() => {
fetchPosts();
}, [fetchPosts]);
return (
<RemoteComponent
remote={{ posts }}
loading={PostsLoading}
reject={({ posts }) => <PostsError error={posts.error} />}
success={({ posts }) => <ListPosts posts={posts.data} />}
/>
);
};
const mapStateToProps = ({ posts }: StoreState) => ({
posts: posts.posts,
});
const mapDispatchToProps = (
dispatch,
) => ({
fetchPosts: () => dispatch(fetchPostsAction()),
});
connect(mapStateToProps, mapDispatchToProps)(PostsContainer);

GitHub: https://github.com/alismael/remote-data
NPM: https://www.npmjs.com/@alismael/remote-data

And that’s it. Thanks for reading.

--

--