React Data Fetching methods

August 11, 2025
Pratham Israni
19 views
React#react#data fetching#useEffect#use#useSWR#useQuery

Intro

So, I was recently exploring some new technologies and I found multiple better approaches for fetching data than just useEffect. Initially, we all learn fetching data through useEffect, because it works and helps understand hooks'behavior. But as apps grow, we start facing issues, so let’s start with the basic one.

UseEffect (async/await)

Basic one, first let the component mount properly on UI, then fetch data. If we use async/await, we can make our syntax much cleaner and readable then .then chains.

import { useState, useEffect } from "react";

export default function App() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function getData() {
      try {
        const res = await fetch("URL"); // Enter your URL Here mate
        const json = await res.json();
        setData(json);
      } catch (err) {
        console.error(err);
      }
    }
    getData();
  }, []);

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

But its bad because:

  • We have to write same logic again and again in each component.

  • No caching, no fallbacks, Duplicacy issue.

  • Also, it waste 1 render + 100s of CPU cycles

  • too many useState variables.

Custom Hooks

It helps to write your data fetching logic once and use it anywhere, it helps when you need to fetch data on multiple locations, and it also manage it states and effects. Every time you call useCustomHook("URL") , it creates new states and eventlisteners itself.

import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch(url);
      setData(await res.json());
    }
    fetchData();
  }, [url]);

  return data;
}

export default function App() {
  const data = useFetch("URL");  // Enter your URL Here mate
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

It is better because it follows: DRY(Don't Repeat Yourself), Easy Debugging and more readable.

Suspense (React built-in)

So, We generally uses Suspense in Lazy Loading but we can also use it in data fetching, to show a fallback till our component is react

Let's say, i wrap a component A in Suspense boundary and setting fallback <Loading/>. So, whenever my component A throws a promise the Suspense triggers and show fallback component, till the promise resolved. This is a modern data fetching in react.

import {Suspense} from 'react
function page() {
   return (
      <Suspense fallback={<>Loading..</>}>
          <Content/>
      <Suspense/>
   )
}

let serverData = null;
const promiseFn = function() {
   return new Promise(function(res) {
      fetch("URL")
      .then((res)=>res.json())
      .then((result)=>{
         serverData = result.products
         res(result.products)
       })
       .catch((e)=>{
         serverData = "Error Occured"
         res()
       })
   })
}

function Content(){
  if(!serverData){
     throw promiseFn()
  } else if(typeof serverData === 'string'){
     return <>{serverData}</>
  }
  return <div>{JSON.stringify(serverData, null, 2)</div>
}

we are reducing Loading state, because it will be handled by Suspense, also we are handling data and error with same variable serverData.
In React 17, this method was unstable. In 18, it become stable and in 19, we get a wrapper of it called use.

use (Unstable in React 19)

use is a React API that lets you read value of resource. If we send an async function inside use() so it automatically handle Suspense with it, we don't have to throw promise manually.

import { use } from "react";

async function fetchProducts() {
  const res = await fetch("URL");
  if (!res.ok) throw new Error("Error Occurred");
  const data = await res.json();
  return data.products;
}

export default function Page() {
  const products = use(fetchProducts());

  return (
    <div>
      {JSON.stringify(products, null, 2)}
    </div>
  );
}

It is basically an ideal fit for server components in Next.js. it fetches data at the render time so it don't show any empty state and then data like other methods.

useQuery (@tanstack/react-query)

TanStack Query is a powerful library for data fetching, caching, and background updates in React. It solves most problems we face with useEffect and custom hooks

import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";

const queryClient = new QueryClient();

async function fetchProducts() {
  const res = await fetch("URL");
  if (!res.ok) throw new Error("Error Occurred");
  const data = await res.json();
  return data.products;
}

function Products() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["products"],  // unique key for cache
    queryFn: fetchProducts,  // async function
  });

  if (isLoading) return <>Loading...</>;
  if (isError) return <>Error: {error.message}</>;

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

export default function Page() {
  return (
    <QueryClientProvider client={queryClient}>
      <Products />
    </QueryClientProvider>
  );
}

Why it is better than most:

  • State handling of API :Automatically manage loading -> success -> error

  • In memory cache based on queryKey

  • Auto retry which is configurable

  • SWR in background, shows cached data ,loads fresh data and update on UI.

useSWR (Stale-While-Revalidate)

it is a lightweight alternative of tanstack. Simpler way

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Page() {
  const { data, error, isLoading } = useSWR("URL", fetcher);

  if (isLoading) return <>Loading...</>;
  if (error) return <>Error occurred</>;

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Pratham Israni

Full-Stack Developer specializing in Interactive Web Experiences. SIH 2024 Finalist building innovative solutions with modern web technologies.

© 2025 Pratham Israni. All rights reserved.

Built with ❤️ using Next.js & TypeScript