In React applications, fetching data from an API is a common task. However, when multiple components need to fetch different data, they often duplicate the same logic, leading to code that is:
- Difficult to maintain β -- Updates need to be made in multiple places.
- Repetitive β -- Similar
useEffect
logic is used across components. - Error-prone β -- Debugging network requests becomes harder.
To fix this, we will:
- Refactor API calls into a Custom Hook (
useFetchData.js
) to centralize logic. - Use npm to manage dependencies and install third-party packages.
- Introduce Chalk for better logging -- Adding color-coded debugging messages to track API calls in the browser console.
Before starting, confirm you have Node.js and npm installed.
Run the following command to check:
node -v
npm -v
You can use this repo , create one using Vite:
# Create a new Vite project
npm create vite@latest my-app --template react
# Navigate into the project folder
cd my-app
# Install necessary dependencies
npm install
# Start the development server
npm run dev
This will:
β
Set up a React development environment using Vite.
β
Install default dependencies (React, ReactDOM).
β
Start a local server at http://localhost:5173/
.
To enhance debugging and manage package dependencies, install the following:
npm install react-router-dom chalk
- β Enhances debugging -- Color-coded logs help identify success, errors, and warnings.
- β Improves readability -- Makes console output clearer when fetching API data.
- β
react-router-dom
-- Enables page navigation.
Right now, both Posts.jsx
and Users.jsx
fetch data separately. Each uses useEffect
with duplicated API-fetching logic.
import React, { useState, useEffect } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((res) => {
if (!res.ok) {
throw new Error("Failed to fetch posts");
}
return res.json();
})
.then((data) => {
setPosts(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p className="loading">Loading posts...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="container">
<h2>Posts</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export default Posts;
Code for Users.jsx
(before refactoring)
import React, { useState, useEffect } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => {
if (!res.ok) {
throw new Error("Failed to fetch users");
}
return res.json();
})
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p className="loading">Loading users...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="container">
<h2>Users</h2>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default Users;
Create a new file inside src/hooks/
called useFetchData.js
.
π File: src/hooks/useFetchData.js
import { useState, useEffect } from "react";
import chalk from "chalk";
/**
* Custom Hook for fetching API data.
* @param {string} url - The API endpoint.
* @param {Object} options - Optional fetch settings.
* @returns {Object} { data, loading, error, refetch }
*/
function useFetchData(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch function that can be triggered manually
const fetchData = async () => {
setLoading(true);
console.log(chalk.blue(`Fetching data from: ${url}`));
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error("Failed to fetch data");
const result = await response.json();
console.log(chalk.green("Data fetched successfully!"), result);
setData(result);
} catch (err) {
console.log(chalk.red("Error fetching data:"), err.message);
setError(err.message);
} finally {
setLoading(false);
}
};
// Fetch data on component mount
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
export default useFetchData;
β Improvements:
- Encapsulates API logic -- No need to write
useEffect
in every component. - Reusability -- Works in any component needing API data.
- Refetch function -- Allows manual data fetching with a button click.
- Uses Chalk -- Adds color-coded console logs for easier debugging.
π File: src/components/Posts.jsx
import React from "react";
import useFetchData from "../hooks/useFetchData";
function Posts() {
const { data, loading, error, refetch } = useFetchData(
"https://jsonplaceholder.typicode.com/posts"
);
if (loading) return <p className="loading">Loading posts...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="container">
<h2>Posts</h2>
<button onClick={refetch}>Refresh Posts</button>
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export default Posts;
β Refactored Features:
- Uses
useFetchData
instead of duplicating logic. - Includes a "Refresh" button to manually re-fetch posts.
- Chalk logs API requests and errors.
Now, let's replace the old useEffect
logic in Users.jsx
with our Custom Hook (useFetchData
).
π File: src/components/Users.jsx
import React from "react";
import useFetchData from "../hooks/useFetchData";
function Users() {
const { data, loading, error, refetch } = useFetchData(
"https://jsonplaceholder.typicode.com/users"
);
if (loading) return <p className="loading">Loading users...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="container">
<h2>Users</h2>
<button onClick={refetch}>Refresh Users</button>
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default Users;
β
Refactored Features in Users.jsx
:
- Uses
useFetchData
to remove redundantuseEffect
logic. - Includes a "Refresh Users" button to manually re-fetch users.
- Improved readability and maintainability.
To list all installed packages:
npm list --depth=0
Updating Dependencies
To update all installed packages to the latest minor/patch versions, run:
npm update
Checking for Security Vulnerabilities
To scan the project for security vulnerabilities in dependencies, run:
npm audit
If critical vulnerabilities require major version updates, use:
npm audit fix --force
To track changes efficiently, follow this Git workflow:
- Create a new feature branch:
git checkout -b feature-custom-hook
- Stage and commit changes:
git add .
git commit -m "Added useFetchData custom hook and npm dependency management"
- Push the branch to GitHub:
git push origin feature-custom-hook
- ** Create a pull request (PR) on GitHub and merge into
main
- ** After merging, delete the feature branch locally:**
git branch -d feature-custom-hook
- Modular Code: Extracts logic into reusable functions.
- Easier Maintenance: Updates apply to all components using the hook.
- Less Repetition: No need to write
useEffect
API calls in every component.
- Dependency Tracking: Manages third-party libraries efficiently.
- Security Updates: Detects and fixes vulnerabilities.
- Version Control: Ensures compatibility across different project setups.
By completing this lesson, students have:
β
Created a Custom Hook (useFetchData
) for API fetching.
β
Used the Custom Hook in multiple components (Posts.jsx
, Users.jsx
).
β
Removed duplicate useEffect
logic, making the code more modular.
β
Installed and used Chalk to log API requests.
β
Followed npm best practices for package management.
π Next Steps: πΉ Extend useFetchData
to support POST requests.
πΉ Optimize with useCallback to prevent unnecessary re-fetching.
πΉ Introduce pagination to manage large datasets efficiently.