Advanced · Frameworks

React: Building Real Applications

Once a site grows beyond a few pages, hand-writing DOM updates becomes chaos. React is the world's most popular library for building user interfaces out of reusable pieces. This course gives you the mental model professionals actually use.

18 modules ~50 min read Animated examples DOM knowledge required
Module 1

Why React Exists

In the last course you updated the DOM by hand: querySelector, then textContent. That's perfect for a counter. But imagine Facebook — thousands of elements, all changing constantly. Doing that by hand is a nightmare of bugs.

React flips the model. Instead of writing step-by-step instructions to change the DOM ("find this, update that"), you describe what the UI should look like for a given state, and React figures out the minimal DOM changes for you. You worry about data; React worries about the DOM.

Imperative vs declarativeVanilla JS is imperative: "do this, then this." React is declarative: "here's what it should look like." The declarative style is why large apps stay maintainable.

React also introduces two ideas that changed front-end forever: breaking the UI into reusable components, and keeping a fast in-memory copy of the page (the Virtual DOM).

The Virtual DOM & Reconciliation

Updating the real DOM is historically slow and expensive. When your data changes, React creates a new Virtual DOM (a lightweight JavaScript representation of what the DOM should look like) and compares it with the previous Virtual DOM. This comparison process is called Reconciliation. React then calculates the absolute minimum number of changes needed and updates the real DOM in one swift motion (called Diffing). This is why React apps feel so fast without manual optimization.

Module 2

Components & JSX

A component is a reusable, self-contained piece of UI — written as a JavaScript function that returns markup. A navbar is a component. A post is a component. A button is a component. You compose big UIs out of small ones.

App.jsx
function App() {
  return (
    <div>
      <Navbar />
      <Feed />
    </div>
  );
}

That HTML-looking syntax inside JavaScript is JSX. It's not really HTML — it's JavaScript in disguise that React turns into real elements. A component's name is Capitalised, and you use it like a custom tag: <Navbar />.

Embedding JavaScript in JSX

JSX allows you to write dynamic UI by embedding plain JavaScript expressions inside curly braces {}. You can read variables, call functions, or do math right inside your markup.

Profile.jsx
const user = "Ali";
const avatar = "https://example.com/ali.png";

return (
  <div className="card">
    <img src={avatar} alt={user} />
    <h1>Hello, {user.toUpperCase()}</h1>
  </div>
);

The Single Parent Rule & FragmentsA component must return one single top-level element. If you need to return multiple side-by-side elements without wrapping them in an extra <div>, use a React Fragment: <> ... </>.

Animated example

Composing an app from components

<App />
<Feed />
  1. Everything lives inside one top-level component: <App />.
  2. It renders a <Navbar /> component — a self-contained piece you could reuse on any page.
  3. …and a <Feed /> component below it. Nest components inside components to build any interface.

Think in componentsLook at any website and mentally draw boxes: header, search bar, each card, each button. Every box is a component. That decomposition is React thinking.

Module 3

Props: Passing Data In

A reusable component needs to be configurable. Props (short for "properties") are the inputs you pass to a component — just like arguments to a function — so the same component can render different data.

Card.jsx
function Card({ name }) {
  return <div>Hello, {name}</div>;
}

// reuse it with different props:
<Card name="Sara" />
<Card name="Omar" />
Animated example

One component, many props

name="Sara" →
S
Hello, Sara
O
Hello, Omar
L
Hello, Lina
  1. Pass name="Sara" and the Card renders "Hello, Sara".
  2. The same component with name="Omar" renders a different card. Zero duplicate code.
  3. Feed it a list of users and you get a card for each — this is how real feeds and galleries are built.

The children Prop

Sometimes you don't know what content a component will hold beforehand (like a Modal or a Card wrapper). You can pass content between the opening and closing tags of a component, and access it using the special children prop.

Wrapper.jsx
function CardWrapper({ children }) {
  return <div className="card-shadow">{children}</div>;
}

// Usage:
<CardWrapper>
  <h1>Title inside wrapper</h1>
</CardWrapper>

Props flow one way — and are read-onlyData flows down from parent to child. A component must never modify its own props. To change something over time, you need state.

Module 4

State: A Component's Memory

Props come from outside and don't change. State is data a component owns and can change over time — and here's the magic: when state changes, React automatically re-renders the component to match. No manual DOM updates ever again.

Counter.jsx
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>
    Clicked {count} times
  </button>;
}

useState(0) is a Hook. It gives you two things: the current value (count) and a function to update it (setCount). You never assign count directly — you call setCount, and that's what tells React to re-render.

Updating Objects & Arrays (Immutability)

When your state is an object or an array, you cannot modify it directly (e.g., user.name = "Ali"). React won't know it changed. Instead, you must create a new object or array using the spread operator (...):

Immutability.jsx
// Updating an object
setUser({ ...user, name: "Ali" });

// Adding to an array
setItems([...items, newItem]);

Updating based on Previous State

When the new state depends on the old state (like incrementing a counter), it's safer to pass a function to the setter. React guarantees this function gets the absolute most recent state.

SafeUpdate.jsx
// Safer counter update
setCount(prevCount => prevCount + 1);
Animated example

setState → automatic re-render

count = 0
0
React re-renders the component
  1. Click calls setCount(1). State changes from 0 → 1.
  2. React re-renders automatically — the displayed number updates itself. You never touched the DOM.

This is the whole point of ReactIn vanilla JS you change data and manually update the DOM (two steps, easy to desync). In React you only change state — the UI is guaranteed to follow. One source of truth.

Module 5

Rendering Lists & Conditional UI

Almost every real interface shows a list of things (posts, products, messages) and changes based on conditions (logged in or not, loading or loaded). React handles both with plain JavaScript you already know.

Lists: .map() + a key

Remember .map() from JavaScript? In React you use it to turn a data array into an array of elements. Each item needs a unique key so React can track it efficiently:

Fruits.jsx
const fruits = ["🍎", "🍌", "🍇"];

return (
  <ul>
    {fruits.map((f, i) => <li key={i}>{f}</li>)}
  </ul>
);

Conditional rendering

Show different UI based on state using a normal ternary ? : or the && operator right inside your JSX:

Greeting.jsx
{isLoggedIn
  ? <h2>Welcome back!</h2>
  : <button>Log in</button>}
Animated example

A mapped list, then conditional UI

🍎
🍌
🍇
  1. map renders the first item, 🍎, as its own element.
  2. …then 🍌…
  3. …then 🍇. One line of code, a whole list on screen.
  4. Now conditional UI: when isLoggedIn is true, show a welcome message.
  5. Flip it to false and the same component shows a log-in button instead.

The danger of using index as keyReact uses key to tell items apart when the list changes. If you use the array index (0, 1, 2...) as a key, and then sort or delete an item, React gets confused because the indices shift. Always use a stable, unique ID from your database (like user.id). Use the index only if the list will never be reordered or filtered.

Module 6

Forms & Controlled Components

Every login, search box and comment field is a form. In React, forms work a little differently than plain HTML — and understanding the pattern, called a controlled component, unlocks how React thinks about the relationship between data and the UI.

State is the single source of truth

In plain HTML, the input box itself remembers what you typed. In React, we flip that: a piece of state holds the value, the input is told to display that state (value={name}), and every keystroke updates the state through onChange. The input never stores anything on its own — it just mirrors state. That's a "controlled component."

NameField.jsx
const [name, setName] = useState("");

return (
  <input
    value={name}                              // shows state
    onChange={e => setName(e.target.value)}  // writes state
  />
);
Animated example

Typing flows through state and back to the UI

Type your name…
onChange → setName()
state: name = (empty)
Hello, ! 👋
  1. The input is controlled: its value is read from state, and it has an onChange handler ready.
  2. The user types. Each keystroke fires onChange, which calls setName(e.target.value) — writing to state.
  3. State changes → React re-renders → the input and the live "Hello, Sara!" preview both update from the same value.
  4. The input can never drift out of sync with your data, because the data is the input. One source of truth.

Handling Multiple Inputs

Instead of making a separate useState for every single field in a large form, you can keep them in one object and use the input's name attribute to update the correct field dynamically:

MultiForm.jsx
const [form, setForm] = useState({ email: "", password: "" });

function handleChange(e) {
  setForm({ ...form, [e.target.name]: e.target.value });
}

Why bother?Because state holds the value, you can instantly validate as the user types, disable the submit button until the form is valid, transform input (uppercase, formatting), or pre-fill from a server — all just by reading and setting one piece of state. The form's data is always right there in your hands.

Module 7

Routing: Multiple Pages in a SPA

React builds "Single Page Applications" — the browser loads one HTML file and JavaScript swaps the content. But real apps still need multiple pages (Home, About, Profile) with real URLs you can bookmark and share. The tool for that is React Router.

The URL picks the component

The core idea is beautifully simple: you map each URL path to a component. When the URL changes, React Router swaps in the matching component — without a full page reload, so it stays instant:

App.jsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/user/:id" element={<Profile />} />
</Routes>
Animated example

The URL changes, the component swaps

myapp.com/
<Home /> — 🏠 Welcome!
  1. The URL is / → React Router shows the <Home> component.
  2. Click an "About" link → the URL becomes /about and <About> swaps in — no page reload, instant.
  3. A dynamic route /user/:id captures the id from the URL, so /user/42 renders that specific profile. Real, shareable URLs on a single-page app.

Nested Routes & Layouts

React Router allows you to nest routes to share layouts (like a persistent sidebar) across multiple pages. An <Outlet /> component is used in the parent to render the matching child route.

Router.jsx
<Route path="/dashboard" element={<DashboardLayout />}>
  <Route path="stats" element={<Stats />} />
  <Route path="settings" element={<Settings />} />
</Route>

Use <Link>, not <a>Inside a React Router app, navigate with its <Link to="/about"> component instead of a plain <a href>. A normal link triggers a full page reload (throwing away your app's state); <Link> swaps the component instantly and keeps everything alive.

Module 8

Context: Sharing State Globally

Passing props down is fine for one or two levels. But when a deeply-nested component needs data from far above — the logged-in user, the theme, the language — threading props through every layer in between becomes a nightmare called "prop drilling." React's Context solves it.

The problem: prop drilling

Imagine the current user is known at the top of your app, but a tiny avatar component ten levels deep needs it. Without Context, every component in between must accept and forward the user prop — even ones that don't use it at all. Tedious and fragile.

The fix: a Provider any component can read

Context lets you put a value in one place (a Provider) and read it directly from any descendant with useContext, skipping all the layers in between:

theme.jsx
// 1. Provide the value near the top
<UserContext.Provider value={user}>
  <App />
</UserContext.Provider>

// 2. Read it anywhere below — no props needed
const user = useContext(UserContext);
Animated example

Context skips the layers

Provider · user = "Sara"
Layout (doesn't need it)
Sidebar (doesn't need it)
Avatar → useContext() → "Sara" ✓
  1. The Provider at the top holds the value: user = "Sara".
  2. The middle layers — Layout, Sidebar — don't use user at all. Without Context, they'd still have to forward it. (Prop drilling!)
  3. With Context, the deep Avatar reads the value directly with useContext — the data beams straight past the layers that don't care. Clean and scalable.

Custom Hooks for Context

Instead of importing both useContext and UserContext in every file, it's a best practice to create a custom hook to encapsulate it. This makes reading context cleaner and allows you to throw a helpful error if the context is missing.

useUser.jsx
export function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error("useUser must be used within a UserProvider");
  }
  return context;
}

Context is for "global-ish" dataUse it for things many components need — the theme, the current user, the language. Don't reach for it for every piece of state; ordinary props are simpler and clearer for local data. For large, complex app state, teams often add a dedicated library (Redux, Zustand) — but Context covers most needs.

Module 9

Effects & Where to Go Next

Sometimes a component needs to reach outside itself — fetch data from a server, set a timer, read the URL. Those are side effects, and the useEffect Hook is where they belong.

Profile.jsx
useEffect(() => {
  // runs after the component appears
  fetch("/api/user")
    .then(res => res.json())
    .then(data => setUser(data));
}, []);  // [] = run once, on mount

The Dependency Array

The second argument to useEffect is the dependency array. It tells React exactly when to re-run the effect:

  • useEffect(fn, []) — Runs only once when the component appears (mounts). Perfect for initial data fetching.
  • useEffect(fn, [id]) — Runs on mount, and whenever id changes. Useful for fetching new data when a user selects a different profile.
  • useEffect(fn) (no array) — Runs on every single render. This is very rarely what you want and can cause infinite loops!

Cleanup Functions

If your effect creates something that runs continuously (like an interval, or an event listener), you must clean it up when the component is removed to prevent memory leaks. You do this by returning a cleanup function:

Timer.jsx
useEffect(() => {
  const timer = setInterval(() => setTick(t => t + 1), 1000);
  // React runs this cleanup function before the component unmounts
  return () => clearInterval(timer);
}, []);
Animated example

A component loads data with useEffect

  1. The component appears on screen (it "mounts").
  2. useEffect fires and starts fetching data from the server — the UI shows a loading state.
  3. The response arrives…
  4. setUser(data) updates state, React re-renders, and the real profile appears.

Advanced useEffect: Race Conditions

When fetching data based on a changing ID, old network requests might resolve after newer ones. You should use a boolean flag in your cleanup function to ignore stale responses:

SafeFetch.jsx
useEffect(() => {
  let ignore = false;
  
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) setUser(data);
    });
    
  return () => { ignore = true; };
}, [userId]);
Module 10

Custom Hooks: Reusable Logic

Components let you reuse UI. Custom Hooks let you reuse stateful logic. If you find yourself writing the same useState and useEffect combos across different components (like fetching data, listening to window size, or managing a form), you can extract them into your own Hook.

Writing a Custom Hook

A custom hook is just a JavaScript function whose name starts with "use" and that calls other Hooks.

useFetch.js
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(d => { setData(d); setLoading(false); });
  }, [url]);

  return { data, loading };
}

Now, instead of duplicating that code everywhere, any component can just call useFetch:

UsersList.jsx
function UsersList() {
  const { data, loading } = useFetch("/api/users");
  
  if (loading) return <p>Loading...</p>;
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Module 11

Performance Optimization & Next Steps

By default, when a React component's state changes, it and all its descendants re-render. Usually, React is fast enough that this doesn't matter. But for heavy computations or huge lists, you might need to optimize.

React.memo

You can wrap a component in React.memo() to skip re-rendering it if its props haven't changed. This is great for pure UI components.

ExpensiveChart.jsx
const ExpensiveChart = React.memo(function Chart({ data }) {
  // Huge D3 rendering logic...
  return <svg>...</svg>;
});

useMemo and useCallback

Sometimes you need to prevent heavy calculations on every render, or keep a function reference perfectly stable so it doesn't trigger React.memo updates in child components.

  • useMemo caches the result of a calculation until its dependencies change.
  • useCallback caches a function definition until its dependencies change.

Where to go from here

You now understand React's core: components, JSX, props, state, effects, hooks, and optimization. That's genuinely the 95% of day-to-day React. To go professional, explore next:

  • State Management Libraries — for massive applications, look into libraries like Zustand, Redux Toolkit, or Jotai.
  • Modern Meta-Frameworks — take your React skills full-stack with Next.js or Remix for server-side rendering, better SEO, and API routes.
  • React Native — use the exact same React knowledge you just learned to build real iOS and Android mobile apps.

You can build your own website nowAcross these courses you went from "what is HTML" to structuring, styling, programming and building complex interactive React apps. The only thing left is practice — pick a project you care about and build it.

Module 12

Advanced State: useReducer

As your components grow, useState can become hard to manage when you have multiple state variables that rely on each other (e.g., loading, error, and data states). useReducer offers a more structured way to manage complex state transitions.

What is a Reducer?

A reducer is a pure function that takes the current state and an action, and returns the new state. This centralizes your state update logic outside of your component.

reducer.js
function cartReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'remove':
      return state.filter(item => item.id !== action.payload.id);
    default:
      return state;
  }
}

Using useReducer in a Component

Instead of calling a state setter, you call dispatch and pass an action object describing what happened. The reducer handles the how.

ShoppingCart.jsx
function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, []);

  return (
    <button onClick={() => dispatch({ type: 'add', payload: { id: 1, name: 'Apple' } })}>
      Add Apple
    </button>
  );
}

When to use useReducer?If you find yourself writing setLoading(true), setError(null), and setData(null) all at the same time in multiple places, a single dispatch({ type: 'FETCH_START' }) is much cleaner and less prone to bugs.

Module 13

Error Boundaries: Catching UI Crashes

In the past, a JavaScript error inside a component used to corrupt React's internal state and cause it to emit cryptic errors on next renders. These errors were always caused by an earlier error in the application code, but React did not provide a way to handle them gracefully in components, and could not recover from them. Error boundaries solve this.

What is an Error Boundary?

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

Class Components RequiredCurrently, there is no Hook equivalent to getDerivedStateFromError or componentDidCatch, so error boundaries must be written as class components. Alternatively, you can use the popular react-error-boundary library which provides a ready-made component and Hook.

Module 14

Portals: Escaping the DOM Tree

Normally, when you return an element from a component's render method, it's mounted into the DOM as a child of the nearest parent node. Sometimes, you need to insert a child into a different location in the DOM — like rendering a modal or a tooltip that should visually break out of its container and avoid overflow: hidden issues.

Using createPortal

React provides Portals to achieve this. You can use createPortal(child, container) to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

Modal.jsx
import { createPortal } from 'react-dom';

function Modal({ children }) {
  // Imagine <div id="modal-root"></div> exists in index.html
  const modalRoot = document.getElementById('modal-root');
  
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    modalRoot
  );
}

Event Bubbling through PortalsEven though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Context works exactly the same, and crucially, events bubble up from the portal to its ancestors in the React tree, not the DOM tree. So a click inside a modal can be caught by its React parent!

Module 15

Refs & DOM Access (useRef)

While React's declarative state handles most UI updates, sometimes you need an "escape hatch" to directly access a DOM element (like focusing an input or measuring its size), or to store a mutable value that does not trigger a re-render when updated. Enter useRef.

Accessing the DOM

Pass a ref object to a React element's ref attribute, and React will set its .current property to the actual DOM node.

FocusInput.jsx
function FocusInput() {
  const inputRef = useRef(null);

  function focusIt() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusIt}>Focus the input</button>
    </>
  );
}

Storing Mutable Values

If you need to keep track of an interval ID, a previous state value, or a simple flag that shouldn't cause the UI to update when it changes, useRef acts like an instance variable in a class.

Module 16

Advanced Data Fetching (React Query)

Fetching data with useEffect is fine for simple apps, but it lacks caching, background updates, retry logic, and pagination. Modern React apps use dedicated server-state libraries like TanStack Query (React Query).

UserProfile.jsx
import { useQuery } from '@tanstack/react-query';

function UserProfile() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['user', 1],
    queryFn: () => fetch('/api/user/1').then(res => res.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error fetching data!</div>;
  return <div>Hello, {data.name}</div>;
}
Module 17

State Management (Zustand)

While Context API is great for small amounts of global state (like a theme), it causes all consumers to re-render when anything changes. For complex global state, tools like Zustand or Redux Toolkit are industry standards.

store.js
import { create } from 'zustand';

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

function BearCounter() {
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
}
Module 18

React Server Components & Next.js

The future of React goes beyond the browser. Meta-frameworks like Next.js allow you to render React components on the server, sending zero JavaScript to the client for parts of your app that don't need interactivity.

  • SEO & Performance: Server rendering means search engines can easily index your pages, and users see content immediately.
  • Direct Database Access: Server Components can securely talk to your database without exposing secrets to the browser.
page.jsx (Server Component)
import db from './database';

// This component runs EXCLUSIVELY on the server.
export default async function UsersPage() {
  const users = await db.query('SELECT * FROM users');

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

You've mastered the fundamentals 🎓

HTML, CSS, JavaScript, the DOM and React — you now have the full front-end skill set. There's one course left: the capstone, where you put it all together and build a complete, real website from scratch.