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

Featurefetchaxios
JSON ParsingManual (response.json())Automatic
Error HandlingManual (checks response.ok)Automatic (throws for non-2xx)
Request CancellationUses AbortControllerBuilt-in (CancelToken)
InterceptorsNoYes

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

  1. 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
}
  1. 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.

  1. 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;
FeaturePurpose
React.lazy()Enables component-level code splitting by lazy-loading components
SuspenseProvides a fallback UI (like "Loading...") while lazy-loaded components are being fetched
fallback propA 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

TechniquePurposeWhen to UseReact 19 Replacement?
useMemoAvoid recalculating / re-fetching unless deps changeExpensive functions, API callsYes – React 19’s built-in cache
debounceDelay function execution until input stabilizesTyping/search inputStill useful (no native debounce)
useRef CachingAvoid refetching same API endpoint repeatedlyStatic or repeat dataPartially – 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

FeaturePurposeWhen to Use
fetchWithRetry (Exponential)Resilient API calls with auto-retryRetry on failure due to network/rate limits
ErrorBoundaryCatch render-time errors and show fallback UIWrapping data-fetchers or critical UI sections
getServerSideProps (SSR)Fetch fresh data for every requestDynamic 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 JSSecure 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;
ConceptWhat it Does
WebSocketEnables a persistent, real-time connection between client and server.
socket.onmessageCalled whenever the server sends a new message to the client.
setMessagesAppends 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 CleanupEnsures the socket is closed when the component unmounts, preventing memory leaks.
[] in useEffectEnsures 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!

0
Subscribe to my newsletter

Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaishnavi Dwivedi
Vaishnavi Dwivedi