Fetching data in react- API

You might design the website beyond imagination, adding animations and awesome motion designs. Not only that, you can add logic too. However, this won't be enough for even a simple task because connecting to a database is essential. So, let's understand how we can connect to a database after writing the logic.
Using the fetch
API
The fetch
API is built into modern browsers and provides a simple way to make HTTP requests.
import { useState, useEffect } from 'react';
// Functional component to fetch and display data from an API
function FetchData() {
// State to store the fetched data
const [data, setData] = useState(null);
// useEffect runs once after the component mounts (empty dependency array)
useEffect(() => {
// Fetch data from the given API endpoint
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json()) // Convert response to JSON
.then(data => setData(data)) // Update state with fetched data
.catch(error => console.error('Error:', error)); // Handle any errors
}, []);
return (
<div>
{/* Render the data if available, otherwise show loading message */}
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'}
</div>
);
}
export default FetchData;
Using axios
axios
is a popular HTTP client with features like request/response interceptors and automatic JSON parsing.
import { useState, useEffect } from 'react';
import axios from 'axios';
// Functional component that fetches data using Axios and displays it
function AxiosData() {
// State variable to hold the fetched data
const [data, setData] = useState(null);
// useEffect hook to perform the data fetching on component mount
useEffect(() => {
// Make a GET request to the API using Axios
axios.get('https://api.example.com/data')
.then(response => {
// Set the data state with the response from the API
setData(response.data);
})
.catch(error => {
// Log any errors that occur during the request
console.error('Error:', error);
});
}, []); // Empty dependency array ensures this runs only once on mount
return (
<div>
{/* If data is available, display it formatted as JSON. Otherwise, show loading text */}
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'}
</div>
);
}
export default AxiosData;
Key Differences
Feature | fetch | axios |
JSON Parsing | Manual (response.json() ) | Automatic |
Error Handling | Manual (checks response.ok ) | Automatic (throws for non-2xx) |
Request Cancellation | Uses AbortController | Built-in (CancelToken ) |
Interceptors | No | Yes |
Handling Loading and Error States
A robust data-fetching implementation should handle:
Loading state (while waiting for the response)
Error state (if the request fails)
Success state (when data is fetched)
Example with All States
import { useState, useEffect } from 'react';
function DataFetching() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Using a Custom Hook
For reusability, abstract data fetching into a custom hook:
import { useState, useEffect } from 'react';
// Custom hook to fetch data from a given URL
function useFetch(url) {
// State to hold the fetched data
const [data, setData] = useState(null);
// State to indicate whether data is currently being loaded
const [loading, setLoading] = useState(true);
// State to store any error messages from the fetch process
const [error, setError] = useState(null);
// useEffect to trigger data fetching when the URL changes
useEffect(() => {
// Start fetching data from the provided URL
fetch(url)
.then(response => {
// If response is not OK (status not in the range 200–299), throw an error
if (!response.ok) throw new Error('Network error');
// Otherwise, parse the response as JSON
return response.json();
})
.then(data => {
// Save the fetched data to state
setData(data);
// Mark loading as complete
setLoading(false);
})
.catch(error => {
// If any error occurs during the fetch, save the error message to state
setError(error.message);
// Stop the loading indicator
setLoading(false);
});
}, [url]); // Re-run the effect only when the `url` changes
// Return the data, loading status, and error from the hook
return { data, loading, error };
}
// Main component that uses the custom useFetch hook
function App() {
// Call the custom hook and destructure the returned values
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos');
// Display loading message while data is being fetched
if (loading) return <div>Loading...</div>;
// Display error message if there was an issue during the fetch
if (error) return <div>Error: {error}</div>;
// If data is successfully fetched, display it in a formatted JSON block
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
export default App;
Displaying Fetched Data
Once data is fetched, it can be rendered in various ways:
Lists (
map
)Tables
Cards
Example: Rendering a List
import { useEffect, useState } from "react";
// Custom hook to fetch data from a given URL
function useFetch(url) {
// State to store the fetched data
const [data, setData] = useState(null);
// State to indicate loading status
const [loading, setLoading] = useState(true);
// State to capture and display any errors
const [error, setError] = useState(null);
// Fetch data whenever the URL changes
useEffect(() => {
fetch(url)
.then((response) => {
// If response is not OK (e.g. 404 or 500), throw an error
if (!response.ok) throw new Error("Network error");
return response.json(); // Parse the JSON data
})
.then((data) => {
setData(data); // Store the data in state
setLoading(false); // Stop loading
})
.catch((error) => {
setError(error.message); // Store the error message
setLoading(false); // Stop loading even on error
});
}, [url]);
// Return the data, loading state, and error for use in components
return { data, loading, error };
}
// Component: Displaying user data as styled cards
function UserCards() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/todos");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "1rem" }}>
{/* Render first 2 items from the fetched data as cards */}
{data.slice(0, 2).map((user) => (
<div
key={user.id}
style={{
border: "1px solid #ccc",
borderRadius: "8px",
padding: "16px",
width: "200px",
boxShadow: "0 2px 5px rgba(0,0,0,0.1)",
}}
>
{/* Display title as name and task */}
<h3>{user.name || `User ${user.id}`}</h3>
<p>
<strong>Task:</strong> {user.title}
</p>
</div>
))}
</div>
);
}
// Component: Displaying user data in a table
function UserTable() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/todos");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<table border="1" cellPadding="10">
<thead>
<tr>
{/* Table headers */}
<th>Task</th>
<th>Completed</th>
</tr>
</thead>
<tbody>
{/* Render first 2 items in the table */}
{data.slice(0, 2).map((user) => (
<tr key={user.id}>
<td>{user.title}</td>
<td>{user.completed ? "Yes" : "No"}</td>
</tr>
))}
</tbody>
</table>
);
}
// Component: Displaying data as a list
function UserList() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/todos");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{/* Render first 2 items as list elements */}
{data.slice(0, 2).map((user) => (
<li key={user.id}>{user.title}</li>
))}
</ul>
);
}
// Main App component to render all views
function App() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/todos");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<>
{/* Render data in different UI formats */}
<UserCards />
<UserList />
<UserTable />
</>
);
}
export default App;
Using async/await
in React
async/await
simplifies asynchronous code by making it look synchronous.
useEffect(() => {
// Define an asynchronous function within useEffect
const fetchData = async () => {
try {
// Make a GET request to the API
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
// Throw an error if response is not OK
if (!response.ok) throw new Error('Request failed');
// Parse JSON and set data
const data = await response.json();
setData(data);
} catch (error) {
// Handle fetch error
setError(error.message);
} finally {
// End loading regardless of outcome
setLoading(false);
}
};
// Immediately call the async function
fetchData();
}, []); // Runs only on component mount
// OR ---------------------------------------------------------------------------------
// Define the async function globally (or inside component but outside useEffect)
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
if (!response.ok) throw new Error('Request failed');
const data = await response.json();
setData(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
// Simply call the already defined async function
fetchData();
}, []);
Using async/await
with axios
useEffect(() => {
// Define an asynchronous function to fetch data using Axios
const fetchData = async () => {
try {
// Make a GET request to the API using axios
const response = await axios.get('https://api.example.com/data');
// On successful response, update the state with received data
setData(response.data);
} catch (error) {
// If an error occurs, store the error message in state
setError(error.message);
} finally {
// Regardless of success or failure, stop the loading indicator
setLoading(false);
}
};
// Immediately call the fetchData function when the component mounts
fetchData();
}, []); // Empty dependency array means this effect runs only once on initial render
Features for Data Fetching
Suspense for Data Fetching (Experimental)
Allows components to "wait" for data before rendering.
import { Suspense } from 'react';
// Main application component
function App() {
return (
// Wrap components that may suspend inside <Suspense>
// The fallback prop is what gets rendered while waiting (e.g., a loader)
<Suspense fallback={<div>Loading...</div>}>
{/* This component may suspend while fetching data */}
<UserProfile />
</Suspense>
);
}
// Component to display user information
function UserProfile() {
// Call a function that fetches data and suspends until it's ready
// In real use, this would likely be a resource with a .read() method
const data = fetchUserData(); // Assume this throws a promise if data is not ready
// Once data is resolved, display it
return <div>{data.name}</div>;
}
// NOTE: `fetchUserData` is assumed to be a suspending function/resource.
// For example, it could be using the "resource.read()" pattern described with Suspense for data fetching.
Tip: In real-world usage, fetchUserData()
would typically look like this:
const userResource = createResource(fetch('...').then(res => res.json()));
function fetchUserData() {
return userResource.read(); // Suspends while loading
}
Transitions (
startTransition
)
Prioritizes urgent updates (like user input) over non-urgent ones (like data fetching).
import { startTransition, useState } from 'react';
/**
* A simple search input component using React's startTransition.
* This pattern is useful when you want to distinguish between urgent updates
* (like typing in the input) and non-urgent updates (like fetching search results).
*/
function Search() {
// State to keep track of the current input value (typed by the user)
const [query, setQuery] = useState('');
// State to store the results returned from the backend/search function
const [results, setResults] = useState([]);
/**
* Event handler for input changes.
* Updates the input field immediately (urgent update),
* and triggers the data fetching inside `startTransition` (non-urgent update).
*/
const handleSearch = (e) => {
const input = e.target.value;
// This update is considered urgent: we want the UI to reflect the typed input immediately.
setQuery(input);
// Wrap non-urgent state updates (like fetching and updating search results) inside startTransition.
// This tells React: "If you're busy, deprioritize this update. Don't block the urgent stuff."
startTransition(() => {
fetchResults(input).then(setResults);
});
};
// Render an input field that shows the current query and listens for changes
return <input value={query} onChange={handleSearch} />;
}
What is startTransition
?
startTransition
is a feature from React 18+ that allows you to mark certain state updates as non-urgent.
Urgent updates: Things the user expects to see immediately (like typing or clicking).
Non-urgent updates: Things that can wait a bit (like filtering large data sets, fetching suggestions, updating a chart, etc.)
Why use startTransition
here?
When a user types quickly into the input:
setQuery
is urgent: you want the text in the input to reflect instantly.fetchResults
is non-urgent: it's okay if the results update slightly later.
By wrapping fetchResults()
inside startTransition
, you prevent potential UI jank (like laggy typing) caused by expensive or slow computations.
When to use startTransition
You are updating heavy UI after a user input.
You want to maintain responsiveness while still performing state updates.
You’re fetching or processing large data and don't want to block the input or animation.
Streaming SSR with
React.lazy
Improves initial load time by streaming components as they load.
import React, { Suspense } from 'react';
// What is React.lazy?
// React.lazy is a built-in function that allows you to dynamically import components.
// It enables "code-splitting" at the component level — which means your app only loads
// what's needed when it's needed, improving performance for large apps.
// Why use React.lazy?
// Normally, when you import a component, it's bundled and loaded with your main JavaScript file.
// If you have many components, this can slow down your initial load time.
// React.lazy allows you to split your code so that large or infrequently-used components are loaded **on demand**.
// This will load UserList only when <UserList /> is actually rendered.
const UserList = React.lazy(() => import('./UserList'));
function App() {
return (
// What is Suspense?
// Suspense is a wrapper component that lets you specify a fallback UI
// (like a loading spinner or message) while the lazy component is being fetched.
// It's **required** when using React.lazy.
// Why use Suspense?
// Because React.lazy returns a promise-based component, React needs to know what
// to display while it's being resolved. That’s where Suspense comes in.
<Suspense fallback={<div>Loading...</div>}>
{/* The fallback is shown while UserList is still loading */}
<UserList />
</Suspense>
);
}
export default App;
Feature | Purpose |
React.lazy() | Enables component-level code splitting by lazy-loading components |
Suspense | Provides a fallback UI (like "Loading...") while lazy-loaded components are being fetched |
fallback prop | A JSX element displayed until the dynamic component is ready |
When Should You Use This?
Use React.lazy
and Suspense
when:
You want to improve performance for large apps by splitting code into chunks.
You want to load a component only when it's needed (e.g., modals, settings panels, routes).
You want to make your app load faster initially and reduce bundle size.
Limitations
React.lazy only works for default exports.
Suspense for data fetching (like API calls) is a React 19+ (or experimental in 18) feature.
You can't use
React.lazy
outside of a component context — it must be called inside a file or module scope.
Optimisation in API calls :
1. Memoization with useMemo
import { useMemo } from 'react';
// An asynchronous function to fetch user data based on userId
const fetchUserData = async (userId) => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
};
function UserProfile({ userId }) {
// useMemo is used here to prevent re-invoking fetchUserData on every render
// unless `userId` changes. This ensures we only make a new request when necessary.
const userData = useMemo(() => fetchUserData(userId), [userId]);
// Note: React 19 introduces built-in caching and async handling, making useMemo for this less necessary.
return <div>{userData?.name}</div>;
}
When to Use:
Prevent repeated calculations or API calls for the same input.
Useful for expensive computations or repeated network requests.
Why It’s Becoming Less Necessary in React 19:
React 19 includes built-in data cache mechanisms (e.g., use
, suspense
, and async component support), reducing reliance on useMemo
for API calls.
2. Debouncing API Requests (e.g., Search Bars)
import { useState, useEffect } from 'react';
import { debounce } from 'lodash'; // Lodash provides a simple and reliable debounce function
function Search() {
const [query, setQuery] = useState('');
// debounce ensures this function only fires after the user stops typing for 500ms
// This avoids firing an API call on every keystroke
const fetchResults = debounce(async (query) => {
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
console.log('Fetched:', data);
}, 500); // Delay in milliseconds
useEffect(() => {
// Only call the API if the query isn't empty
if (query) {
fetchResults(query);
}
// Cleanup debounce on unmount to avoid memory leaks
return () => {
fetchResults.cancel();
};
}, [query]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
When to Use:
Search inputs
Live filtering
Scenarios where rapid input can lead to too many API requests
Why Debounce Matters:
It reduces network load, prevents server spamming, and improves UX by only sending requests when the user is done typing.
3. Client-Side Caching with useRef
import { useRef, useState, useEffect } from 'react';
function useCachedFetch(url) {
const cache = useRef({}); // Persistent object to store fetched results
const [data, setData] = useState(null);
useEffect(() => {
// If data for this URL is already cached, use it
if (cache.current[url]) {
setData(cache.current[url]);
return;
}
// If not cached, fetch from API and then store it
fetch(url)
.then(res => res.json())
.then(data => {
cache.current[url] = data; // Store the result in cache
setData(data);
});
}, [url]); // Re-run when URL changes
return data;
}
// Usage
function App() {
const data = useCachedFetch('/api/posts');
return (
<div>
{data ? data.map(item => <div key={item.id}>{item.title}</div>) : 'Loading...'}
</div>
);
}
When to Use:
When the same API endpoint is frequently accessed.
For static or rarely-changing data (e.g., user info, dropdown options).
To avoid re-fetching the same data every time the component renders.
Why useRef
?
useRef
preserves data across renders without causing re-renders.Acts as a simple in-memory cache that’s component-scoped.
Summary Table
Technique | Purpose | When to Use | React 19 Replacement? |
useMemo | Avoid recalculating / re-fetching unless deps change | Expensive functions, API calls | Yes – React 19’s built-in cache |
debounce | Delay function execution until input stabilizes | Typing/search input | Still useful (no native debounce) |
useRef Caching | Avoid refetching same API endpoint repeatedly | Static or repeat data | Partially – use() + Suspense |
Advanced Error Handling & Retries
1. Exponential Backoff Retry Mechanism
Why Use It:
Automatically retry failed requests (e.g., due to network glitches or server overload).
Useful for resilient systems that shouldn't fail on the first error.
Helps handle rate-limiting or transient errors more gracefully.
async function fetchWithRetry(url, retries = 3) {
try {
const res = await fetch(url);
// ❌ If response is not OK (status not 2xx), throw an error
if (!res.ok) throw new Error('Request failed');
// Return parsed response if successful
return res.json();
} catch (error) {
if (retries <= 0) throw error;
// Wait using exponential backoff: 1s, 2s, 3s...
await new Promise(resolve => setTimeout(resolve, 1000 * (4 - retries)));
// Retry with one less attempt
return fetchWithRetry(url, retries - 1);
}
}
// Usage
fetchWithRetry('https://api.example.com/data')
.then(data => console.log('Fetched data:', data))
.catch(error => console.error('All retries failed:', error));
When to Use:
API calls to unstable services or third-party APIs
Retryable failures like HTTP 429 (Too Many Requests), 500s, or network timeouts
2. Global Error Boundary in React
Why Use It:
Prevent your entire app from crashing due to an unhandled component error.
Display a fallback UI and optionally log errors for analytics/debugging.
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false };
// Called during render phase when an error occurs in child component
static getDerivedStateFromError(error) {
return { hasError: true };
}
// Called during commit phase, allows logging to a service
componentDidCatch(error, errorInfo) {
console.error('Logged Error:', error, errorInfo);
// Example: logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong. Please try again later.</h2>;
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<DataFetchingComponent />
</ErrorBoundary>
);
}
When to Use:
Around components that are error-prone (like data fetchers)
As a top-level wrapper to catch unexpected bugs
3. Server-Side Rendering (SSR) in Next.js
Why Use It:
Renders the page on each request (server-side), useful for dynamic content or user-specific data.
Improves SEO and initial load performance.
// pages/posts.js
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data }, // Passed to the component as props
};
}
function Page({ data }) {
return <div>{JSON.stringify(data)}</div>;
}
When to Use:
User-specific dashboards
Search results
Content that changes frequently and must be always up-to-date
4. Static Site Generation (SSG) with getStaticProps
Why Use It:
Fetches data at build time, perfect for static content like blogs or docs.
Super-fast and SEO-friendly.
// pages/blog.js
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts },
revalidate: 60, // ISR: Revalidate every 60 seconds
};
}
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
When to Use:
Blogs, documentation, landing pages
Pages that can be updated occasionally (via ISR)
Summary Table
Feature | Purpose | When to Use |
fetchWithRetry (Exponential) | Resilient API calls with auto-retry | Retry on failure due to network/rate limits |
ErrorBoundary | Catch render-time errors and show fallback UI | Wrapping data-fetchers or critical UI sections |
getServerSideProps (SSR) | Fetch fresh data for every request | Dynamic pages, dashboards, authenticated content |
getStaticProps (SSG) | Fetch data once at build-time (with optional ISR) | Blogs, landing pages, semi-static content |
React Server Components (RSC) | Fetch data on server without client JS | Secure data, optimized performance in App Router |
Real-Time Data with WebSockets
import { useState, useEffect } from 'react';
function LiveUpdates() {
// State to store incoming messages from the WebSocket server
const [messages, setMessages] = useState([]);
useEffect(() => {
// Establish a WebSocket connection to the server
const socket = new WebSocket('wss://api.example.com/ws');
// Handle incoming messages from the server
// When a new message is received, parse it and update the state
socket.onmessage = (event) => {
const newMessage = JSON.parse(event.data); // Convert JSON string to object
setMessages((prevMessages) => [...prevMessages, newMessage]); // Append new message to previous list
};
// Cleanup function: closes the WebSocket connection when the component unmounts
return () => {
socket.close(); // Prevent memory leaks and unnecessary open connections
};
}, []); // Empty dependency array means this effect runs once (on mount)
// Render the list of messages
return (
<div>
{messages.map((msg) => (
<p key={msg.id}>{msg.text}</p> // Render each message, using `id` as a unique key
))}
</div>
);
}
export default LiveUpdates;
Concept | What it Does |
WebSocket | Enables a persistent, real-time connection between client and server. |
socket.onmessage | Called whenever the server sends a new message to the client. |
setMessages | Appends each new message to the existing list of messages in state. |
JSON.parse() | Converts the message from a string format to a usable JavaScript object. |
useEffect Cleanup | Ensures the socket is closed when the component unmounts, preventing memory leaks. |
[] in useEffect | Ensures the socket connection is created once, just like componentDidMount . |
Now lets build something!!!
We have done to-do app right? so now we will extend that with real apis along with login functionality.
so the folder struncture and project setup first using
Vite
Tailwind CSS
Axios
Toastify (for notifications)
Framer Motion (for animations)
npm create vite@latest todo-app
cd todo-app
npm install
after creating these files lets discuss the flow
Writing a complete code can be a hassle, so I will provide you with component-wise code and explanations. However, the GitHub repository and deployed URL are available below, so be sure to check them out.
// Importing ToastContainer from react-toastify for displaying notifications
import { ToastContainer } from "react-toastify";
// Importing the default CSS styles for the toast notifications
import "react-toastify/dist/ReactToastify.css";
// Importing the main routing component for the application
import Routing from "./Routing";
/**
* Main App component that serves as the root component of the application.
* It renders the application routes and sets up toast notifications configuration.
* @returns {JSX.Element} The root component structure
*/
function App() {
return (
// React Fragment to group multiple elements without adding extra nodes to the DOM
<>
{/* Main application routing component that handles all route rendering */}
<Routing />
{/* Toast notification container with configuration:
- position: top-right (notification appears at top right corner)
- autoClose: 3000ms (notifications automatically close after 3 seconds)
- hideProgressBar: hides the progress bar animation on notifications
*/}
<ToastContainer position="top-right" autoClose={3000} hideProgressBar />
</>
);
}
export default App;
// Importing necessary components from react-router-dom for routing functionality
import {
BrowserRouter as Router, // Router component that uses HTML5 history API
Routes, // Container for all Route components
Route, // Component to define a single route
Navigate, // Component to programmatically navigate
} from "react-router-dom";
// Importing page components
import Home from "./pages/Home"; // Home page component
import Login from "./pages/Login"; // Login page component
import { ReactNode } from "react"; // Type definition for React children
/**
* Utility function to check if user is authenticated
* @returns {boolean} True if user is authenticated (has token), false otherwise
*/
const isAuthenticated = (): boolean => {
// Checks for authentication token in localStorage
// Note: Replace with your actual authentication logic if needed
return Boolean(localStorage.getItem("token"));
};
/**
* PrivateRoute component - Protects routes that require authentication
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to render if authenticated
* @returns {JSX.Element} Either renders children or redirects to login
*/
const PrivateRoute = ({ children }: { children: ReactNode }) => {
// If authenticated, render the child components
// If not authenticated, redirect to login page with replace to prevent back navigation
return isAuthenticated() ? <>{children}</> : <Navigate to="/login" replace />;
};
/**
* Main Routing component - Defines all application routes
* @returns {JSX.Element} The complete routing structure for the application
*/
const Routing = () => {
return (
// Router component that wraps all route definitions
<Router>
{/* Routes container for all individual route definitions */}
<Routes>
{/* Protected home route - only accessible when authenticated */}
<Route
path="/home"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
{/* Public login route - accessible to all users */}
<Route path="/login" element={<Login />} />
{/* Catch-all route - redirects based on authentication status:
- Authenticated users go to /home
- Unauthenticated users go to /login
The 'replace' prop prevents this redirect from being added to history
*/}
<Route
path="*"
element={
<Navigate to={isAuthenticated() ? "/home" : "/login"} replace />
}
/>
</Routes>
</Router>
);
};
export default Routing;
// Importing required libraries and components
import { motion } from "framer-motion"; // For animations
import { Edit, Plus, Trash2 } from "lucide-react"; // Icons for UI actions
import { useEffect, useState } from "react"; // React hooks
import AddModal from "../components/AddModal"; // Modal for adding todos
import DeleteModal from "../components/DeleteModal"; // Modal for deleting todos
import axios from "axios"; // HTTP client for API calls
import { toast } from "react-toastify"; // Toast notifications
// Define TypeScript interface for Todo items
interface Todo {
_id: string; // Unique identifier from database
task: string; // Todo task text
name: string; // User who created the todo
completed: boolean; // Completion status
createdAt: string; // Creation timestamp
updatedAt?: string; // Optional update timestamp
__v?: number; // Version key from MongoDB
}
const Home: React.FC = () => {
// State management
const [todos, setTodos] = useState<Todo[]>([]); // Array of todo items
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false); // Delete modal visibility
const [showAddModal, setShowAddModal] = useState<boolean>(false); // Add modal visibility
const [editIndex, setEditIndex] = useState<number | null>(null); // Index of todo being edited
const [deleteIndex, setDeleteIndex] = useState<number | null>(null); // Index of todo to delete
const [editText, setEditText] = useState<string>(""); // Text during editing
/**
* Handles editing a todo item
* @param index - Index of the todo in the array
* @param updatedFields - Optional partial todo object with fields to update
*/
const handleEdit = async (
index: number,
updatedFields?: Partial<Omit<Todo, "_id" | "createdAt" | "updatedAt" | "__v">>
) => {
const todoToUpdate = todos[index];
try {
// API call to update todo
const res = await axios.put(
`https://todo-one-orpin.vercel.app/updateTodo/${todoToUpdate._id}`,
{
name: localStorage.getItem("name"), // Get current user from localStorage
task: updatedFields?.task ?? todoToUpdate.task, // Use new task or existing
isCompleted: updatedFields?.completed ?? todoToUpdate.completed, // Use new status or existing
}
);
// Update local state with response data
const updated = [...todos];
updated[index] = res.data.data;
setTodos(updated);
setEditIndex(null); // Reset edit mode
setEditText(""); // Clear edit text
toast.success("Todo updated successfully!"); // Show success notification
} catch (err) {
console.error("Error updating todo:", err);
toast.error("Failed to update todo. Please try again."); // Show error notification
}
};
/**
* Fetches todos from the API for the current user
*/
const fetchTodos = async () => {
try {
const res = await axios.get(
`https://todo-one-orpin.vercel.app/getAllTodos?name=${localStorage.getItem(
"name"
)}`
);
setTodos(res.data.data); // Update state with fetched todos
} catch (err) {
console.error("Error fetching todos:", err);
toast.error("Failed to fetch todos. Please check your connection.");
}
};
// Fetch todos on component mount
useEffect(() => {
fetchTodos();
}, []);
return (
<div className="min-h-screen bg-gray-100 p-6 w-[100vw]">
{/* Main todo list container */}
<div className="max-w-md mx-auto bg-white rounded-2xl shadow-lg p-6 space-y-4">
<h1 className="text-2xl font-bold text-center mb-4">📝 Todo List</h1>
{/* Todo list items with animation */}
{todos.map((todo, index) => (
<motion.div
key={todo._id}
className="flex items-center justify-between border p-3 rounded-lg bg-gray-50 hover:bg-gray-100 transition"
initial={{ opacity: 0, y: 5 }} // Initial animation state
animate={{ opacity: 1, y: 0 }} // Animate to visible
exit={{ opacity: 0 }} // Exit animation
>
{/* Todo content and controls */}
<div className="flex items-center gap-2">
{/* Completion checkbox */}
<input
type="checkbox"
checked={todo.completed}
onChange={() =>
handleEdit(index, { completed: !todo.completed })
}
/>
{/* Editable text or display text */}
{editIndex === index ? (
<input
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={() => handleEdit(index)}
autoFocus
className="border rounded px-2 py-1 text-sm"
/>
) : (
<span className={`${todo.completed ? "line-through text-gray-400" : ""}`}>
{todo.task}
</span>
)}
</div>
{/* Action buttons */}
<div className="flex gap-2">
{/* Edit button */}
<button
onClick={() => {
setEditIndex(index);
setEditText(todo.task);
}}
>
<Edit size={18} className="text-blue-500 hover:text-blue-700" />
</button>
{/* Delete button */}
<button
onClick={() => {
setDeleteIndex(index);
setShowDeleteModal(true);
}}
>
<Trash2 size={18} className="text-red-500 hover:text-red-700" />
</button>
</div>
</motion.div>
))}
{/* Add new todo button */}
<button
className="w-full mt-4 flex items-center justify-center gap-2 text-black py-2 rounded-xl hover:bg-blue-600 transition"
onClick={() => setShowAddModal(true)}
>
<Plus size={18} /> Add Todo
</button>
</div>
{/* Add Todo Modal */}
<AddModal
showAddModal={showAddModal}
setShowAddModal={setShowAddModal}
setTodos={setTodos}
/>
{/* Delete Confirmation Modal */}
<DeleteModal
showDeleteModal={showDeleteModal}
setShowDeleteModal={setShowDeleteModal}
deleteIndex={deleteIndex}
setDeleteIndex={setDeleteIndex}
setTodos={setTodos}
todos={todos}
/>
</div>
);
};
export default Home;
// Importing required libraries and components
import axios from "axios"; // HTTP client for API calls
import { AnimatePresence, motion } from "framer-motion"; // Animation library components
import { X } from "lucide-react"; // Close icon from Lucide
import { useState } from "react"; // React state hook
import { toast } from "react-toastify"; // Toast notification library
/**
* Interface defining the structure of a Todo item
* @property {string} _id - Unique identifier from database
* @property {string} name - Name of the user who created the todo
* @property {string} task - The todo task content
* @property {boolean} completed - Completion status of the todo
* @property {string} createdAt - Timestamp of when the todo was created
*/
interface Todo {
_id: string;
name: string;
task: string;
completed: boolean;
createdAt: string;
}
/**
* Props interface for the AddModal component
* @property {boolean} showAddModal - Controls modal visibility
* @property {(show: boolean) => void} setShowAddModal - Function to update modal visibility
* @property {React.Dispatch<React.SetStateAction<Todo[]>>} setTodos - Function to update todos list
*/
interface AddModalProps {
showAddModal: boolean;
setShowAddModal: (show: boolean) => void;
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>;
}
/**
* AddModal Component - Modal dialog for adding new todos
* @param {AddModalProps} props - Component props
* @returns {JSX.Element} A modal dialog with form to add new todos
*/
const AddModal: React.FC<AddModalProps> = ({
showAddModal,
setShowAddModal,
setTodos,
}) => {
// State for the new todo input
const [newTodo, setNewTodo] = useState<string>("");
/**
* Handles adding a new todo item
* - Validates input
* - Makes API call to create todo
* - Updates local state
* - Shows success/error feedback
*/
const handleAdd = async () => {
// Skip empty inputs
if (!newTodo.trim()) return;
try {
// API call to create new todo
const res = await axios.post(
"https://todo-one-orpin.vercel.app/createTodo",
{
name: localStorage.getItem("name"), // Get current user from localStorage
task: newTodo, // Use the entered todo text
}
);
// Update local state with the new todo
setTodos((prev) => [...prev, res.data.data]);
// Reset form and close modal
setNewTodo("");
setShowAddModal(false);
// Show success notification
toast.success("Todo added successfully!");
} catch (err) {
console.error("Error adding todo:", err);
// Show error notification
toast.error("Failed to add todo. Please try again.");
}
};
return (
/**
* AnimatePresence handles exit animations for components
* Only renders when showAddModal is true
*/
<AnimatePresence>
{showAddModal && (
// Modal backdrop with fade animation
<motion.div
className="fixed inset-0 bg-black bg-opacity-40 flex justify-center items-center z-50"
initial={{ opacity: 0 }} // Start invisible
animate={{ opacity: 1 }} // Animate to visible
exit={{ opacity: 0 }} // Animate out
>
{/* Modal content with scale animation */}
<motion.div
className="bg-white p-6 rounded-xl shadow-lg space-y-4 w-80"
initial={{ scale: 0.8 }} // Start slightly scaled down
animate={{ scale: 1 }} // Animate to full size
exit={{ scale: 0.8 }} // Animate out scaled down
>
{/* Modal Header with close button */}
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">Add New Todo</h2>
<button
onClick={() => setShowAddModal(false)}
aria-label="Close modal"
>
<X size={20} />
</button>
</div>
{/* Todo input field */}
<input
type="text"
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter todo..."
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAdd()} // Submit on Enter
autoFocus // Focus input when modal opens
/>
{/* Submit button */}
<button
className="w-full bg-blue-500 text-white py-2 rounded-xl hover:bg-blue-600 transition disabled:opacity-50"
onClick={handleAdd}
disabled={!newTodo.trim()} // Disable if empty
>
Add
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export default AddModal;
// Importing required libraries and components
import axios from "axios"; // HTTP client for API requests
import { AnimatePresence, motion } from "framer-motion"; // Animation library components
import { toast } from "react-toastify"; // Toast notification library
/**
* Interface defining the structure of a Todo item
* @property {string} _id - Unique identifier from database
* @property {string} name - Name of the user who created the todo
* @property {string} task - The todo task content
* @property {boolean} completed - Completion status of the todo
* @property {string} createdAt - Timestamp of creation
*/
interface Todo {
_id: string;
name: string;
task: string;
completed: boolean;
createdAt: string;
}
/**
* Props interface for the DeleteModal component
* @property {boolean} showDeleteModal - Controls modal visibility
* @property {(show: boolean) => void} setShowDeleteModal - Function to update modal visibility
* @property {number | null} deleteIndex - Index of todo to be deleted
* @property {(index: number | null) => void} setDeleteIndex - Function to update delete index
* @property {React.Dispatch<React.SetStateAction<Todo[]>>} setTodos - Function to update todos list
* @property {Todo[]} todos - Current list of todos
*/
interface DeleteModalProps {
showDeleteModal: boolean;
setShowDeleteModal: (show: boolean) => void;
deleteIndex: number | null;
setDeleteIndex: (index: number | null) => void;
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>;
todos: Todo[];
}
/**
* DeleteModal Component - Confirmation dialog for deleting todos
* @param {DeleteModalProps} props - Component props
* @returns {JSX.Element} A confirmation modal dialog
*/
const DeleteModal: React.FC<DeleteModalProps> = ({
showDeleteModal,
setShowDeleteModal,
deleteIndex,
setDeleteIndex,
setTodos,
todos,
}) => {
/**
* Handles the deletion of a todo item
* - Validates delete index exists
* - Makes API call to delete todo
* - Updates local state
* - Shows success/error feedback
*/
const handleDelete = async () => {
// Guard clause for null index
if (deleteIndex === null) return;
try {
const todoToDelete = todos[deleteIndex];
// API call to delete todo with verification
await axios.delete(
`https://todo-one-orpin.vercel.app/deleteTodo/${todoToDelete._id}`,
{
data: {
name: localStorage.getItem("name") // Verify owner before deletion
},
}
);
// Update local state by filtering out deleted todo
setTodos(todos.filter((_, i) => i !== deleteIndex));
// Reset modal state
setShowDeleteModal(false);
setDeleteIndex(null);
// Success feedback
toast.success("Todo deleted successfully!");
} catch (err) {
console.error("Error deleting todo:", err);
// Error feedback
toast.error("Failed to delete todo. Please try again.");
}
};
return (
/**
* AnimatePresence handles exit animations
* Only renders when showDeleteModal is true
*/
<AnimatePresence>
{showDeleteModal && (
// Modal backdrop with fade animation
<motion.div
className="fixed inset-0 bg-black bg-opacity-40 flex justify-center items-center z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
role="dialog" // Accessibility attribute
aria-modal="true" // Accessibility attribute
>
{/* Modal content with scale animation */}
<motion.div
className="bg-white p-6 rounded-xl shadow-lg space-y-4 w-80 max-w-[90vw]"
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
exit={{ scale: 0.8 }}
>
{/* Modal header */}
<h2 className="text-lg font-semibold text-center">
Confirm Delete
</h2>
{/* Confirmation message */}
<p className="text-center text-sm text-gray-600">
{deleteIndex !== null && todos[deleteIndex]
? `Delete "${todos[deleteIndex].task}"?`
: "Are you sure you want to delete this todo?"}
</p>
{/* Action buttons */}
<div className="flex justify-between mt-4 gap-3">
{/* Cancel button */}
<button
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition flex-1"
onClick={() => {
setShowDeleteModal(false);
setDeleteIndex(null);
}}
aria-label="Cancel deletion"
>
Cancel
</button>
{/* Delete button */}
<button
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition flex-1"
onClick={handleDelete}
aria-label="Confirm deletion"
>
Delete
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export default DeleteModal;
Deployed application -
https://todo-reducer-with-backend-cn35.vercel.app/login
Github repo -
https://github.com/vaishdwivedi1/todo-reducer-with-backend
Conclusion
To sum it all up, we've learned how to connect to a database after setting up your website's logic and how to fetch data using both fetch
and axios
in React. Along the way, we pointed out the main differences between these libraries, managed loading and error states, and introduced handy tools like custom hooks. We also touched on advanced techniques like debounce, memoization, and caching, and explored performance optimization using React 19 features. Finally, we brought everything together by building a real-world project—a todo app powered by Vite, Tailwind CSS, Axios, Toastify, and Framer Motion, with real-time data handled through WebSockets.
Please share your views; they matter a lot!
Subscribe to my newsletter
Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by