Imagine that your spaceship repeatedly asks Mission Control for the same planetary data. Every time you wait for a response, even if the data hasn't changed. That's wasteful! The solution is caching -- storing data locally and refreshing it in the background.
Without cache:
With cache:
SWR is a caching strategy used by browsers and libraries. The name says it all:
1// SWR visualization:
2// Step 1: User enters the page
3// -> Cache exists? YES -> Show cached data (instantly!)
4// -> Simultaneously: send request to server
5
6// Step 2: Server responds
7// -> Data changed? YES -> Update view and cache
8// -> Same data? -> Do nothing
9
10// Step 3: User returns to the page
11// -> Repeat from Step 1 (instant display!)The simplest cache can be built with a
Map object:1// Global cache - lives outside components
2const cache = new Map();
3
4function useFetchWithCache(url, ttl = 60000) {
5 const [data, setData] = useState(() => {
6 const cached = cache.get(url);
7 if (cached && Date.now() - cached.timestamp < ttl) {
8 return cached.data; // Instant data from cache!
9 }
10 return null;
11 });
12 const [loading, setLoading] = useState(!cache.has(url));
13 const [error, setError] = useState(null);
14
15 useEffect(() => {
16 const controller = new AbortController();
17
18 async function fetchData() {
19 try {
20 // If we have cache - don't show spinner
21 if (!cache.has(url)) setLoading(true);
22 setError(null);
23
24 const response = await fetch(url, {
25 signal: controller.signal,
26 });
27 if (!response.ok) {
28 throw new Error(\`HTTP \${response.status}\`);
29 }
30 const result = await response.json();
31
32 // Save to cache with timestamp
33 cache.set(url, {
34 data: result,
35 timestamp: Date.now(),
36 });
37 setData(result);
38 } catch (err) {
39 if (err.name !== 'AbortError') {
40 setError(err.message);
41 }
42 } finally {
43 setLoading(false);
44 }
45 }
46
47 fetchData();
48 return () => controller.abort();
49 }, [url, ttl]);
50
51 return { data, loading, error };
52}TTL determines how long cached data is considered fresh:
1// TTL = 60000ms (1 minute)
2// Data older than 1 minute will be fetched again
3
4// Short TTL (5-30s) - frequently changing data
5// e.g., cryptocurrency prices, mission status
6const liveData = useFetchWithCache('/api/status', 5000);
7
8// Medium TTL (1-5 min) - moderately changing data
9// e.g., planet list, ship catalog
10const catalog = useFetchWithCache('/api/planets', 60000);
11
12// Long TTL (10-60 min) - rarely changing data
13// e.g., configuration, static lists
14const config = useFetchWithCache('/api/config', 600000);Sometimes you need to force fresh data retrieval (e.g., after adding a new item):
1// Simple invalidation - remove from cache
2function invalidateCache(url) {
3 cache.delete(url);
4}
5
6// After adding a new mission - invalidate the missions list
7async function createMission(data) {
8 await fetch('/api/missions', {
9 method: 'POST',
10 headers: { 'Content-Type': 'application/json' },
11 body: JSON.stringify(data),
12 });
13
14 // Force refresh of missions list
15 invalidateCache('/api/missions');
16}Instead of waiting for the server, we can update the UI immediately:
1function MissionList() {
2 const [missions, setMissions] = useState([]);
3
4 const deleteMission = async (id) => {
5 // 1. OPTIMISTICALLY remove from UI (instantly!)
6 const previousMissions = [...missions];
7 setMissions(prev => prev.filter(m => m.id !== id));
8
9 try {
10 // 2. Send DELETE request to server
11 await fetch(\`/api/missions/\${id}\`, {
12 method: 'DELETE',
13 });
14 // Success - UI already updated!
15 } catch (error) {
16 // 3. ERROR - restore previous state (rollback)
17 setMissions(previousMissions);
18 alert('Failed to delete mission');
19 }
20 };
21
22 return (
23 <ul>
24 {missions.map(m => (
25 <li key={m.id}>
26 {m.name}
27 <button onClick={() => deleteMission(m.id)}>
28 Delete
29 </button>
30 </li>
31 ))}
32 </ul>
33 );
34}In large projects, instead of writing your own cache, we use the TanStack Query library (formerly React Query). It's like an advanced onboard computer that automatically manages all communication:
1import {
2 useQuery,
3 useMutation,
4 useQueryClient,
5 QueryClient,
6 QueryClientProvider,
7} from '@tanstack/react-query';
8
9const queryClient = new QueryClient();
10
11function App() {
12 return (
13 <QueryClientProvider client={queryClient}>
14 <PlanetList />
15 </QueryClientProvider>
16 );
17}
18
19function PlanetList() {
20 // useQuery - automatic cache, SWR, retry, refetch
21 const { data, isLoading, error } = useQuery({
22 queryKey: ['planets'],
23 queryFn: () =>
24 fetch('https://swapi.dev/api/planets/')
25 .then(res => res.json()),
26 staleTime: 60000, // Data fresh for 1 minute
27 gcTime: 300000, // Remove from cache after 5 minutes
28 });
29
30 if (isLoading) return <p>Scanning the galaxy...</p>;
31 if (error) return <p>Error: {error.message}</p>;
32
33 return (
34 <ul>
35 {data.results.map(planet => (
36 <li key={planet.name}>{planet.name}</li>
37 ))}
38 </ul>
39 );
40}1// 1. queryKey - unique data identifier in cache
2// Changing the key = new request
3const planets = useQuery({
4 queryKey: ['planets', page],
5 queryFn: () => fetchPlanets(page),
6});
7
8// 2. staleTime - how long data stays fresh
9// staleTime: 0 - always refresh (default)
10// staleTime: 60000 - fresh for 1 minute
11// staleTime: Infinity - never refresh
12
13// 3. gcTime (garbage collection) - when to remove from cache
14// gcTime: 300000 - remove after 5 min of inactivity
15
16// 4. Automatic refetch:
17// - When the window regains focus
18// - When network reconnects after disconnection
19// - At regular intervals (refetchInterval)
20
21// 5. useMutation - write operations with invalidation
22const mutation = useMutation({
23 mutationFn: (newMission) =>
24 fetch('/api/missions', {
25 method: 'POST',
26 body: JSON.stringify(newMission),
27 }),
28 onSuccess: () => {
29 // After adding a mission - invalidate the missions list cache
30 queryClient.invalidateQueries({
31 queryKey: ['missions']
32 });
33 },
34});Caching is the superpower of every cosmic navigator -- data is available instantly, the server isn't overloaded, and the user never waits for data they've already seen!