Lesson 12 of 49 intermediate

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

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. 1. useState — local state
  2. 2. Importing required dependencies
  3. 3.
  4. 4. Declaring a function
  5. 5. Declaring a variable
  6. 6. Returning a value
  7. 7.
  8. 8.
  9. 9.
  10. 10. Closing expression
  11. 11. Closing block
  12. 12.
  13. 13. useEffect — fetch data on mount
  14. 14. Importing required dependencies
  15. 15.
  16. 16. Declaring a function
  17. 17. Declaring a variable
  18. 18. Declaring a variable
  19. 19.
  20. 20.
  21. 21.
  22. 22. Method chaining on the previous expression
  23. 23. Method chaining on the previous expression
  24. 24. Method chaining on the previous expression
  25. 25.
  26. 26.
  27. 27. Conditional check
  28. 28. Returning a value
  29. 29. Closing block
  30. 30.
  31. 31. React Query — the better way
  32. 32. Importing required dependencies
  33. 33.
  34. 34. Declaring a function
  35. 35. Declaring a variable
  36. 36.
  37. 37.
  38. 38.
  39. 39.
  40. 40. Conditional check
  41. 41. Conditional check
  42. 42. Returning a value
  43. 43. Closing block
  44. 44.
  45. 45. NestJS — Enable CORS (main.ts)
  46. 46. Declaring a variable
  47. 47.
  48. 48.
  49. 49.
  50. 50.
  51. 51.
  52. 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

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook