Lesson 13 of 49 intermediate

The Frontend Tech Stack

Meet Your Toolkit

Open interactive version (quiz + challenge)

Real-world analogy

Building a frontend is like throwing a party. React is the venue, Vite is the fast setup crew, Tailwind is the decorator, React Router is the floor plan, React Query is the caterer who brings food from the kitchen (backend), and Mantine/Radix are the pre-made party supplies!

What is it?

A modern React frontend uses multiple libraries working together: Vite for building, Tailwind for styling, React Query for data fetching, React Hook Form + Zod for forms, and component libraries for pre-built UI elements.

Real-world relevance

This exact tech stack (React + Vite + Tailwind + React Query + Zod) is used by thousands of production apps. It's considered one of the best modern React setups.

Key points

Code example

// React Query — fetch data the smart way 🧠
import { useQuery } from '@tanstack/react-query';

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
    staleTime: 5 * 60 * 1000, // cache for 5 minutes!
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Oops! Something broke 💥</p>;

  return (
    <ul className="space-y-2">
      {data.map(user => (
        <li key={user.id} className="bg-white p-4 rounded-lg shadow">
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// React Hook Form + Zod — bulletproof forms 🛡️
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email!'),
  password: z.string().min(8, 'Too short!'),
});

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <input {...register('password')} type="password" />
      {errors.password && <span>{errors.password.message}</span>}
      <button type="submit">Login</button>
    </form>
  );
}

// React Router — navigate like a pro 🗺️
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

// Zustand — global state 🌍
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
}));

function Counter() {
  const { count, increment, decrement } = useStore();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

Line-by-line walkthrough

  1. 1. React Query — fetch data the smart way 🧠
  2. 2. Importing required dependencies
  3. 3.
  4. 4. Declaring a function
  5. 5. Declaring a variable
  6. 6.
  7. 7.
  8. 8.
  9. 9.
  10. 10.
  11. 11. Conditional check
  12. 12. Conditional check
  13. 13.
  14. 14. Returning a value
  15. 15.
  16. 16.
  17. 17.
  18. 18.
  19. 19.
  20. 20.
  21. 21.
  22. 22. Closing expression
  23. 23. Closing block
  24. 24.
  25. 25. React Hook Form + Zod — bulletproof forms 🛡️
  26. 26. Importing required dependencies
  27. 27. Importing required dependencies
  28. 28. Importing required dependencies
  29. 29.
  30. 30. Declaring a variable
  31. 31.
  32. 32.
  33. 33.
  34. 34.
  35. 35. Declaring a function
  36. 36. Declaring a variable
  37. 37.
  38. 38.
  39. 39.
  40. 40. Returning a value
  41. 41.
  42. 42.
  43. 43.
  44. 44.
  45. 45.
  46. 46.
  47. 47.
  48. 48. Closing expression
  49. 49. Closing block
  50. 50.
  51. 51. React Router — navigate like a pro 🗺️
  52. 52. Importing required dependencies
  53. 53.
  54. 54. Declaring a function
  55. 55. Returning a value
  56. 56.
  57. 57.
  58. 58.
  59. 59.
  60. 60.
  61. 61.
  62. 62.
  63. 63.
  64. 64.
  65. 65.
  66. 66.
  67. 67. Closing expression
  68. 68. Closing block
  69. 69.
  70. 70. Zustand — global state 🌍
  71. 71. Importing required dependencies
  72. 72.
  73. 73. Declaring a variable
  74. 74.
  75. 75.
  76. 76.
  77. 77.
  78. 78.
  79. 79. Declaring a function
  80. 80. Declaring a variable
  81. 81. Returning a value
  82. 82.
  83. 83.
  84. 84.
  85. 85.
  86. 86.
  87. 87. Closing expression
  88. 88. Closing block

Spot the bug

const schema = z.object({
  email: z.string().email(),
  age: z.string().min(18),
});
Need a hint?
Look at the type used for 'age' and what min() means for strings...
Show answer
Age should be z.number().min(18), not z.string().min(18). For strings, .min(18) checks string LENGTH (18+ characters), not the numeric value. Fix: change z.string().min(18) to z.number().min(18).

Explain like I'm 5

Building a website is like throwing a birthday party! React is the room, Vite sets up super fast, Tailwind decorates everything pretty, React Router is the map of rooms, and React Query is the person who brings snacks from the kitchen whenever you're hungry!

Fun fact

Vite (pronounced 'veet') means 'fast' in French. And it IS fast — 10-100x faster than the old create-react-app setup. The creator, Evan You, also created Vue.js! 🏎️

Hands-on challenge

Build a 'Pokemon Card Viewer' — fetch data from pokeapi.co and display a card with the Pokemon's name, image, types, and stats. Add a search bar that filters by name, a loading skeleton, and an error state that shows a funny 'Pokemon escaped!' message.

More resources

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