State Management & Data Fetching
Making Your Frontend Talk to Your Backend
Open interactive version (quiz + challenge)Real-world analogy
Imagine a classroom with a whiteboard (state). When the teacher writes something new (state update), every student (component) who's watching the board sees the change instantly. useState is the whiteboard. useContext is like a school-wide PA system — everyone hears it. React Query is like a librarian who fetches books (data) for you, keeps a copy cached, and automatically checks if there's a newer edition!
What is it?
State management is how you store and update data in your React app. Local state (useState) is for single components. Context (useContext) shares state across components. React Query manages server state — data that comes from your API. Data fetching is how your frontend communicates with your backend API using HTTP requests.
Real-world relevance
Every app you use daily does this: Instagram loads your feed (data fetching), updates the like count instantly when you tap (optimistic update), and caches your feed so it loads fast when you come back (React Query caching).
Key points
- useState — Local State — const [count, setCount] = useState(0). Each component manages its own data. When you call setCount, React re-renders that component with the new value. Simple and direct.
- useEffect — Side Effects — Run code when something changes: useEffect(() => { fetchData() }, [userId]). The array at the end says 'run this when userId changes'. Empty array [] means 'run once on mount'.
- useContext — Shared State — Pass data through the component tree without prop drilling. Create a context, wrap your app in a Provider, and any child can access the value. Great for themes, auth, and language.
- fetch & axios — HTTP Requests — fetch('http://localhost:3000/api/users') calls your NestJS backend. axios is a popular library that's easier to use: axios.get('/users'). Both return promises.
- React Query (TanStack Query) — The game-changer for data fetching. useQuery(['users'], fetchUsers) handles loading, errors, caching, and refetching automatically. No more manually managing loading states!
- CORS Explained — Cross-Origin Resource Sharing. Your React app (localhost:3000) calls NestJS (localhost:4000) — browser blocks it for security! Fix: enable CORS on the backend with app.enableCors().
- Loading & Error States — Every data fetch has 3 states: loading (spinner), success (show data), error (show message). React Query gives you { isLoading, isError, data } — no manual state juggling.
- Optimistic Updates — Update the UI immediately BEFORE the server responds, then roll back if it fails. Makes the app feel instant. React Query makes this easy with onMutate/onError callbacks.
Code example
// useState — local state
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
// useEffect — fetch data on mount
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('http://localhost:3000/api/users')
.then(res => res.json())
.then(data => { setUsers(data); setLoading(false); })
.catch(err => console.error(err));
}, []); // empty array = run once
if (loading) return <p>Loading...</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// React Query — the better way
import { useQuery, useMutation } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, isError } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>Something went wrong!</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// NestJS — Enable CORS (main.ts)
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:3000', // React app URL
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
});
await app.listen(4000);Line-by-line walkthrough
- 1. useState — local state
- 2. Importing required dependencies
- 3.
- 4. Declaring a function
- 5. Declaring a variable
- 6. Returning a value
- 7.
- 8.
- 9.
- 10. Closing expression
- 11. Closing block
- 12.
- 13. useEffect — fetch data on mount
- 14. Importing required dependencies
- 15.
- 16. Declaring a function
- 17. Declaring a variable
- 18. Declaring a variable
- 19.
- 20.
- 21.
- 22. Method chaining on the previous expression
- 23. Method chaining on the previous expression
- 24. Method chaining on the previous expression
- 25.
- 26.
- 27. Conditional check
- 28. Returning a value
- 29. Closing block
- 30.
- 31. React Query — the better way
- 32. Importing required dependencies
- 33.
- 34. Declaring a function
- 35. Declaring a variable
- 36.
- 37.
- 38.
- 39.
- 40. Conditional check
- 41. Conditional check
- 42. Returning a value
- 43. Closing block
- 44.
- 45. NestJS — Enable CORS (main.ts)
- 46. Declaring a variable
- 47.
- 48.
- 49.
- 50.
- 51.
- 52. Waiting for an async operation to complete
Spot the bug
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => setUsers(data));
});
return <ul>{users.map(u => <li>{u.name}</li>)}</ul>;
}Need a hint?
Look at the useEffect dependency array (or lack thereof)...
Show answer
useEffect has no dependency array, so it runs on EVERY render causing an infinite loop. Fix: add an empty array: useEffect(() => { ... }, []);
Explain like I'm 5
Your app is like a classroom. useState is each student having their own notebook. useContext is a whiteboard everyone can read. React Query is a librarian who goes to the library, brings back books, remembers what you asked for, and checks for new editions!
Fun fact
CORS errors are the #1 most searched frontend-backend integration issue on Stack Overflow. Almost every developer hits this wall the first time they connect React to an API. The fix is usually just one line on the backend!
Hands-on challenge
Build a simple React component that fetches a list of users from a NestJS API. Show a loading spinner while fetching, an error message if it fails, and the list of users on success. Bonus: Try it with React Query!
More resources
- React - Managing State (React Official)
- TanStack Query Documentation (TanStack)
- React State Management Tutorial (Fireship)