Four Ways to Fetch Data in React

Fetch data from REST APIs in React

React is a focused component library. So it has no opinion on how to request remote data. If you’re requesting and sending data to web APIs via HTTP, here are four options to consider.

  1. Inline
  2. Centralized
  3. Custom Hook
  4. react-query/swr

Let’s explore each.

Side note: I’m making HTTP calls with fetch in this post, but the patterns apply to alternatives like Axios too. Also, if you’re using GraphQL, there are other good options to consider like Apollo. This post assumes you’re calling a traditional REST API.

Option 1: Inline

This is the simplest and most obvious option. Make the HTTP call in the React component and handle the response.

fetch("/users").then(response => response.json());

Looks simple enough. But this example overlooks loading state, error handling, declaring and setting related state, and more. In the real world, HTTP calls look more like this.

import React, { useState, useEffect } from "react"; export default function InlineDemo() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(`${process.env.REACT_APP_API_BASE_URL}users`) .then(response => { if (response.ok) return response.json(); throw response; }) .then(json => { setUsers(json); }) .catch(err => { console.error(err); setError(err); }) .finally(() => { setLoading(false); }); }, []); if (loading) return "Loading..."; if (error) return "Oops!"; return users[0].username;

For a simple app with a few calls, this works fine. But the state declarations and useEffect above are boilerplate. If I’m making many HTTP calls, I don’t want to duplicate and maintain around 20 lines of code for each one. Inline calls get ugly fast.

Look at all the concerns I have to be sure to cover:

  1. Declare loading state
  2. Declare error state
  3. Log the error to the console
  4. Check if the response returns a 200 via response.ok
  5. Convert the response to json if the response is ok and return the promise
  6. Throw an error if the response isn’t ok
  7. Hide the loading state in finally to assure the loader is hidden even if an error occurs
  8. Declare an empty dependency array so that the useEffect only runs once

And this is a simple example that’s ignoring many other relevant concerns – as you’ll see below.

Let’s look at some options that reduce the boilerplate.

Option 2: Centralized folder

What if we handled all HTTP calls in one folder? With this approach, we create a folder called services and place functions that make HTTP calls inside there. Services is the most popular term, but plenty of other good alternative names like “client”, or “api” are discussed here.

Point is, all HTTP calls are handled via plain ‘ol JavaScript functions, stored in one folder. Here’s a centralized getUsers function:

export function getUsers() { return fetch(`${process.env.REACT_APP_API_BASE_URL}users`).then(response => response.json() );

And here’s the call to the getUsers function.

import React, { useState, useEffect } from "react";
import { getUsers } from "./services/userService"; export default function CentralDemo() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { getUsers() .then(json => { setUsers(json); setLoading(false); }) .catch(err => { console.error(err); setError(err); }); }, []); if (loading) return "Loading..."; if (error) return "Oops!"; return users[0].username;

This doesn’t simplify the call site much. 🤷‍♂️ The primary benefit is it enforces consistently handling HTTP calls. Here’s the idea: When related functions are handled together, it’s easier to handle them consistently. If the userService folder is full of functions that make HTTP calls, it’s easy for me to assure they do so consistently. Also, if the calls are reused, they’re easy to call from this centralized location.

However, there’s still a lot of boilerplate at the call site. We can do better.

Option 3 – Custom Hook

With the magic of React Hooks, we can finally centralize repeated logic. So how about creating a custom useFetch hook to streamline our HTTP calls?

Here’s the custom hook.

import { useState, useEffect, useRef } from "react";
// This custom hook centralizes and streamlines handling of HTTP calls
export default function useFetch(url, init) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const prevInit = useRef(); const prevUrl = useRef(); useEffect(() => { // Only refetch if url or init params change. if (prevUrl.current === url && prevInit.current === init) return; prevUrl.current = url; prevInit.current = init; fetch(process.env.REACT_APP_API_BASE_URL + url, init) .then(response => { if (response.ok) return response.json(); setError(response); }) .then(data => setData(data)) .catch(err => { console.error(err); setError(err); }) .finally(() => setLoading(false)); }, [init, url]); return { data, loading, error };

Yours might look different, but I’ve found this basic recipe goes a long way. This single hook dramatically simplifies all call sites. Look how much less code it requires to fetch data with this handy custom hook:

import React from "react";
import useFetch from "./useFetch"; export default function HookDemo() { const { data, loading, error } = useFetch("users"); if (loading) return "Loading..."; if (error) return "Oops!"; return data[0].username;

For many apps, a custom hook like this is all you need. But this hook is already quite complex, and it’s omitting a variety of concerns. What about caching? What about refetching if the client’s connection is unreliable? Would you like to refetch fresh data when the user refocuses the tab? What about eliminating duplicate queries?

You could enhance this custom hook to do all that. But chances are, you should just reach for option 4…

Option 4 – react-query or swr

These libraries taught me to ask a variety of questions I wasn’t even considering.

With react-query or swr, caching, retry, refetch on focus, duplicated queries, and much more are handled for me. I don’t have to maintain a custom hook. And each HTTP call requires little code:

import React from "react";
import { getUsers } from "./services/userService";
import { useQuery } from "react-query"; export default function ReactQueryDemo() { const { data, isLoading, error } = useQuery("users", getUsers); if (isLoading) return "Loading..."; if (error) return "Oops!"; return data[0].username;

I’m sold. 😀 For most apps, this is my preferred option today. Here’s the full codesandbox so you can compare yourself.

Have another way you like handling HTTP calls? I welcome your feedback in the comments, or on Twitter.

Also, I’m publishing a new course on Pluralsight called “Managing React State” later this summer. This is an excerpt, so I’d love to hear your feedback.

Did you enjoy this post? Sign up below to get an email when I post. 📬

More on React

React: The Big Picture
Creating Reusable React Components
Building Applications with React and Redux
Securing React Apps with Auth0