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>;
}