React + Axios + Hooks: Part 2 — Swap fetch for Axios and add a usePosts hook

John John 6 min
React + Axios + Hooks: Part 2 — Swap fetch for Axios and add a usePosts hook

Learn how to replace fetch with Axios, build an Axios client, add createPost, and introduce a reusable usePosts hook. Keep the UI unchanged while improving structure.

React API Series: Part 2 – Axios + Custom Hooks

In Part 1, we used the Fetch API to load posts from JSONPlaceholder and render a minimal list with loading and error states. In Part 2, we’ll swap Fetch for Axios and move our fetching logic into a reusable custom hook. Your UI stays the same; only how we get data changes.

Previously…

  • We created an API boundary at src/api/client.js that exported getPosts() using fetch.
  • src/App.jsx rendered a list of posts and managed loading and error state with useEffect.

We’ll keep the same filenames and imports so consumers don’t break while we upgrade the internals.

Links for context:

  • Part 1: /react-api-part-1-fetch (includes the glossary)

Install Axios

Axios simplifies HTTP calls and gives us consistent response/data handling.

npm install axios

Update the API client to use Axios

We’ll replace the Fetch logic with a configured Axios instance and expose two functions: getPosts() and createPost().

// src/api/client.js
import axios from 'axios';

// Create a pre-configured Axios instance
const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  timeout: 10000,
});

export async function getPosts() {
  try {
    const res = await api.get('/posts');
    return res.data; // Axios puts parsed JSON on res.data
  } catch (err) {
    const message = err.response
      ? `Request failed: ${err.response.status} ${err.response.statusText}`
      : err.request
      ? 'Network error or no response'
      : err.message;
    throw new Error(message);
  }
}

export async function createPost(post) {
  try {
    const res = await api.post('/posts', post);
    return res.data;
  } catch (err) {
    const message = err.response
      ? `Request failed: ${err.response.status} ${err.response.statusText}`
      : err.request
      ? 'Network error or no response'
      : err.message;
    throw new Error(message);
  }
}

What this code does

  • Creates a reusable Axios instance with a base URL and JSON headers.
  • Implements getPosts() using api.get('/posts'), returning res.data.
  • Adds createPost() using api.post('/posts', post) for a simple POST demo.
  • Normalizes errors to a readable Error message so components can show friendly text.

Add a reusable usePosts hook

Move loading, error, and data management into a custom hook so components stay clean and the fetching logic is reusable.

// src/hooks/usePosts.js
import { useEffect, useState } from 'react';
import { getPosts } from '../api/client';

export function usePosts() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchPosts = async () => {
    setLoading(true);
    setError(null);
    try {
      const data = await getPosts();
      setPosts(data);
    } catch (err) {
      setError(err.message || 'Failed to load posts');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPosts();
    // runs once on mount
  }, []);

  return { posts, loading, error, reload: fetchPosts };
}

What this code does

  • Encapsulates the fetching lifecycle inside a hook (usePosts).
  • Exposes posts, loading, and error to consumers, matching Part 1’s state names.
  • Provides a reload function you can call to refetch posts on demand.
  • Keeps React components simple by moving async concerns out of the UI.

Update App.jsx to use the hook (UI unchanged)

Replace the manual useEffect in App.jsx with the usePosts hook. The UI output is unchanged.

// src/App.jsx
import React from 'react';
import { usePosts } from './hooks/usePosts';

function App() {
  const { posts, loading, error } = usePosts();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What this code does

  • Imports and calls usePosts() instead of managing useEffect and state locally.
  • Renders the same loading and error states as Part 1.
  • Keeps render output identical: an <h1> and a list of post titles.

Quick POST demo with createPost()

createPost() shows how to send data. JSONPlaceholder fakes writes and returns a payload with an id (usually 101). To try it without changing the UI, run this in the browser console while the dev server is running:

// In the browser DevTools console at http://localhost:5173
const { createPost } = await import('/src/api/client.js');
const newPost = await createPost({ title: 'Hello Axios', body: 'Demo body', userId: 1 });
console.log('Created:', newPost);

You should see an object with your fields plus an id. This won’t persist on the server (JSONPlaceholder is a mock API), but it’s perfect for verifying your client code.

What this code does

  • Dynamically imports your createPost function directly from the Vite dev server.
  • Sends a JSON payload to /posts and logs the mock response.
  • Leaves the app UI untouched, keeping the list view the same.

Error and loading handling

  • The hook initializes loading to true, flips it off in finally, and clears previous errors before a new request.
  • Errors from Axios are normalized in client.js so the UI just shows Error: <message>.
  • You can call reload() from usePosts (e.g., on a button click) to refetch — we’re not wiring that into the UI here to keep it identical to Part 1.

Recap

  • Installed Axios and rewired src/api/client.js to use a configured Axios instance.
  • Exposed getPosts() and added createPost() for a quick POST demonstration.
  • Introduced src/hooks/usePosts.js to centralize loading, error, and data state.
  • Updated src/App.jsx to consume the hook. The rendered UI didn’t change.

These changes improve structure and reusability without altering the user-facing output.

Part 3 (React Query)

Next, we’ll integrate React Query to handle caching, background refetching, request deduplication, and mutations with less boilerplate. We’ll keep the same file names in play and evolve the data layer while preserving the UI.

Continue to Part 3