The previous lesson left two loose ends. The first is technical: the timer in StationAvailability returned an id that had to be stored somewhere so it could be canceled, and a plain variable won't do because it's lost on every render, while state would trigger a pointless re-render. The second goes further back, to 03-05: there it was mentioned that you can read a form field by reaching straight into the DOM node, and the explanation was postponed. useRef solves both, because they're really the same problem: it's a box that survives renders without being part of them. In this lesson you'll see its two uses — a reference to a DOM node and a mutable instance value —, when ref.current can be read and when it can't, how to pass refs to your own components in React 19 (where ref is now a normal prop), callback refs for dynamic elements, and the exact boundary between what deserves useRef and what deserves useState.

Contents

  1. The problem: remembering without re-rendering
  2. useRef, the mutable box
  3. useState, useRef, and a plain variable: comparison table
  4. Use 1: a reference to a DOM node
  5. Four real cases in CicloUrbano
  6. When ref.current is available
  7. Use 2: a mutable instance value
  8. The golden rule: never read or write current during render
  9. Closing the debt from 03-05: ref versus FormData
  10. Passing refs to your own components in React 19
  11. Callback refs for dynamic elements
  12. When NOT to use useRef

  1. The problem: remembering without re-rendering

Picture a stopwatch on CicloUrbano's operator panel: it starts when you press "Start review" and stops when you press "Finish". You need to store the id that setInterval returns so you can cancel it later. Let's look at the two options you already know, and why neither works.

// ❌ Option A: a plain variable. It's lost on every render.
function ReviewTimer() {
  const [seconds, setSeconds] = useState(0);
  let intervalId = null;                  // born and lost with every render

  function handleStart() {
    intervalId = setInterval(() => setSeconds((prev) => prev + 1), 1000);
  }

  function handleStop() {
    clearInterval(intervalId);            // already null here: this stops nothing
  }
}

The first setSeconds triggers a render, the component function runs again, and intervalId is back to null. The stopwatch can't be stopped.

// ❌ Option B: state. It works, but it re-renders needlessly.
const [intervalId, setIntervalId] = useState(null);

Now it does survive, but every setIntervalId forces a full re-render of the component to store a number that doesn't appear anywhere on screen. That's wasted work, and in large components it shows.

What we need is a third thing: something that persists like state but doesn't trigger renders like a plain variable. That's exactly useRef.

  1. useRef, the mutable box

const ref = useRef(initialValue);

useRef returns the very same object, render after render, with a single property:

{ current: initialValue }

And that's the whole trick. Nothing more to it. Three properties define its behavior:

  • The returned object is stable. React never replaces it; on render 1 and on render 500 it's the same reference. That's why it doesn't need to go in an effect's dependency array (05-02).
  • current is mutable, and you change it yourself. There's no updater function: you assign it directly, ref.current = something. React doesn't get involved.
  • Changing current triggers no re-render at all. React doesn't even know it happened.

The stopwatch, solved:

// src/components/ReviewTimer.jsx
import { useState, useRef } from 'react';

function ReviewTimer({ bike }) {
  const [seconds, setSeconds] = useState(0);
  const intervalId = useRef(null);   // ← the box that survives

  function handleStart() {
    if (intervalId.current !== null) return;   // already running
    intervalId.current = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);
  }

  function handleStop() {
    clearInterval(intervalId.current);
    intervalId.current = null;
  }

  return (
    <>
      <p>Reviewing {bike.model}: {seconds}s</p>
      <button type="button" onClick={handleStart}>Start review</button>
      <button type="button" onClick={handleStop}>Finish</button>
    </>
  );
}

export default ReviewTimer;

Notice the split: seconds is state because it's shown on screen and must trigger a re-render; intervalId is a ref because it's internal plumbing nobody looks at.

  1. useState, useRef, and a plain variable: comparison table

Plain variable (let x) useRef useState
Persists between renders? ❌ No, it resets ✅ Yes ✅ Yes
Does changing it re-render? ❌ No ❌ No ✅ Yes
How you change it x = value ref.current = value setX(value)
Can it be read during render? ✅ Yes ⚠️ You shouldn't ✅ Yes
Can it be written during render? ✅ Yes ❌ No ❌ No
Does it belong in an effect's dependencies? ✅ Yes (it's reactive) ❌ No (it's stable) ✅ Yes
What it's for Local calculations within a render Remembering without re-rendering; accessing the DOM Data shown on screen

The boundary comes down to one question: does this piece of data appear in what gets drawn? If yes, it's state. If not, it's probably a ref.

  1. Use 1: a reference to a DOM node

This is the most visible use. You declare the ref, pass it as the ref attribute of a JSX element, and React places the browser's real node there.

import { useRef } from 'react';

function FocusExample() {
  const field = useRef(null);          // 1. starts as null

  function handleFocus() {
    field.current.focus();             // 3. field.current IS the real <input>
  }

  return (
    <>
      <input ref={field} type="text" />   {/* 2. React puts the node in current */}
      <button type="button" onClick={handleFocus}>Go to field</button>
    </>
  );
}

The flow, precisely:

flowchart TD
    A["Render: useRef(null) returns { current: null }"] --> B["JSX declares ref={field}"]
    B --> C["Commit: React creates/updates the DOM node"]
    C --> D["React assigns field.current = the real node"]
    D --> E["Effects and handlers can now use field.current"]
    E --> F{"Does the element unmount?"}
    F -- "Yes" --> G["React sets field.current = null"]
    style D fill:#dcfce7
    style G fill:#fecaca

It's important to understand that React fills and clears current. You never assign it yourself in this use case: you only read it.

  1. Four real cases in CicloUrbano

Focusing the first field of the booking form

When the user presses "Book" on a card, the form appears, and it's a courtesy to have the cursor ready. It's also an accessibility requirement (03-06): someone navigating by keyboard shouldn't have to tab all the way there.

// src/components/BookingForm.jsx (excerpt)
import { useRef, useEffect } from 'react';

function BookingForm({ onCreateBooking, userId = 'usr-01' }) {
  const bikeField = useRef(null);

  useEffect(() => {
    bikeField.current?.focus();   // on mount, focus goes to the first field
  }, []);

  return (
    <form noValidate onSubmit={handleSubmit}>
      <label htmlFor="bicicletaId">Bike</label>
      <select id="bicicletaId" name="bicicletaId" ref={bikeField} …>
        …
      </select>
      …
    </form>
  );
}

The ?. isn't decorative: if the component renders with the form hidden, current will still be null, and a direct access would throw an error.

Scrolling the list to the selected bike

// src/components/BikeList.jsx (excerpt)
function BikeList({ bikes = [], stations = [], selectedId, onSelect, onBook }) {
  const selectedCard = useRef(null);

  useEffect(() => {
    selectedCard.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }, [selectedId]);

  return (
    <ul className={styles.list}>
      {bikes.map((bike) => (
        <li
          key={bike.id}
          ref={bike.id === selectedId ? selectedCard : null}
        >
          <BikeCard … />
        </li>
      ))}
    </ul>
  );
}

The trick is to pass the ref only to the element that matters and null to the rest. React only assigns current on that one, and the effect fires when selectedId changes.

Measuring an element

function FleetSummary({ fleet }) {
  const container = useRef(null);
  const [fitCount, setFitCount] = useState(0);

  useEffect(() => {
    const width = container.current.getBoundingClientRect().width;
    setFitCount(Math.floor(width / 260));   // 260px per card
  }, []);

  return <div ref={container}>…</div>;
}

Measuring is an operation that can only be done against the real DOM: React doesn't know about pixels. That's why it lives in an effect, after the commit.

Controlling a native <dialog>

Modal (04-02) used a <div> with role="dialog". The native <dialog> element offers trapped focus and closing on Escape for free, but it opens and closes with imperative methods, not attributes:

// src/components/Modal.jsx (variant using a native <dialog>)
import { useRef, useEffect } from 'react';
import styles from './Modal.module.css';

/**
 * Props:
 *  - title    (string, required)
 *  - open     (boolean, required)
 *  - children (content)
 *  - onClose  (function, required)
 */
function Modal({ title, open, children, onClose }) {
  const dialog = useRef(null);

  useEffect(() => {
    const node = dialog.current;
    if (!node) return;
    if (open && !node.open) {
      node.showModal();      // imperative method: there's no equivalent attribute
    } else if (!open && node.open) {
      node.close();
    }
  }, [open]);

  return (
    <dialog ref={dialog} className={styles.window} onClose={onClose}>
      <h2>{title}</h2>
      {children}
      <button type="button" onClick={onClose}>Close</button>
    </dialog>
  );
}

export default Modal;

This example sums up the philosophy: React declares what is on screen; when the browser only offers an imperative API, useRef is the bridge.

  1. When ref.current is available

Moment Value of ref.current
During the first render null (or whatever initial value you passed)
During any later render The node from the previous render — don't use it
In an effect (useEffect) ✅ The current node, already in the DOM
In an event handler ✅ The current node
After the element unmounts null (React clears it)

The practical consequence: you can't use the node during render. This doesn't work:

// ❌ current is null on the first render: TypeError
function BadPanel() {
  const box = useRef(null);
  const height = box.current.offsetHeight;   // 💥
  return <div ref={box}>…</div>;
}

And even if it didn't fail, it would still be wrong: render must be pure (04-03), and reading the DOM makes it depend on external state. The place for that is an effect.

  1. Use 2: a mutable instance value

Besides nodes, current can hold anything at all. Three patterns you'll see all the time.

Storing a timer's id

You've already seen this in ReviewTimer. It's the canonical case: a piece of plumbing that has to survive and that nobody looks at.

Storing the previous value of a prop or a state value

Useful for animating a change, logging a transition, or comparing.

// src/components/DockCounter.jsx (excerpt)
import { useState, useRef, useEffect } from 'react';

function DockCounter({ freeDocks, station }) {
  const previousDocks = useRef(freeDocks);
  const [trend, setTrend] = useState('steady');

  useEffect(() => {
    if (freeDocks > previousDocks.current) setTrend('rising');
    else if (freeDocks < previousDocks.current) setTrend('falling');
    else setTrend('steady');

    previousDocks.current = freeDocks;   // written INSIDE the effect
  }, [freeDocks]);

  return (
    <p>
      {station.name}: {freeDocks} docks ({trend})
    </p>
  );
}

What matters is where current gets written: inside the effect, meaning after the render. If it were written in the component body, the "previous" value would already have been overwritten before you got the chance to compare it.

Counting renders for debugging

function BikeCard({ bike, stationName, onSelect, onBook }) {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`BikeCard ${bike.id}: render #${renderCount.current}`);
  });
  …
}

With useState this counter would be an infinite loop: incrementing it triggers a render, which increments it again. With useRef nothing happens, because it triggers no render. It's a common tool for investigating performance problems before opening the Profiler (08-05).

  1. The golden rule: never read or write current during render

During render, a component must not read or write ref.current. Only in effects and event handlers.

Writing during render breaks purity: two calls to the same function with the same arguments would stop producing the same result, and React reserves the right to call components more than once (StrictMode) or to abandon a render halfway through.

// ❌ writing during render: impure, and StrictMode counts it double
function Wrong() {
  const visits = useRef(0);
  visits.current += 1;
  return <p>{visits.current}</p>;
}

Reading during render is less serious but just as treacherous: since changing current doesn't re-render, whatever you paint with that value can go stale on screen with nothing to fix it. If a piece of data has to be visible, it's state.

The one tolerated exception is lazy ref initialization, when the initial value is expensive to build:

function MapPlayer() {
  const instance = useRef(null);
  if (instance.current === null) {
    instance.current = createMap();   // runs exactly once; never reads a changing value
  }
  …
}

It's accepted because the assignment happens exactly once and the result doesn't depend on the render.

  1. Closing the debt from 03-05: ref versus FormData

03-05 left open the comparison between the two ways of reading an uncontrolled field. Here they both are, side by side, on the same incident form in CicloUrbano.

// With FormData: reads ALL fields from their name attribute
function IncidentForm({ onRegister }) {
  function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.target);
    onRegister({
      bicicletaId: formData.get('bicicletaId'),
      description: formData.get('description'),
      urgent: formData.get('urgent') === 'on'
    });
    event.target.reset();
  }
  …
}

// With useRef: one ref per field
function IncidentForm({ onRegister }) {
  const bikeField = useRef(null);
  const descriptionField = useRef(null);
  const urgentField = useRef(null);

  function handleSubmit(event) {
    event.preventDefault();
    onRegister({
      bicicletaId: bikeField.current.value,
      description: descriptionField.current.value,
      urgent: urgentField.current.checked
    });
    bikeField.current.focus();   // ← FormData simply can't do this
  }
  …
}
Criterion FormData useRef
Reading every field on submit ✅ One line, nothing to declare ❌ One ref per field
Adding a new field ✅ Just give it a name ❌ You have to declare another ref
Reading a single stray field Valid Valid
Acting on the node: focus, select(), scrollIntoView ❌ Impossible ✅ Its home turf
Integrating a library that needs the node ❌ Impossible ✅ Its home turf
File-type values Valid (formData.get('photo')) Valid (field.current.files)

Conclusion: FormData for reading, useRef for acting. And they combine perfectly well: reading with FormData on submit while keeping a single ref to the first field to give it back focus afterward is the cleanest split.

  1. Passing refs to your own components in React 19

Putting ref on a DOM element always works. Putting it on your own component is a different story, because <BikeCard /> isn't a node: it's a function.

In React 19 this is now solved on its own: ref is a normal prop on function components. All you have to do is receive it and forward it:

// src/components/TextField.jsx  (React 19)
function TextField({ label, id, ref, ...rest }) {
  return (
    <p>
      <label htmlFor={id}>{label}</label>
      <input id={id} ref={ref} {...rest} />
    </p>
  );
}

export default TextField;
// Used from BookingForm
const dateField = useRef(null);

<TextField ref={dateField} id="startDate" label="Start date" type="datetime-local" />
// dateField.current is the <input> inside

In React 18 and earlier, ref didn't arrive as a prop: React intercepted it. You had to wrap the component in forwardRef, and you'll keep running into it constantly in existing code and in libraries:

// Older form, still functional in React 19 (deprecated but supported)
import { forwardRef } from 'react';

const TextField = forwardRef(function TextField({ label, id, ...rest }, ref) {
  return (
    <p>
      <label htmlFor={id}>{label}</label>
      <input id={id} ref={ref} {...rest} />
    </p>
  );
});
React 18 React 19
Receiving ref in a function component forwardRef(fn) ref arrives as a normal prop
Old code using forwardRef Mandatory Still works, marked as deprecated

One more piece, mentioned so you'll recognize it: useImperativeHandle lets a component expose to its parent a custom object instead of the DOM node (for example { focus, clear }), limiting what the parent can touch. It's a component-library tool, rarely seen in application code.

  1. Callback refs for dynamic elements

When the number of elements isn't known in advance — a ref for each bike card — you can't declare one useRef per element: that would break hooks rule 1 (04-04), which forbids calling them inside loops.

The solution is to pass a function as the ref attribute. React calls it with the node when it mounts:

// src/components/BikeList.jsx (excerpt with per-element refs)
import { useRef } from 'react';

function BikeList({ bikes = [], selectedId, ...rest }) {
  const nodesById = useRef(new Map());   // A SINGLE ref, holding a map inside

  function scrollToItem(id) {
    nodesById.current.get(id)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }

  return (
    <ul>
      {bikes.map((bike) => (
        <li
          key={bike.id}
          ref={(node) => {
            nodesById.current.set(bike.id, node);
            // React 19: the function can return a CLEANUP
            return () => nodesById.current.delete(bike.id);
          }}
        >
          <BikeCard bike={bike} {...rest} />
        </li>
      ))}
    </ul>
  );
}

Two things change in React 19:

  • A ref function can return a cleanup function, just like an effect. React runs it when the element unmounts. Before, you had to work around it by checking whether the argument was null.
  • Precisely because of that, in React 19 a ref function must no longer return anything else. Watch out for the shorthand ref={(node) => map.set(id, node)}, which returns the map by accident: you need braces.
// ❌ returns the Map: React would interpret it as a cleanup function
ref={(node) => nodesById.current.set(bike.id, node)}

// ✅ with braces: returns nothing, or an explicit cleanup
ref={(node) => { nodesById.current.set(bike.id, node); }}

  1. When NOT to use useRef

useRef is tempting because it "works" and doesn't re-render. Here are the uses you should reject:

  • Storing data that gets shown on screen. The symptom is unmistakable: you change current, the screen doesn't notice, and you end up bolting on a fake useState (const [, forceUpdate] = useState(0)) to force a re-render. If you have to force a render, that data was state all along.
  • Replacing state "for performance". The cost of a well-designed render is negligible; the cost of an interface showing stale data isn't. Optimization has its own module (Module 8) and its own tools.
  • Manipulating DOM that React controls. Changing node.textContent, adding classes with classList, or deleting children by hand collides with reconciliation (01-05): React can overwrite your changes on the next render, or fail when it doesn't find what it expected. You can touch what React doesn't manage — focus, scroll, selection, playing a video — but not the structure or the content.
  • As a cache for computations. That's what useMemo (08-03) is for, and it also invalidates correctly when the inputs change.
// ❌ Antipattern: state disguised as a ref
const filter = useRef('todos');
function handleTypeChange(type) {
  filter.current = type;   // the list does NOT update; there's no render
}

// ✅ It's shown on screen → it's state
const [selectedType, setSelectedType] = useState('todos');

As a final, related note, React offers useId to generate unique, stable identifiers for pairing a label with a field without collisions when a form repeats on the same page. It has nothing to do with the DOM directly, but it rounds out the accessibility toolkit from 03-06:

const dateId = useId();
<label htmlFor={dateId}>Start date</label>
<input id={dateId} type="datetime-local" />

Common Mistakes and Tips

  • Accessing ref.current during render. On the first render it's null, and the access throws a TypeError. Use effects or handlers, and ?. as a safety net.
  • Forgetting that React clears current on unmount. A timer that uses the ref afterward may find null.
  • Writing current in the component body. It breaks purity, and with StrictMode counters come out doubled.
  • Using useRef for visible data. If you end up needing to force a render, that data was state.
  • Putting a ref in an effect's dependencies. The object is stable: it adds nothing and confuses whoever reads it.
  • Accidentally returning a value from a callback ref. In React 19 it's interpreted as a cleanup function. Use braces.
  • Modifying DOM that React manages. Only touch what React doesn't control: focus, scroll, selection, media.
  • Using forwardRef in new React 19 code. It's no longer necessary; save it for maintaining existing code.
  • Tip: when declaring a ref, name it after what it stores (bikeField, intervalId, nodesById). A ref called ref tells the reader nothing.
  • Tip: if you're torn between useState and useRef, ask yourself whether the screen should look different when that value changes. If yes, state; if no, ref.

Exercises

Exercise 1. This component is meant to count how many times the selected bike has changed and display it on screen, but it doesn't work. Explain why and fix it.

function SelectionCounter({ selectedBike }) {
  const changes = useRef(0);

  useEffect(() => {
    changes.current += 1;
  }, [selectedBike]);

  return <p>You've changed bikes {changes.current} times</p>;
}

Exercise 2. Write BikeSearch so that, on top of its current behavior, it offers a "Clear" button that empties the field and returns focus to the search field. Explain why the focus needs a ref and the text doesn't.

Exercise 3. BookingPanel must open inside a native <dialog> and close both via the "Cancel" button and the Escape key (which the browser handles on its own). Write the BookingDialog component that wraps BookingPanel, with props open, onClose, and children, making sure React's state and the <dialog>'s own state never fall out of sync.

Solutions

Solution 1.

The bug is one of category: changes is shown on screen, so it should be state. Writing to changes.current doesn't trigger any render, so the <p> keeps showing whatever value it was last painted with. The counter climbs internally while the screen stays at 0 (or at whatever number happened to be there when some other change caused an unrelated re-render — which is worse, because the bug becomes intermittent).

function SelectionCounter({ selectedBike }) {
  const [changes, setChanges] = useState(0);
  const isFirstRender = useRef(true);   // THIS is a legitimate ref

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;    // the initial mount doesn't count as a change
      return;
    }
    setChanges((prev) => prev + 1);
  }, [selectedBike]);

  return <p>You've changed bikes {changes} times</p>;
}

The solution teaches both sides: changes becomes useState because it's visible; isFirstRender stays as useRef because it's internal plumbing that shouldn't re-render anything. And setChanges uses the functional form (05-01) because the new value depends on the previous one.

Solution 2.

// src/components/BikeSearch.jsx
import { useState, useRef } from 'react';

/**
 * Props:
 *  - onSearch (function, required)
 */
function BikeSearch({ onSearch }) {
  const [query, setQuery] = useState('');
  const field = useRef(null);

  function handleChange(event) {
    setQuery(event.target.value);
    onSearch(event.target.value);
  }

  function handleClear() {
    setQuery('');
    onSearch('');
    field.current?.focus();   // acting on the node: only possible with the ref
  }

  return (
    <div>
      <input
        ref={field}
        type="search"
        value={query}
        onChange={handleChange}
        aria-label="Search bikes by model"
      />
      <button type="button" onClick={handleClear} disabled={query === ''}>
        Clear
      </button>
    </div>
  );
}

export default BikeSearch;

Why the text doesn't need a ref and the focus does: the text is shown in the field, so it's state; React paints it with value={query}, and setting it to '' is all it takes to clear the field. Focus, on the other hand, isn't a piece of data React draws: it's a property of the browser. There's no JSX attribute that says "this field has focus right now"; there's only the node's focus() method. That's why you have to reach the real node. It's the exact line between declarative and imperative.

Solution 3.

// src/components/BookingDialog.jsx
import { useRef, useEffect } from 'react';
import styles from './BookingDialog.module.css';

/**
 * Wraps BookingPanel in a native <dialog>.
 * Props:
 *  - open     (boolean, required)
 *  - onClose  (function, required)
 *  - children (dialog content)
 */
function BookingDialog({ open, onClose, children }) {
  const dialog = useRef(null);

  useEffect(() => {
    const node = dialog.current;
    if (!node) return;

    if (open && !node.open) {
      node.showModal();
    } else if (!open && node.open) {
      node.close();
    }
  }, [open]);

  return (
    <dialog
      ref={dialog}
      className={styles.dialog}
      onClose={onClose}          // ← also fires on Escape
      onCancel={(event) => {     // Escape fires 'cancel' before 'close'
        event.preventDefault();  // we prevent the native close directly…
        onClose();               // …and let React's own state drive it
      }}
      aria-labelledby="booking-dialog-title"
    >
      <h2 id="booking-dialog-title">Confirm booking</h2>
      {children}
    </dialog>
  );
}

export default BookingDialog;

The keys to keeping things in sync: the !node.open and node.open checks avoid calling showModal() on a dialog that's already open — which throws a browser error — and close() on one that's already closed. And onClose is essential because the browser can close the dialog on its own with Escape: without that notification, React's state would keep saying open: true while the dialog was actually closed, and the next attempt to open it would do nothing. This is the typical problem when integrating an element with its own state: its changes have to be propagated back to React, just like a controlled field propagates its own with onChange (03-04).

Conclusion

useRef is a { current } box that survives renders and that, when changed, triggers none. Its two uses follow from that: storing a mutable instance value — a timer's id, a prop's previous value, a debugging counter — and holding a reference to a DOM node to do what React doesn't express declaratively: focusing a field, measuring an element, scrolling the list to the selected bike, or opening a <dialog>. You've seen when current is available (after the commit, never during render), the rule against reading or writing it while rendering, the comparison with FormData left open in 03-05 and now closed, how ref is now a normal prop in React 19 — with forwardRef reduced to legacy code —, and callback refs with cleanup for elements that appear and disappear. And above all, you now have a clear boundary: if the data is visible, it's state; if it isn't, it might be a ref.

With useState, useEffect, and useRef you now control what a component remembers and how it connects to the outside world. But there's still a debt outstanding from 04-01: when a piece of data has to travel from App down to a component buried five levels below, today you only know how to do it by passing props through every rung, even when the components in between never use them for anything. That prop drilling clutters every signature along the way and turns any change into a manual trek through half the application. React has a direct channel between an ancestor and any of its descendants, with no stops in between. The next lesson is The useContext Hook.

React Course

Module 1: Getting Started with React

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved