For a long time now, React has required us to use separate processes when we want remote data to live in component state vs redux. With hooks, we can use a single, simple mechanism to do either. This was originally a presentation given at the Downtown ReactJS Meetup in Austin, TX. Here are the video of the original presentation, the slides seen in the presentation, and the todo list demo (← all examples in this post are from this repo).
How have we handled requests?
Historically, there have been two main approaches to making remote requests in React. If the request is only relevant to a single component scope, you just make the request within the component and store the state of the request in the component’s state. If you need the state of the request to be more widely available, you store it in Redux - maybe even make the request with redux using something like Thunk. These two approaches are very different; so, if your app requires both types, you’ll have two very different processes for making requests.
In component state
function MyComponent() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [data, setData] = useState([]);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(
result => {
setIsLoaded(true);
setData(result);
},
error => {
setIsLoaded(true);
setError(error);
}
)
}, []);
return isLoaded ? (
<DataChart data={data} />
) : (
<LoadingScreen />
);
}
Making requests within a component is certainly the most flexible of these two approaches. Each component can make and handle the request exactly as it needs, and it’s very transparent - anyone reading the component can see the whole story. Unfortunately, that flexibility can also lead to poor reusability and a lot of duplicate code. There is also no global awareness of the request state without adding significant complexity to get the values into redux or closely managing prop inheritance. Finally, it’s very easy to have multiple components requesting the same data on large projects.
In Redux
function getData() {
return dispatch => {
dispatch(getDataStarted());
fetch('https://some.example.com/data')
.then(response => response.json())
.then(response => {
dispatch(getTodoSuccess(response))
})
.catch(error => {
dispatch(getTodoFailure(error))
})
}
}
On the contrary, making requests within redux actions ensures global awareness and great reusability. A central process for getting or setting any remote item is certainly the goal. But, now we’ve locked ourselves in to a process that only works with Redux. If only a single component needs access to a piece of information, we’ve committed ourselves to either add significant overhead to keep this data in redux, or circumvent our otherwise global standard to make the request within the component.
After trying both of these methods in combination, and thanks in large part to the success of React Hooks, I believe that I have finally landed on the best option for handing requests within react - the useApi hook pattern.
What is the useApi hook?
The useApi hook is a custom React Hook that is the center of an extremely flexible, transparent, efficient, and simple pattern for making all remote requests in a React application. The hook itself takes two arguments: a requestBuilder, and an options object. It outputs an array with 3 values - the responseState, a makeRequest function, and a reset function. We’ll talk through each of those in a moment, but first, here is the hook itself.
import { useState, useEffect } from 'react';
const processError = async (response, options = {}) => {
const { errorMessage } = options;
// Try to extract the error message from the body
try {
// Get the error as json
const error = await response.json()
// Reject with the error
return Promise.reject(error)
} catch (e) {
// Reject with the default message
return Promise.reject(errorMessage || 'Something went wrong')
}
};
export const initialResponse = {
pristine: true,
data: null,
pending: false,
complete: false,
error: null
};
export default function useApi(apiFn = i => i, options = {}) {
// Initialize the response state
const [response, setResponse] = useState(
options.initialResponse || initialResponse
);
// Initialize the request state
const [request, setRequest] = useState();
// Watch for changes in the request to fire off
// api requests
useEffect(() => {
const makeRequest = async _request => {
if (!_request) return;
setResponse({
pristine: false,
data: null,
pending: true,
complete: false,
error: null
});
const { url, ...requestOptions } = _request;
const rawResponse = await fetch(url, requestOptions);
if (rawResponse.ok) {
const data = await rawResponse.json();
setResponse({
pristine: false,
data,
pending: false,
complete: true,
error: null
});
} else {
const error = await processError(rawResponse);
setResponse({
pristine: false,
data: null,
pending: false,
complete: true,
error
});
}
};
makeRequest(request);
}, [request]);
return [
response,
(...args) => setRequest(apiFn(...args)),
() => setResponse(initialResponse)
];
}
Arguments
const [responseState, makeRequest, reset] = useApi(requestBuilder, options);
The useApi hook takes two arguments: a requestBuilder, and an options object. A requestBuilder is a function that returns a request object, i.e. the object that defines a request in axios, or fetch, or ‘ajax’, or whatever your HTTP Request library of choice happens to be. I, personally, like to keep my requestBuilder functions in the Domain Model model classes for my application. For example, the Label.api.list method in the code sample below.
import {API_BASE_URL} from "../constants/api";
export default class Label {
constructor({
name = '',
color = 'gray'
}) {
this.name = name;
this.color = color;
}
static create(data) {
return Object.freeze(new Label(data));
}
static api = {
list() {
return {
url: `${API_BASE_URL}/labels.json`,
method: 'get'
}
}
}
}
The second argument - the options object - is not something that I personally use much in any of my applications because I use the fetch api. In the example above, you’ll see that an initialResponse option can be passed in to be the initial state for the response variable (most likely for when the data is stored in redux). However, I recognized that other libraries can allow configurations outside of what is generated by the requestBuilder. For example, an axios user could provide an optional instance of the axios function that has special interceptors or other special configurations that won’t apply to all requests.
Output
const [responseState, makeRequest, reset] = useApi(requestBuilder, options);
The useApi hook outputs an array containing 3 values: the responseState, a makeRequest function, and a reset function.
responseState
The responseState is probably the most beautiful part of this pattern. It encapsulates the complete state for any request in 5 values:
const { pristine, complete, pending, data, error } = responseState;
pristine- boolean - No requests like this one have been made?complete- boolean - The latest request is done?pending- boolean - The latest request is still happening?data- any - The response body of a successful request (or null)error- any - The response body of a failed request (or null)
These responseState properties can be stored in a component’s local state and used freely, or they can go in a dispatch off to redux any time they update - all in one, nice, consistent package.
makeRequest
The makeRequest function is essentially a wrapper around the requestBuilder argument. Calling the makeRequest function with the arguments expected by your requestBuilder simply takes the output of the requestBuilder function and sets it as the request state property in the useApi hook (which kicks off the request).
reset
The reset function just sets the response state in the hook back to the pristine initial response. This is used very rarely in my applications, but it is nice to have it when you need it.
How should it be implemented?
How can we take this great pattern for making requests, and use it in an extensible, maintainable, understandable, and efficient way? The answer, in the world of React Hooks, is always use hooks! Any piece of information that you need to get from a server should have its own hook. Earlier, when we were discussing the requestBuilder function, I showed a model class for an object called Label. In the todo list demo application where that class lives, there is also a useLabels hook. Labels are a global concept in that app; so, naturally, their state is stored in Redux. Here is the useLabels hook from that app.
import {useEffect} from 'react';
import {useDispatch, useSelector} from "react-redux";
import useApi from "../../../hooks/useApi";
import {set, selectLabels} from "../labelsSlice";
import Label from "../../../models/Label";
import useLockout from "../../../hooks/useLockout";
export default function useLabels({ auto = true } = {}) {
const [getCheckLockout, lockout] = useLockout('labels');
// Dispatch from redux...
const dispatch = useDispatch();
// Lists from redux...
const labels = useSelector(selectLabels);
// The useApi hook returns a response object with
// the properties complete, pending, data, and error;
// and a function that initiates the HTTP request when
// it is called.
const [responseState, getLabels] = useApi(Label.api.list);
// On mount, if the response in redux isn't complete,
// request the labels
useEffect(() => {
if (auto && !getCheckLockout() && labels.pristine) {
lockout();
getLabels();
}
// eslint-disable-next-line
}, []);
// If the response isn't already complete in redux, update it
// with each change to the local response state
useEffect(() => {
if (
!responseState.pristine && (
labels.complete !== responseState.complete ||
labels.pending !== responseState.pending
)
) {
dispatch(set(responseState));
}
}, [responseState, labels, dispatch]);
// Return the redux response state
return [labels, getLabels];
}
There is quite a bit going on in this particular hook. There is a useLockout concept that you can check out here. There is an onMount-style useEffect that automatically fetches the labels for the first component that uses this hook. There is some redux work that syncs the hook’s responseState with redux when the instance of the hook is the one making the request. All of that comes together so that absolutely every component that needs to use the labels can do it with one, single line:
const [labelState] = useLabels();
and they are guaranteed access to the data.
import React, {useMemo} from 'react';
import PropTypes from 'prop-types';
import LabelPill from "../LabelPill";
import useLabels from "../../hooks/useLabels";
import Loader from "../Loader";
import './index.css';
export default function LabelPicker({ onClick = () => {}}) {
const [{ data: labels, pending }] = useLabels();
return pending ? (
<Loader />
) : (
<ul className="LabelPicker">
{labels && labels.map(label => (
<li key={label.name} className="LabelPicker-item">
<LabelPill label={label} onClick={onClick} />
</li>
))}
</ul>
)
}
LabelPicker.propTypes = {
onClick: PropTypes.func
}
Take a second to think about that. You don’t have to make sure that a component is inside of some specific provider or has a specific parent passing the data as a prop. You don’t have to do a ton of orchestration to pull down the data all at once. When a component using this hook needs this data, it will appear, regardless of how many instances there are. It’s magic!
Of course, all of that is optional. You should absolutely tailor these hooks to your needs. The example above is just the most common implementation in my apps for data that needs to be shared. But that leads us to one of the best things about the useApi hook. You can make a hook like the one above and instantly have full context in Redux (or some other provider context), OR you can use the exact same tool to use data in a single component’s state. The same pattern is flexible enough for every need!
Closing Thoughts
React has gone through a lot of iterations. Those iterations have had all kinds of patterns for handling HTTP requests - centralized, decentralized, homogenous, heterogeneous, data layer, no data layer… But in the world of hooks, the best pattern that I have found is the useApi pattern. The pattern includes:
- Defining your app’s API using
requestBuilderfunctions - Creating individual hooks that wrap
useApifor each type of object that you interact with (e.g. useLists, useUpdateList, useDeleteList, useItems, etc) - Using the response object throughout your app (
{ pristine, complete, pending, data, error })
If you are starting a project from scratch, I highly recommend following this pattern (along with the Domain Model pattern) from the get go. If your project is already going, it’s never too late to start! The next time that you need a new endpoint, create it as a requestBuilder function. Build a hook to use that endpoint, and use the response object in your new components. Every time you modify an endpoint or a component, update it to use this pattern and leave things better than you found them.