The useApi Hook
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
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
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.
Arguments
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.
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
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:
- `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.
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:
and they are guaranteed access to the data.
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 `requestBuilder` functions
- Creating individual hooks that wrap `useApi` for 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.