cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

React frameworks

What Next.js, React Router, TanStack Start and Expo add on top of React: routing, data loading, rendering and bundling, and when Vite alone is enough.

By the end, you can
  • 01
    Say what a framework addsName the jobs React leaves to you, routing, data loading, rendering strategies, bundling and server code, and how a framework does them together.
  • 02
    Explain react.dev's adviceExplain why react.dev recommends a framework for a new app, using the network waterfall a hand-built app tends to create.
  • 03
    Choose for a projectCompare Next.js, React Router, TanStack Start, Expo and Vite alone, and pick one for a project from what it needs.

What React leaves to you

React turns components into a page and keeps the page matching your data. A whole app needs more than that: pages with their own addresses, data from a server, HTML that shows before the JavaScript runs, code split into pieces, and a way to change data on the server. Here is a two-page tea shop built with React and nothing else. Press Teas, then Home, then Teas again:

A two-page shop with React alonePop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; // Stands in for a request to your server: three teas, after 0.6 seconds.function fetchTeas() {  return new Promise((resolve) => setTimeout(() => resolve(["Mint", "Chai", "Sencha"]), 600));} export default function App() {  const [page, setPage] = useState("home");  const [teas, setTeas] = useState(null);   useEffect(() => {    if (page !== "teas") return;    let ignore = false;    fetchTeas().then((list) => {      if (!ignore) setTeas(list);    });    return () => {      ignore = true;    };  }, [page]);   return (    <>      <nav className="row">        <button onClick={() => setPage("home")}>Home</button>        <button onClick={() => setPage("teas")}>Teas</button>      </nav>      {page === "home" && <h1>Welcome to the tea shop</h1>}      {page === "teas" && (teas ? <ul>{teas.map((tea) => <li key={tea}>{tea}</li>)}</ul> : <p>Loading teas…</p>)}    </>  );}
ResultLoading…

Line 9 keeps the current page in state, and the buttons on lines 26 and 27 change it. Lines 12 to 21 are an Effect that asks for the teas when the Teas page shows, the way Fetching data taught, and line 30 shows Loading teas… until they arrive, 0.6 seconds later. It works. But look at what's missing:

  • Addresses. The page is state, not part of the address, so the browser's Back button doesn't go back to Home, and nobody can share a link to the Teas page. That's a router's job (Routing).
  • Data at the right time. The request starts only after the Teas page has rendered, and starts again on every visit.
  • HTML. The server sends an empty page, and everything appears once the JavaScript has run: client-side rendering (Rendering strategies).
  • Code per page. Both pages come in one file. A real shop with fifty pages sends all fifty to every visitor.
  • Server code. Adding to a cart needs an API, or Server Functions, which need a server and a build that supports them (Server Functions).

You can add each of these yourself, with libraries. A framework is React plus all of them, chosen to work together.

A large window titled framework. In the middle, a function card, React, which turns components into the page. Around it, five cards for the jobs a framework adds: routing, rendering, bundling on the left, data loading and server code on the right.a frameworkroutingrenderingbundlingReactcomponents → pagedata loadingserver code
React turns components into the page. A framework surrounds it with the other jobs a whole app needs, and makes them work together: the router knows each page's data, code, and rendering.
Real-life analogyAn engine, or a car

An engine on its own doesn't take you anywhere. A car maker fits steering, brakes and seats around it, all made to work together. You can build your own car around an engine too, and some people should, but you then decide, fit, and maintain every part.

In real life: The engine, which turns fuel into motion
In React: React, which turns components and data into a page
In real life: The steering, brakes, and seats
In React: Routing, data loading, rendering, and bundling
In real life: A car, with all of them fitted to one engine
In React: A framework like Next.js or React Router
In real life: Building your own car around an engine
In React: React with Vite, and the libraries you pick
The main idea

A React framework is React plus the other parts a whole app needs, built to work together: routing, data loading, rendering strategies, bundling, and code that runs on the server. react.dev recommends starting a new app with one: Next.js, React Router, or Expo for phone apps. Without one, you pick and connect those parts yourself.

Why react.dev recommends a framework

react.dev's Creating a React App page, checked on October 26, 2026, says: if you want to build a new app or website with React, start with a framework. Its Build a React app from Scratch page gives the reasons against going without one:

  • You end up building your own framework, one problem at a time, as the app grows.
  • Server rendering, static generation and Server Components are yours to build, and so is any future React feature that needs support from the framework.
  • Apps built by hand tend to load slowly in a specific way: one request waits for another, a waterfall.
  • It's harder to get help, because your routing and data loading are unlike anyone else's.

The waterfall is the easiest to see. Here a tea page's code loads separately, as it would in an app split into one piece per page, and so does its data; each takes 500 ms. The timeline.jsx tab is a stopwatch that lists what happened when. Press Open the tea page:

The page's code, then its data: a waterfallPop out in the code editor (opens in a new tab)JSX
import { Suspense, lazy, use, useEffect, useState } from "react";import { Timeline, clock } from "./timeline.jsx"; // Change to true: start loading the data as soon as the page is chosen.const startDataEarly = false; let teas = null;function loadTeas() {  teas ??= clock.wait(500).then(() => {    clock.mark("The data arrives");    return ["Mint", "Chai", "Sencha"];  });  return teas;} // Stands in for downloading the tea page's code, which takes 500 ms.const TeaPage = lazy(() =>  clock.wait(500).then(() => {    clock.mark("The page's code arrives");    return { default: TeaList };  }),); function TeaList() {  const list = use(loadTeas());  useEffect(() => clock.mark("The page shows"), []);  return <ul>{list.map((tea) => <li key={tea}>{tea}</li>)}</ul>;} export default function App() {  const [open, setOpen] = useState(false);   function openTeaPage() {    clock.start();    if (startDataEarly) loadTeas();    setOpen(true);  }   return (    <>      <button onClick={openTeaPage} disabled={open}>Open the tea page</button>      {open && (        <Suspense fallback={<p>Loading…</p>}>          <TeaPage />        </Suspense>      )}      <Timeline />    </>  );}
ResultLoading…

Lines 8 to 14 load the teas, once, in 500 ms. Lines 17 to 22 use lazy for the tea page's code, which also takes 500 ms. TeaList asks for the data with use on line 25, when it first renders, and line 26 marks when it shows. The timeline says: 500 ms: The page's code arrives, 1000 ms: The data arrives, 1000 ms: The page shows. The data couldn't start until the code that asks for it had arrived, so the two waits happened one after the other.

Now press Edit & run and set line 5 to true. Line 35 then starts the data the moment you choose the page, at the same time as the code. Run it again: everything arrives at 500 ms, and the page shows at 500 ms, twice as soon.

Two timelines from 0 to 1000 milliseconds. Top, data asked for in the component: the page's code loads from 0 to 500, then the data from 500 to 1000, and the page shows at 1000. Bottom, data asked for at the route: the code and the data both load from 0 to 500, and the page shows at 500.In the componentcodedatashowsAt the routecodedatashows0 ms500 ms1000 ms
Asked for in the component, the data waits for the code: the page shows at 1000 ms. Asked for when the page is chosen, both load at once: the page shows at 500 ms.

Starting the data with the navigation is exactly what a framework's router does: when you go to a page, it knows that page's code and its data, and loads both at once. Next.js does it with Server Components, which read the data on the server while they render; React Router and TanStack Start do it with each route's loader. With a router library and a data library you can do it too, by connecting them yourself; frameworks have it built in. Here is the fixed version, to compare with the original:

Starting the data when the page is chosenPop out in the code editor (opens in a new tab)JSX
import { Suspense, lazy, use, useEffect, useState } from "react";import { Timeline, clock } from "./timeline.jsx"; // Change to true: start loading the data as soon as the page is chosen.const startDataEarly = true; let teas = null;function loadTeas() {  teas ??= clock.wait(500).then(() => {    clock.mark("The data arrives");    return ["Mint", "Chai", "Sencha"];  });  return teas;} // Stands in for downloading the tea page's code, which takes 500 ms.const TeaPage = lazy(() =>  clock.wait(500).then(() => {    clock.mark("The page's code arrives");    return { default: TeaList };  }),); function TeaList() {  const list = use(loadTeas());  useEffect(() => clock.mark("The page shows"), []);  return <ul>{list.map((tea) => <li key={tea}>{tea}</li>)}</ul>;} export default function App() {  const [open, setOpen] = useState(false);   function openTeaPage() {    clock.start();    if (startDataEarly) loadTeas();    setOpen(true);  }   return (    <>      <button onClick={openTeaPage} disabled={open}>Open the tea page</button>      {open && (        <Suspense fallback={<p>Loading…</p>}>          <TeaPage />        </Suspense>      )}      <Timeline />    </>  );}
ResultLoading…
A framework doesn't mean a server

react.dev notes that all the frameworks it recommends can build a browser-only app, a single-page app, or static files for any static host, with no server. You can start that way and turn on server rendering for some routes later, without rewriting the app. You only need a server for the features that run there, like rendering per request or Server Functions.

What a framework does for you

How to read the table: each row is one job, who does it with React and Vite alone, and who does it in a framework.

Who does each job
JobWith React and ViteWith a framework
Choosing the page for an addressYou add a router library and list the routes.Files or a routes file decide it, and the build knows every page.
Loading a page's dataYou fetch in components, or set up loaders yourself.Each route has a loader, or is a Server Component, and loading starts with the navigation.
The first HTMLAn empty page until the JavaScript runs.Client, server or build time, chosen per route.
Splitting the codeOne bundle, unless you split it by hand with lazy.One piece per page, loaded with that page's data.
Changing data on the serverYou write an API and call it with fetch.Server Functions or route actions, with fresh data after.

Routes from files

Most frameworks find your pages from your files, so adding a page is adding a file. This example is a small model of Next.js's rules, written for the lesson in the routes.js tab. Type other addresses into the box, like /cart or /blog:

Files become addresses (a model of Next.js's rules)Pop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { matchUrl, toRoute } from "./routes.js"; // The files in a Next.js app's app folder. Add one, and its page exists.const files = [  "app/page.jsx",  "app/teas/page.jsx",  "app/teas/[id]/page.jsx",  "app/cart/page.jsx",]; export default function App() {  const [url, setUrl] = useState("/teas/mint");  const match = matchUrl(files, url);   return (    <>      <table>        <thead>          <tr>            <th>File</th>            <th>Address</th>          </tr>        </thead>        <tbody>          {files.map((file) => (            <tr key={file}>              <td>{file}</td>              <td>{toRoute(file) ?? "not a page"}</td>            </tr>          ))}        </tbody>      </table>      <label className="row">        Visit        <input value={url} onChange={(event) => setUrl(event.target.value)} />      </label>      <p>{match ? `${match.file} shows it, with params ${JSON.stringify(match.params)}` : "No page here: Next.js shows its 404 page."}</p>    </>  );}
ResultLoading…

Lines 5 to 10 list the files. In routes.js, line 4 says that only page.jsx files are pages, and line 6 turns the folders into an address, with a folder in brackets, [id], becoming a part you can read, :id. The table shows /, /teas, /teas/:id and /cart. For /teas/mint, line 38 of App.jsx says app/teas/[id]/page.jsx shows it, with params {"id":"mint"}. For /blog, there's no page.

Left, an editor window listing three files: app/page.jsx, app/teas/page.jsx, and app/teas/[id]/page.jsx. Arrows lead to three addresses on the right: /, /teas, and /teas/mint.filesapp/page.jsxapp/teas/page.jsxapp/teas/[id]/page.jsx//teas/teas/mint
Each page.jsx is a page, at the address its folders spell. A folder in brackets, [id], matches any value, so one file serves /teas/mint, /teas/chai, and every other tea.

Because the framework knows every page at build time, it can do the other jobs per page: build each one as its own piece of code, start loading a page's code and data before you even click a link to it, and choose how each one renders. Not every framework uses folders: React Router lists its routes in a file, and TanStack Router names the files with dots (teas.$id.jsx).

Next.js and its App Router

Next.js is maintained by Vercel, and react.dev lists it first. Its App Router (the app folder) is built on Server Components: every component is one unless its file says "use client". This is the tea page:

A Next.js page: a Server Component that reads the databaseJSX
// app/teas/[id]/page.jsx
import { db } from "@/db";
import AddToCart from "./AddToCart";

export default async function TeaPage({ params }) {
  const { id } = await params;
  const tea = await db.teas.find(id);
  return (
    <main>
      <h1>{tea.name}</h1>
      <AddToCart teaId={tea.id} />
    </main>
  );
}

Line 1 is the file's place, which makes it the page for /teas/ and any id. Line 5 is an async Server Component that gets the address's parts as params; line 6 waits for them, because in Next.js 15 and later params is a promise. Line 7 reads the tea straight from the database, on the server, with no API and no Effect. Line 11 renders AddToCart, a Client Component that can call a Server Function. A loading.jsx next to the page becomes its Suspense fallback:

loading.jsx: the fallback while the page loadsJSX
// app/teas/[id]/loading.jsx
export default function Loading() {
  return <p>Loading the tea…</p>;
}

What Next.js gives you, as of version 16.4.3 (checked October 26, 2026):

  • Server Components, Server Functions and streaming, in what react.dev calls the most complete implementation of the Server Components specification so far.
  • Static, server or client rendering chosen per route, with revalidation (Rendering strategies used its revalidate setting).
  • Its own bundler, Turbopack, the default for new projects since Next.js 16.
  • Deployment to any Node.js server, a Docker container, or a static export, which drops what needs a server, like Server Functions and cookies.

One thing to know: the App Router runs a React canary build that ships inside Next.js, with stable React 19 plus features still being tried in frameworks. Next.js's docs still ask you to list react and react-dom in package.json, for your tools, but the pages use Next.js's own copy.

React Router as a framework

React Router, maintained by Shopify, is the routing library from the Routing lesson. Since version 7 it also has a framework mode: a Vite plugin that adds server rendering, code split per route, and type-checked route modules. Its routes live in one file:

app/routes.js: the list of routesJavaScript
// app/routes.js
import { index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.jsx"),
  route("teas/:id", "routes/tea.jsx"),
];

Line 5 makes routes/home.jsx the page for /, and line 6 maps teas/:id to routes/tea.jsx. Each of those files is a route module:

routes/tea.jsx: a route module with a loaderJSX
// app/routes/tea.jsx
import { db } from "../db.js";

export async function loader({ params }) {
  return { tea: await db.teas.find(params.id) };
}

export default function Tea({ loaderData }) {
  return <h1>{loaderData.tea.name}</h1>;
}

Lines 4 to 6 are the route's loader, which runs before the page renders. With server rendering on, it runs on the server: while the first page renders, and through a request from the browser on later navigations. Line 9 reads what it returned as loaderData. A route module can also export an action for forms, as in Server Functions. How the app renders is one setting:

react-router.config.js: no server, three prerendered pagesJavaScript
// react-router.config.js
export default {
  ssr: false,
  prerender: ["/", "/teas/mint", "/teas/chai"],
};

Line 3, ssr: false, makes a browser-only app (SPA mode). Line 4 still renders three addresses to HTML at build time. Remove line 3, and the other pages render on the server for each request. Version 8 came out in June 2026: it needs Node.js 22.22 or later, React 19.2.7 or later and Vite 7 or later, and it drops the old react-router-dom package. The latest is 8.5.0. Its Server Components mode was still called experimental in its docs on October 26, 2026.

TanStack Start, Expo, and others

TanStack Start

TanStack Start adds server rendering, streaming and server functions to TanStack Router, a router known for type-checking every route, parameter and search parameter. Its server functions are its own API, not React's "use server":

A TanStack Start route with a server functionJSX
// src/routes/teas.$id.jsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { db } from "../db.js";

const getTea = createServerFn({ method: "GET" })
  .validator((id) => String(id))
  .handler(async ({ data }) => db.teas.find(data));

export const Route = createFileRoute("/teas/$id")({
  loader: ({ params }) => getTea({ data: params.id }),
  component: TeaPage,
});

function TeaPage() {
  const tea = Route.useLoaderData();
  return <h1>{tea.name}</h1>;
}

Lines 6 to 8 make getTea with createServerFn: line 7 checks the input, and line 8 runs on the server. Line 10 makes the route for /teas/$id, and line 11's loader calls the server function. Line 16 reads the loader's data. TanStack announced Start 1.0 as stable on October 13, 2026 (version 1.169.4 on October 26, 2026); react.dev's list, checked the same day, still calls it Beta, among its "up-and-coming" frameworks.

Expo, for phone apps

Expo is react.dev's recommendation for native apps: real iOS and Android apps, made with React Native, and the web from the same code. Its router uses files too:

An Expo Router screenJSX
// app/teas/[id].jsx, in an Expo app
import { Text, View } from "react-native";
import { useLocalSearchParams } from "expo-router";

export default function TeaScreen() {
  const { id } = useLocalSearchParams();
  return (
    <View>
      <Text>Tea: {id}</Text>
    </View>
  );
}

Line 1 is the file: the screen for /teas/ and an id. Line 2 imports Text and View from react-native; there are no <h1> or <div> in a phone app. Line 6 reads the id, and line 9 shows it. The components, Hooks and state are the React you know. Expo is free; its build and update services are optional and paid. The latest SDK on October 26, 2026 was 58.

Starting any of them

Each one makes a new project with one command:

Starting a project with each toolShell
npx create-next-app@latest
npx create-react-router@latest
npx @tanstack/cli@latest create
npx create-expo-app@latest
npm create vite@latest my-app -- --template react

Lines 1 to 4 start Next.js, React Router, TanStack Start and Expo; line 5 starts a Vite app, as in Setting up a React project. react.dev also lists RedwoodSDK as up-and-coming. How to read the table: each row is one tool, what you get, and what's good to know, checked on October 26, 2026.

Frameworks side by side
ToolWhat you getGood to know
Next.js (App Router)Server Components by default, Server Functions, streaming, static, server or client rendering per route. Version 16.4.3.Maintained by Vercel. Runs on any Node.js server or in Docker, or as a static export. Its App Router ships its own React canary build.
React Router (framework mode)A Vite plugin: routes in a file, a loader and an action per route, server rendering, SPA mode, prerendering. Version 8.5.0.Maintained by Shopify. Version 8 came out in June 2026. Server Components support is still experimental.
TanStack StartTanStack Router's type-checked routes, plus server rendering, streaming and server functions (createServerFn). Version 1.169.4.1.0 was announced as stable on October 13, 2026; react.dev's list still says Beta. Uses Vite.
ExpoReact Native apps for iOS, Android and the web, with file-based routes (Expo Router). SDK 58.For phone apps. The build services are optional and paid; the framework is free.
Vite and a routerA development server and a build (Vite 8.3.5), plus React Router or TanStack Router as a library.Browser-only. Server rendering, Server Components and the like are yours to add.

When Vite and a router are enough

react.dev's other path, building from scratch with Vite, Parcel or Rsbuild, is right for some apps: when your app has needs no framework serves well, when you want to learn how a React app fits together, or when you're sure you'll never need server rendering. In practice that often means:

  • A dashboard or tool behind a login, where search engines never look and an API already exists.
  • React added to part of an existing page, like a widget inside a site built with something else.
  • Learning React, as this course does in its live examples.

You then choose the parts. A common pair is React Router as a library (its data mode) and a data library:

A Vite app with React Router as a libraryJSX
// src/main.jsx, in a Vite app
import { createRoot } from "react-dom/client";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import Home from "./Home.jsx";
import Tea, { teaLoader } from "./Tea.jsx";

const router = createBrowserRouter([
  { path: "/", Component: Home },
  { path: "/teas/:id", Component: Tea, loader: teaLoader },
]);

createRoot(document.getElementById("root")).render(<RouterProvider router={router} />);

Lines 8 to 11 list the routes, and line 10 gives the tea page a loader, so its data starts with the navigation, not after the render: no waterfall. Line 13 renders the router where main.jsx rendered App before. It's a browser-only app, hosted as plain files. The live examples in this course run React 19.3.1 this way too, without a framework, so that every line is in front of you.

Choosing for real projects

No tool is best for everything. Pick what your project needs, and the playground shows how each tool fits and why, from each project's own docs. Try a public shop (search engines), then an admin tool (behind a login), then a phone app:

Playground: which tool fits this project?
The toolspick a need
  • Next.jsPick a need
  • React Router (framework)Pick a need
  • TanStack StartPick a need
  • ExpoPick a need
  • Vite and a routerPick a need
Try it yourself
Nothing picked yet

Pick what's true for your project. Each tool shows how well it fits, and why.

Press any number of needs. Each tool's fit is the weakest among the needs you picked, with one reason per need, from each project's documentation as checked on October 26, 2026. It's a starting point for your own reading, not a ranking.

For Search engines must read its pages, the three web frameworks are good fits and Vite works only with work. Add Everything is behind a login instead, and Vite and a router become a good fit too. Pick I want Server Components and Server Functions and It must run on static hosting together: nothing fits, because Server Functions need a server. When two tools both fit, the deciding questions are usually your team's: which one you know, where you'll host it, and whose docs you like.

Which kind of tool fits?
  • A public tea shop whose product pages must show up in search
  • An admin dashboard behind a login, using the company's existing API
  • An app in the App Store for ordering tea
  • A blog with comments that writers update every day
  • A price chart added to one page of an existing PHP site
  • A small project to learn React itself
  • One booking app for Android, iOS and the web
Try it yourself
0 of 7 correct

Sort each project: a web framework, Vite and a router, or Expo for phone apps.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A framework is a different React." No. Components, Hooks and state are the same; the framework adds the parts around them.
  • "A framework needs a server." No. The recommended ones can all build a browser-only app or static files; only some features, like Server Functions, need a server.
  • "Vite is a framework." No. It's a build tool: a development server and a build. React Router's framework mode and TanStack Start are built on it.
  • "Without a framework, apps are always slow." No. The waterfall comes from when data starts loading. A router with loaders avoids it; a framework just has it built in.
  • "React Router is only a library." Not since version 7. It's a library in declarative and data mode, and a framework in framework mode.
  • "Create React App is the standard way to start." No. It was deprecated in February 2025.

Practice exercises

The exercises use the lesson's two models: the stopwatch in timeline.jsx, and the file rules in routes.js. Try a guess first, then use the hints.

Exercise 1 · Warm-upWhen does the page show?

Predict the time the timeline gives for "The page shows", then press Open the tea page in the result to check.

import { Suspense, lazy, use, useEffect, useState } from "react";import { Timeline, clock } from "./timeline.jsx"; // Change to true: start loading the data as soon as the page is chosen.const startDataEarly = false; let teas = null;function loadTeas() {  teas ??= clock.wait(500).then(() => {    clock.mark("The data arrives");    return ["Mint", "Chai", "Sencha"];  });  return teas;} // Stands in for downloading the tea page's code, which takes 500 ms.const TeaPage = lazy(() =>  clock.wait(500).then(() => {    clock.mark("The page's code arrives");    return { default: TeaList };  }),); function TeaList() {  const list = use(loadTeas());  useEffect(() => clock.mark("The page shows"), []);  return <ul>{list.map((tea) => <li key={tea}>{tea}</li>)}</ul>;} export default function App() {  const [open, setOpen] = useState(false);   function openTeaPage() {    clock.start();    if (startDataEarly) loadTeas();    setOpen(true);  }   return (    <>      <button onClick={openTeaPage} disabled={open}>Open the tea page</button>      {open && (        <Suspense fallback={<p>Loading…</p>}>          <TeaPage />        </Suspense>      )}      <Timeline />    </>  );}
ResultLoading…

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upWhich file shows /teas/chai?

    The box holds /teas/chai. What does the last line of the result say? Answer first, then press Edit & run.

    import { useState } from "react";
    import { matchUrl, toRoute } from "./routes.js";
    
    // The files in a Next.js app's app folder. Add one, and its page exists.
    const files = [
      "app/page.jsx",
      "app/teas/page.jsx",
      "app/teas/[id]/page.jsx",
      "app/cart/page.jsx",
    ];
    
    export default function App() {
      const [url, setUrl] = useState("/teas/chai");
      const match = matchUrl(files, url);
    
      return (
        <>
          <table>
            <thead>
              <tr>
                <th>File</th>
                <th>Address</th>
              </tr>
            </thead>
            <tbody>
              {files.map((file) => (
                <tr key={file}>
                  <td>{file}</td>
                  <td>{toRoute(file) ?? "not a page"}</td>
                </tr>
              ))}
            </tbody>
          </table>
          <label className="row">
            Visit
            <input value={url} onChange={(event) => setUrl(event.target.value)} />
          </label>
          <p>{match ? `${match.file} shows it, with params ${JSON.stringify(match.params)}` : "No page here: Next.js shows its 404 page."}</p>
        </>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd a reviews page

      Visiting /teas/mint/reviews finds no page. Add one file to the list so it does. Which file did you add?

      import { useState } from "react";import { matchUrl, toRoute } from "./routes.js"; // The files in a Next.js app's app folder. Add one, and its page exists.const files = [  "app/page.jsx",  "app/teas/page.jsx",  "app/teas/[id]/page.jsx",  "app/cart/page.jsx",]; export default function App() {  const [url, setUrl] = useState("/teas/mint/reviews");  const match = matchUrl(files, url);   return (    <>      <table>        <thead>          <tr>            <th>File</th>            <th>Address</th>          </tr>        </thead>        <tbody>          {files.map((file) => (            <tr key={file}>              <td>{file}</td>              <td>{toRoute(file) ?? "not a page"}</td>            </tr>          ))}        </tbody>      </table>      <label className="row">        Visit        <input value={url} onChange={(event) => setUrl(event.target.value)} />      </label>      <p>{match ? `${match.file} shows it, with params ${JSON.stringify(match.params)}` : "No page here: Next.js shows its 404 page."}</p>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA fixed folder and a bracket folder

        The list now has app/teas/new/page.jsx too, and the box holds /teas/new. Which file shows it? Answer, then press Edit & run.

        import { useState } from "react";
        import { matchUrl, toRoute } from "./routes.js";
        
        // The files in a Next.js app's app folder. Add one, and its page exists.
        const files = [
          "app/page.jsx",
          "app/teas/page.jsx",
          "app/teas/[id]/page.jsx",
          "app/cart/page.jsx",
          "app/teas/new/page.jsx",
        ];
        
        export default function App() {
          const [url, setUrl] = useState("/teas/new");
          const match = matchUrl(files, url);
        
          return (
            <>
              <table>
                <thead>
                  <tr>
                    <th>File</th>
                    <th>Address</th>
                  </tr>
                </thead>
                <tbody>
                  {files.map((file) => (
                    <tr key={file}>
                      <td>{file}</td>
                      <td>{toRoute(file) ?? "not a page"}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <label className="row">
                Visit
                <input value={url} onChange={(event) => setUrl(event.target.value)} />
              </label>
              <p>{match ? `${match.file} shows it, with params ${JSON.stringify(match.params)}` : "No page here: Next.js shows its 404 page."}</p>
            </>
          );
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeTwo waits, one start

          This tea page also shows its reviews, which load in 500 ms. It now shows at 1500 ms. Make it show as soon as it can. When does it show?

          import { Suspense, lazy, use, useEffect, useState } from "react";import { Timeline, clock } from "./timeline.jsx"; // Change to true: start loading the data as soon as the page is chosen.const startDataEarly = false; let reviews = null;function loadReviews() {  reviews ??= clock.wait(500).then(() => {    clock.mark("The reviews arrive");    return 12;  });  return reviews;} let teas = null;function loadTeas() {  teas ??= clock.wait(500).then(() => {    clock.mark("The data arrives");    return ["Mint", "Chai", "Sencha"];  });  return teas;} // Stands in for downloading the tea page's code, which takes 500 ms.const TeaPage = lazy(() =>  clock.wait(500).then(() => {    clock.mark("The page's code arrives");    return { default: TeaList };  }),); function TeaList() {  const list = use(loadTeas());  useEffect(() => clock.mark("The page shows"), []);  return (    <>      <ul>{list.map((tea) => <li key={tea}>{tea}</li>)}</ul>      <Reviews />    </>  );} function Reviews() {  const count = use(loadReviews());  useEffect(() => clock.mark("The reviews show"), []);  return <p>{count} reviews</p>;} export default function App() {  const [open, setOpen] = useState(false);   function openTeaPage() {    clock.start();    if (startDataEarly) loadTeas();    setOpen(true);  }   return (    <>      <button onClick={openTeaPage} disabled={open}>Open the tea page</button>      {open && (        <Suspense fallback={<p>Loading…</p>}>          <TeaPage />        </Suspense>      )}      <Timeline />    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeA folder for every depth

            The docs folder is [...slug]: in Next.js, a page for any depth below /docs. The model doesn't know it yet. Teach routes.js. What params does /docs/brewing/water get?

            import { useState } from "react";import { matchUrl, toRoute } from "./routes.js"; // The files in a Next.js app's app folder. Add one, and its page exists.const files = [  "app/page.jsx",  "app/teas/page.jsx",  "app/teas/[id]/page.jsx",  "app/cart/page.jsx",  "app/docs/[...slug]/page.jsx",]; export default function App() {  const [url, setUrl] = useState("/docs/brewing/water");  const match = matchUrl(files, url);   return (    <>      <table>        <thead>          <tr>            <th>File</th>            <th>Address</th>          </tr>        </thead>        <tbody>          {files.map((file) => (            <tr key={file}>              <td>{file}</td>              <td>{toRoute(file) ?? "not a page"}</td>            </tr>          ))}        </tbody>      </table>      <label className="row">        Visit        <input value={url} onChange={(event) => setUrl(event.target.value)} />      </label>      <p>{match ? `${match.file} shows it, with params ${JSON.stringify(match.params)}` : "No page here: Next.js shows its 404 page."}</p>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which job is being done, and whether React, a router, or a framework does it.

              The React frameworks quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does react.dev's Creating a React App page recommend for a new app or website?

                Choose an answer to see the explanation.

              2. Question 2 of 8In the waterfall example, each step takes 500 ms. Why does the page show at 1000 ms, not 500 ms?

                Choose an answer to see the explanation.

              3. Question 3 of 8In this Next.js app folder, which file is not a page?

                Read the code, then predictText
                app/
                  page.jsx
                  about/page.jsx
                  teas/[id]/page.jsx
                  teas/[id]/Price.jsx

                Choose an answer to see the explanation.

              4. Question 4 of 8Do the frameworks react.dev recommends need a server?

                Choose an answer to see the explanation.

              5. Question 5 of 8How do TanStack Start's server functions differ from the Server Functions in the last lesson?

                Choose an answer to see the explanation.

              6. Question 6 of 8For which of these is Vite and a router usually enough?

                Choose an answer to see the explanation.

              7. Question 7 of 8Which job does React do on its own, with no framework?

                Choose an answer to see the explanation.

              8. Question 8 of 8Where does the React that a Next.js App Router page runs come from?

                Choose an answer to see the explanation.

              Key takeaways

              • React renders components. A framework adds routing, data loading, rendering strategies, bundling, and server code, built to work together.
              • react.dev recommends a framework for a new app: Next.js, React Router, or Expo for phone apps, with TanStack Start and RedwoodSDK as up-and-coming.
              • Hand-built apps tend to make waterfalls: data that waits for code that waits for a render. Frameworks start a page's code and data together.
              • Frameworks don't require a server: they can build browser-only apps and static files, and add server rendering per route.
              • Vite and a router are enough for apps behind a login, React inside another site, and learning.

              Remember the one-liner.
              React draws the page; a framework runs the app around it.

              Coming next: A tour of the React repository. The last stage opens React's own source code, starting with what lives where.

              CompleteFrontend Clear concepts. Working examples.