What it is: Caching means storing previously fetched data (usually in memory, localStorage, or IndexedDB) so that you can quickly reuse it instead of making a new network request every time.
Why it’s important:
- Reduces API calls
- Improves performance & speed
- Makes the app usable offline or with slow connections
Example Approaches in React:
- Manual caching: Store API responses in
useStateoruseRefand reuse if already fetched. - Persistent caching: Store in
localStorage,sessionStorage, orIndexedDBfor use across page reloads. - Library caching:
- React Query (TanStack Query) → Automatically caches fetched data & revalidates in the background.
- SWR → Stale-while-revalidate caching strategy.
Example with React Query:
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then((res) => res.json()),
staleTime: 1000 * 60 * 5, // cache for 5 mins
});
if (isLoading) return <p>Loading...</p>;
return data.map((user) => <div key={user.id}>{user.name}</div>);
}Here, React Query automatically caches users data and won't refetch unless the cache is stale.
What it is: Breaking a large dataset into smaller “pages” of data, usually fetched one page at a time.
Why it’s important:
- Reduces initial load time
- Makes UI faster & lighter
- Improves API efficiency
Common types:
- Page number based (e.g.,
?page=2&limit=10) - Cursor based (e.g.,
?after=last_id— more scalable for large datasets)
Example (Page-based Pagination):
import { useState, useEffect } from 'react';
function PaginatedUsers() {
const [page, setPage] = useState(1);
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(`/api/users?page=${page}&limit=5`)
.then((res) => res.json())
.then((data) => setUsers(data));
}, [page]);
return (
<div>
{users.map((user) => (
<p key={user.id}>{user.name}</p>
))}
<button onClick={() => setPage((prev) => prev - 1)} disabled={page === 1}>
Prev
</button>
<button onClick={() => setPage((prev) => prev + 1)}>Next</button>
</div>
);
}What it is: A technique where new data is automatically fetched & appended to the list when the user scrolls near the bottom.
Why it’s important:
- Great for feeds (social media, news apps)
- Feels seamless for users
- Saves clicks (no "Next" button)
Challenges:
- Harder to manage memory (older data stays in DOM)
- Requires careful API & scroll handling
Example (Intersection Observer):
import { useEffect, useRef, useState } from 'react';
function InfiniteUsers() {
const [page, setPage] = useState(1);
const [users, setUsers] = useState([]);
const loaderRef = useRef(null);
useEffect(() => {
fetch(`/api/users?page=${page}&limit=5`)
.then((res) => res.json())
.then((data) => setUsers((prev) => [...prev, ...data]));
}, [page]);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setPage((prev) => prev + 1);
}
});
if (loaderRef.current) observer.observe(loaderRef.current);
return () => observer.disconnect();
}, []);
return (
<>
{users.map((user) => (
<p key={user.id}>{user.name}</p>
))}
<div ref={loaderRef} style={{ height: '50px' }} />
</>
);
}| Feature | Best For | Pros | Cons |
|---|---|---|---|
| Caching | Avoiding repeat API calls | Faster load, offline use | Data can get stale |
| Pagination | Large datasets, reports | Good performance, easy | Requires clicks for navigation |
| Infinite Scroll | Social feeds, endless content | Smooth UX, no clicks | Memory use, harder to bookmark |