Data Fetcher component using Hooks and Render Props

Gasim Gasimzada
Frontend Weekly
Published in
5 min readJan 11, 2019
Photo by Cameron Kirby on Unsplash

In most cases, applications that I work with just need to request data from the API and display the result. So, using a sophisticated library like Redux, only complicates the code and increases the bundle size without bringing much of a benefit.

Here is a simple application structure that contains all the needed parts of a “feature”:

/UserProfile
/api.js
/UserProfile.js
/UserProfilePhoto.js
// ...other files

The API is typically an async function that the the fetching based on arguments provided and the “main” component calls the API and renders the data. Let’s give a small example to make this clear:

// api.jsconst fetchProfile = async id => {
const response = await fetch(`https://api.example.com/profile/${id}`);
const jsonData = response.json();
if (!response.ok) {
throw new HttpError(jsonData, response.statusCode);
// this is a custom exception class that stores JSON data
}
return jsonData;
}
// UserProfile.jsimport React, { Component } from 'react';class UserProfile extends Component {
state = { loading: false, data: null, error: null };
async componentDidMount() {
try {
this.setState({ loading: true });
const data = await api.fetchProfile(this.props.id);
this.setState({ data });
} catch (e) {
this.setState({ error: e });
} finally {
this.setState({ loading: false });
}
}
render() {
const { loading, data, error } = this.state;
if (loading) return <Spinner />;
if (error) return <Error error={error} />
// Do something with data
}
}

We have all seen a block of code like this. Show loader before data is fully fetched and show error if fetching failed. However, as your application gets larger, writing the same logical data fetching block becomes quiet a tedious task.

Introducing Fetcher

Fetcher is a React component that performs data fetching and returns a render prop to render the resulting data while performing loading and error on its own. Let’s dive into the code.

Firstly, we are going to create a hook (if you don’t know how to use Hooks, check out ReactJS docs for more info; since it is in development phase, you need to use non production version of React). This hook accepts the “action” as its argument and returns three values — data, loading, error.

// useFetcher.jsimport { useState, useEffect } from 'react';function useFetcher(action) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
async function loadData() {
try {
setLoading(true);
const actionData = await action();
setData(actionData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
useEffect(() => {
loadData();
}, [action]);
return [data, loading, error];
}

This custom hook gets the data based on asynchronous action and sets the state of data fetching (loading, error, data) during the fetching process. This hook provides a low level interface to fetching a data from an action and can be used in any component that needs to provide custom view logic based on each state the data fetching. The second argument of useEffect tests whether the provided value (i.e action) is changed. If the value is not changed, the effect function will not be called during updates. The reason for including this will be disclosed later.

However, in majority of the cases, views for loading and errors states should be the same in order to provide a unified and consistent user experience. By using our custom hook, let’s create a component renders spinners and error components for these two states while providing a render prop to render the data:

// Fetcher.jsimport React from 'react';
import Spinner from './Spinner'; // not provided in this post
import Error from './Error'; // not provided in this post
import useFetcher from './useFetcher';const Fetcher = ({ action, children }) => {
const [data, loading, error] = useFetcher(action);

if (loading) return <Spinner />;
if (error) return <Error error={error} />
if (!data) return null; return children(data);
};
export default Fetcher;

Now, let’s use our Fetcher in User Profile:

import React from 'react';
import api from './api';
import Fetcher from './Fetcher';const UserProfile = ({ id }) =>
<Fetcher action={api.fetchProfile(id)}>
{data => renderProfile(data)}
</Fetcher>
;
export default UserProfile;

As you can see, Fetcher abstracted away all the data fetching logic, making us focus on what to render. However, there is one issue with above example. In useFetcher we have defined loadData to resolve the action function by calling it without an argument:

const actionData = await action();

On the other hand, we have provided a function with an argument to the Fetcher, which does not make sense… yet! There is a library for Redux called Redux Thunk. This library allows dispatching actions asynchronously by making the initial, synchronous action return a function with Redux dispatch function as its argument (check out the examples in their github page for details). We can implement our API actions in a similar fashion — making the “synchronous” functions return an async function, which will then be called from useFetcher:

const fetchProfile = id => async () => {
// ... rest of the code
};

One of the main properties of closures in Javascript is that, “child” functions can access the scope of their ancestors. For our case, id argument in the “outer” function can be used in the “inner” async function.

This approach has a positive side effect. When outer function’s argument changes, a new inner function is created. If the id prop in UserProfile updates for any reason (e.g route changes, component updates), a new function will be passed to our useFetcher hook. We have mentioned earlier that the second argument of useEffect hook tests for changes in the provided value. In this case, the new function will differ from the old function, resulting in the loading new data for the new ID.

We could have created the Fetcher using good old class components with state. Then, why use hooks for this? Besides its coolness factor, hooks allow us to draw a thicker line between component logic (state, lifecycle) and view logic (render); so that, we can provide better isolation between logic and view by creating custom hooks for operations such as the one provided in this post.

Conclusion

Our team has been using Fetcher component for some time now for one of our projects that is in development and we are very happy with the outcome. For majority of the cases, using the Fetcher component that renders loading and error states is sufficient because our Loading and Error states are very across our application. There has been one case where, the Fetcher component was not sufficient; so, we ended up using useFetcher hook to write custom logic for data fetching states. Overall, our codebase has become cleaner due to this very small but very impactful abstraction. We have even added a timeout property to our hook and component to render the spinner if the fetching data took a longer than the specified time (similar to Suspense)

If you are not using Redux and doing a lot of requests to an external API, I recommend building a similar component to abstract away data fetching logic from other logical parts of your component.

EDIT: Fixed loading state values in the first example. Thanks Nir Oren for pointing it out.

--

--