Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Fetching data from an API

Most apps get data from a server through an API — an address that returns JSON. In React the request is sent in an effect and the response goes into state. While there is no response, show "Loading…"; if the request fails, show an error message. This lesson starts the course's second example — a small blog.

A new project

Create the blog project next to the to-do list:

npm create vite@latest blog -- --template react
cd blog
npm install
npm run dev

Posts come from JSONPlaceholder, a free practice API. The address https://jsonplaceholder.typicode.com/posts returns an array of 100 posts with the fields id, title and body.

A simple request

Create src/components/PostList.jsx:

// src/components/PostList.jsx
import { useEffect, useState } from 'react';
 
export default function PostList() {
  const [posts, setPosts] = useState([]);
 
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => response.json())
      .then((data) => setPosts(data));
  }, []);
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Render <PostList /> in src/App.jsx. A moment later the page shows a list of 100 titles. The empty dependency array means the request is sent once.

Loading and errors

Let us rewrite the component with async/await and three pieces of state:

// src/components/PostList.jsx
import { useEffect, useState } from 'react';
 
export default function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    async function load() {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        if (!response.ok) {
          throw new Error(`Error ${response.status}`);
        }
        setPosts(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
 
    load();
  }, []);
 
  if (loading) return <p>Loading…</p>;
  if (error) return <p>Could not load posts: {error}</p>;
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

First the page shows "Loading…", then the list. Change the address to a missing one, for example /postsx, and you see "Could not load posts: Error 404". fetch does not throw on 404 or 500, so response.ok is checked by hand.

A request that depends on a prop

If the address depends on a prop, add the prop to the dependencies and mark the old response as unwanted in the cleanup:

// src/components/PostComments.jsx
useEffect(() => {
  let ignore = false;
 
  fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
    .then((response) => response.json())
    .then((data) => {
      if (!ignore) setComments(data);
    });
 
  return () => {
    ignore = true;
  };
}, [postId]);

If postId changes before the response arrives, the old response is ignored and does not overwrite the new comments.

Common mistake

The effect function cannot be declared async: it must return a cleanup function or nothing, and an async function always returns a promise.

// Wrong:
useEffect(async () => {
  const response = await fetch(url);
}, []);

Declare an async function inside the effect and call it, as in the example above. In real projects loading is often handed to the TanStack Query library — it is covered in the last chapter.

Thanks for reading!

Donate to the site — we work hard for you.