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.jsthat exportedgetPosts()usingfetch. src/App.jsxrendered a list of posts and managed loading and error state withuseEffect.
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 axiosUpdate 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()usingapi.get('/posts'), returningres.data. - Adds
createPost()usingapi.post('/posts', post)for a simple POST demo. - Normalizes errors to a readable
Errormessage 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, anderrorto consumers, matching Part 1’s state names. - Provides a
reloadfunction 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 managinguseEffectand 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
createPostfunction directly from the Vite dev server. - Sends a JSON payload to
/postsand logs the mock response. - Leaves the app UI untouched, keeping the list view the same.
Error and loading handling
- The hook initializes
loadingtotrue, flips it off infinally, and clears previous errors before a new request. - Errors from Axios are normalized in
client.jsso the UI just showsError: <message>. - You can call
reload()fromusePosts(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.jsto use a configured Axios instance. - Exposed
getPosts()and addedcreatePost()for a quick POST demonstration. - Introduced
src/hooks/usePosts.jsto centralize loading, error, and data state. - Updated
src/App.jsxto 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.