React

React is a JavaScript library for building component based interactive UI’s.

Unlike a fully featured framework, its up to you to deal with concerns such as routing, state management, internationalization, form validation, etc, of which React is unopinionated.

React popularised representing markup as JSX (JavaScript XML), a syntax that combines HTML and JavaScript in an expressive way, making it easier to create complex user interfaces.

At runtime React takes a tree of components and builds a JavaScript data structure called the virtual DOM. This virtual DOM is different from the actual DOM in the browser. It’s an efficient, in-memory representation of the component tree. When the state or the data of a component changes, React updates the corresponding nodes in the virtual DOM and compares it against the previous version to identify elements that need updating in the real DOM.

React (being nearly a decade old) drew inspiration from UI patterns during this era (e.g. MVC, MVVM). React collapses the model, view, and view model into a single component, encapsulating all logic for a small piece of functionality in one place. A bit like PHP does things.

React No Frills

React is simple. Peeling back the layers is just a vanilla index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Padre Gino's</title>
  </head>
  <body>
    <div id="root">not rendered</div>
    <script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
    <script src="./src/App.js"></script>
  </body>
</html>

And a little JS to bootstrap App.js:

const Pizza = (props) => {
  return React.createElement("div", {}, [
    React.createElement("h2", {}, props.name),
    React.createElement("p", {}, props.description),
  ]);
};

const App = () => {
  var margheritaProps = {
    name: "Pizza Margherita",
    description:
      "Delicious classic pizza with tomatoes, mozzarella, and basil.",
  };

  return React.createElement("div", {}, [
    React.createElement("h1", {}, "Padre Gino's Pizza"),
    React.createElement(Pizza, margheritaProps),
    React.createElement(Pizza, {
      name: "The Hawaiian",
      description: "A tropical delight with ham and pineapple.",
    }),
  ]);
};

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(React.createElement(App));

Finally serve this up with npx serve

Tools

npm

Start a fresh project with npm init -y, which will generate a package.json

  • npm install --save-dev prettier or npm -i -D prettier

Prettier

Create .prettierrc to define formatting preferences, leaving this as {} will use default for everything.

"scripts": {
  "format": "prettier --write \"src/**/*.{js,jsx,css,html}\""
},

ESLint

On top of Prettier which takes of all the formatting, you may want to enforce some code styles which pertain more to usage: for example you may want to force people to never use with which is valid JS but ill advised to use. ESLint comes into play here. It will lint for these problems.

Install with:

npm i -D eslint@9.9.1 eslint-config-prettier@9.1.0 globals@15.9.0`

# If working with TypeScript
npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react eslint-plugin-react-hooks

Configure by creating an eslint.config.mjs (as of 2025 .eslintrc has been deprecated).

Helpful ESLint config packages:

  • @typescript-eslint/parser: Allows ESLint to parse TypeScript code
  • @typescript-eslint/eslint-plugin: Provides rules specific to TypeScript
  • eslint-plugin-react: Provides React-specific linting rules
  • eslint-plugin-react-hooks: Enforces rules of hooks
  • eslint-config-prettier: Turns off all ESLint rules that are unnecessary or might conflict with Prettier
  • globals is just a big JSON file of what’s available in each environment. We’re going to be in Node.js and Browser environments so we grabbed those two

Tips:

  • /** @type {import('eslint').Linter.Config[]} */ is a VSCode trick to be able to do auto-completions on the config object
  • The config objects are applied in order. We did ESLint’s JS config first, and then our custom one so we can overwrite it where we want to, and then the Prettier one should always come last as all it does is turn off rules that Prettier itself does; it doesn’t add anything.
  • Add a script to package.json called lint that runs eslint .
  • npm run lint -- --fix to auto fix lints
  • npm run lint -- --debug to get some helpfgul

Vite

The build tool we are going to be using today is called Vite. Vite (pronounced “veet”, meaning quick in French) is a tool put out by the Vue team that ultimately ends up wrapping Rollup (rust based of course) which does the actual bundling. The end result is a tool that is both easy to use and produces a great end result.

Install vite itself and the React specific features we will need:

npm install -D vite@5.4.2 @vitejs/plugin-react@4.3.1

Now we need to do some surgery, first by removing any manual React JS imports in index.html. We need to add module to the script tag <script type="module" src="./src/App.js"></script> so that the browser knows it’s working with modern browser technology that allows you in development mode to use modules directly. Instead of having to reload the whole bundle every time, your browser can just reload the JS that has changed. It allows the browser to crawl the dependency graph itself which means Vite can run lightning fast in dev mode. It will still package it up for production so we can support a range of browsers.

Next some NPM scripts to start Vue:

"dev": "vite",
"build": "vite build",
"preview": "vite preview"
  • dev: will run up a server on port 5173 (Fun fact, 5173 sort of spells VITE if you make the 5 its Roman Numeral version, V)
  • build: will prepare static files to be deployed (to somewhere like GitHub Pages, Vercel, Netlify, AWS S3, etc.)
  • preview: lets you preview your production build locally

Be sure to also add "type": "module" to your package.json. Vite has deprecated support for Common.js and now requires you to use ESM style modules.

Vite Proxy

Vite can proxy an API, this is GREAT for local dev and avoiding CORS headaches. With this config requests to /api and /public will be proxied to a completely separate API backend localhost:3000, however will make it appear to be coming from the same origin.

export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
      "/public": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
  plugins: [react()],
});

TypeScript

Install React TS typedefs:

npm i -D @types/react @types/react-dom

When you use TypeScript with JavaScript libraries like React, TypeScript needs type definition files (.d.ts) to understand the types of the library’s exports. The @types/react and @types/react-dom packages provide these type definitions.

These packages tell TypeScript:

  • What props components accept
  • What JSX elements are valid
  • The types for hooks like useState, useEffect, etc.
  • The types for the jsx-runtime module (which handles JSX transformation)

Bleeding Edge and Alternative Tools

  • pnpm a faster, disk space efficient package manager
  • Biome one toolchain for your web project, format, lint, and more in a fraction of a second
  • Oxlint (/oh-eks-lint/) is designed to catch erroneous or useless code without requiring any configurations by default

JSX

In React, everything is just Javascript. JSX takes this to the next level, providing syntactic sugar to crunch out lots of React.createElement, based on a HTML-list syntax. Vite transpiles JSX into JS.

const Pizza = (props: { name: string, description: string, image: string }) => {
  return (
    <div className="pizza">
      <h1>{props.name}</h1>
      <p>{props.description}</p>
      <img src={props.image} alt={props.name} />
    </div>
  );
};

export default Pizza;

DOM control

JSX expressions must have a single parent element. As we now know, will result in a top-level parent React.createElement(), which can in-turn be populated with many child React.createElement() calls. Using a single outer <div> would be one option, but if you really don’t want to include any outer DOM, can use the <React.Fragment> shown above.

Styling

As JSX is just JS, reserved keywords like class and for cannot be used. To avoid keyword clashes alternative such as className as provided.

<React.Fragment>
  <img src={this.state.imageUrl} alt="" />
  <span className="">{this.formatCount()}</span>
  <button>Increment</button>
</React.Fragment>

While using CSS classes is best, inline styles can be acheived by setting the style attribute on a JSX element to a Javascript object, like so:

styles = {
  fontSize: 12,
  fontWeight: "bold"
};

render() {
  return (
    <React.Fragment>
      <span style={this.styles} className="badge badge-primary m-2">
        {this.formatCount()}
      </span>

Or using an anonymous object like this:

<span style={{ fontSize: 30 }} className="badge badge-primary m-2">

Rendering Lists

The map (higher order) function, can be used to deal with lists:

class Counter extends Component {
  state = {
    count: 0,
    tags: ["tag1", "tag2", "tag3"]
  };

  render() {
    return (
      <React.Fragment>
        <ul>
          {this.state.tags.map(tag => (
            <li>{tag}</li>
          ))}
        </ul>

This will render a list of naked li, however React will throw a warning in the console:

Warning: Each child in a list should have a unique “key” prop.

In order to do virtual DOM to DOM comparison, React needs unique identifiers on everything. As there is no id on these li’s, the key attribute can be used:

<li key={tag}>{tag}</li>

Handling Events

JSX provides event handler attributes such as onClick:

handleIncrement() {
  console.log("Increment clicked", this.state.count);
}

render() {
  return (
    <React.Fragment>
      <button
        onClick={this.handleIncrement}
        className="btn btn-secondary btn-sm"
      >
        Increment
      </button>

Passing Parameters to Event Handlers

It is common to want to pass some state along to the event handler, when its invoked.

One option is to create a wrapper function:

handleIncrement = product => {
  console.log(product);
  this.setState({ count: this.state.count + 1 });
};

render() {
  return (
    <React.Fragment>
      <button
        onClick={() => this.handleIncrement({ id: 1 })}
        className="btn btn-secondary btn-sm"
      >
        Increment
      </button>

Passing JSX as Props

To make components even more useful, its possible to pass in inner JSX content, for example:

<Counter key={c.id} value={c.value} selected={true}>
  <h2>semaphore</h2>
</Counter>

The props of the child component, exposes this h2 via a list called children. To make use of the props.children property is easy:

render() {
  return (
    <React.Fragment>
      {this.props.children}
      <span className={this.getBadgeClasses()}>{this.formatCount()}</span>
      ...

React Hooks

A hook called such because it’s a hook that gets caught every time the render function gets called. Because the hooks get called in the same order every single time, they’ll always point to the same piece of state. Because of that they can be stateful: you can keep pieces of mutable state using hooks and then modify them later using their provided updater functions.

An absolutely critical concept for you to grasp is hooks rely on this strict ordering. As such, do not put hooks inside if statements or loops. If you do, you’ll have insane bugs that involve useState returning the wrong state. If you see useState returning the wrong piece of state, this is likely what you did. Every hook must run every time in the same order. They should always be called at the top level of a component.

useState

The call to useState below is called a hook.

// in Order.jsx
import { useState } from "react";

// pizzaType and pizzaSize location
const [pizzaType, setPizzaType] = useState("pepperoni");
const [pizzaSize, setPizzaSize] = useState("medium");

// replace input
<select
  onChange={(e) => setPizzaType(e.target.value)}
  name="pizza-type"
  value={pizzaType}
>
  []
</select>

// add to all the radio buttons
onChange={(e) => setPizzaSize(e.target.value)}

Notes:

  • The argument given to useState is the default value. In our case, we could give it "" as our default value to make the user have to select something first, but in our case we want to default to pepperoni pizza and medium size.
  • useState returns to us an array with two things in it: the current value of that state and a function to update that state. We’re using a feature of JavaScript called destructuring to get both of those things out of the array.
  • We could have put an onChange handler on each of the radio buttons. However event bubbling works the same in React as it does in the normal DOM and we could put it directly on the div that encapsulates all the radio buttons and just have to do it once.
  • The above is known as a controlled form in that we’re using hooks to control each part of the form. In reality, it’s better to leave these uncontrolled (aka don’t set the value) and wrap the whole thing in a form, and listen for submit events and use that event to gather info off the form. If you need to do dynamic validation, react to a user typing a la typeahead, then a controlled input is perfect, otherwise stick to uncontrolled. Also what’s new in React is called a “form action” that is considered unstable. In the future you will just add <form action="blah">[...]</form> and a form action will handle the entire form for you.

useEffect

Effect here means side-effect. We have work on the render hotpath, and then “other background work” we want to happen that doesn’t need to be on that path.

useEffect allows you to say do a render of this component first so the user can see something, then as soon as the render is done, then take care of these other tasks. Here we want the user to see our UI first then we want to make a request to the API so we can initialize a list of pizzas.

useEffect(() => {
  fetchPizzaTypes();
  return () => clearTimeout(timeout); // optional return allows for effect cleanup work
}, []); // empty array here is the state comparison parameter - effectively this is a one-off effect

More complete example:

const intl = Intl.NumberFormat("en-AU", {
  style: "currency",
  currency: "AUD",
});

export default function Order(): JSX.Element {
  const [pizzaList, setPizzaList] = useState<PizzaType[]>([]);
  const [pizzaType, setPizzaType] = useState("pepperoni");
  const [pizzaSize, setPizzaSize] = useState("M");
  const [loading, setLoading] = useState(true);

  let price, selectedPizza;

  if (!loading) {
    selectedPizza = pizzaList.find((pizza) => pizza.id === pizzaType);
  }

  async function fetchPizzaTypes() {
    await new Promise((resolve) => setTimeout(resolve, 3000)); // fake loading delay
    const pizzasResponse = await fetch("/api/pizzas");
    const pizzasJson = await pizzasResponse.json();
    setPizzaList(pizzasJson);
    setLoading(false);
  }

  useEffect(() => {
    fetchPizzaTypes();
  }, []);

  return (
    <div className="order">
      <h2>Create Order</h2>
      <form>

      ...

        <div className="order-pizza">
          {selectedPizza ? (
            <Pizza
              name={selectedPizza.name}
              description={selectedPizza.description}
              image={selectedPizza.image}
            />
          ) : (
            <div>Loading pizza...</div>
          )}
          <p>
            {selectedPizza
              ? intl.format(
                  selectedPizza.sizes[
                    pizzaSize as keyof typeof selectedPizza.sizes
                  ],
                )
              : "$0.00"}
          </p>
        </div>
      </form>
    </div>
  );
}

Notes:

  • We put all the logic for fetching pizza types in an async function to make it more readable. You can’t make the function provided to useEffect async.
  • The [] at the end of the useEffect is where you declare your data dependencies. React wants to know when to run that effect again. You don’t give it data dependencies, it assumes any time any hook changes, the effect needs to be re-scheduled and run again. This is bad because that would mean any time setPizzaList gets called it’d re-run render and all the hooks again. It’d run infinitely since fetchPizzaTypes calls setPizzaList.
  • We’re using a loading flag to only display data once it’s ready. This is how you do conditional showing/hiding of components in React.
  • The key portion is an interesting one. When React renders arrays of things, it doesn’t know the difference between something is new and something is just being re-ordered in the array (think like changing the sorting of a results list, like price high-to-low and then priced low-to-high). Because of this, if you don’t tell React how to handle those situations, it just tears it all down and re-renders everything anew. This can cause unnecessary slowness on devices. This is what key is for. Key tells React “this is a simple identifier of what this component is”. If React sees you just moved a key to a different order, it will keep the component tree. So key here is to associate the key to something unique about that component. 99/100 this is a database ID of some variety.

Custom Hooks

A custom hook is simply a function that calls other hooks, allowing you to encapsulate and reuse stateful logic across multiple components.

One thing that’s pretty special about hooks is their composability i.e. using hooks to make other hooks. People tend to call these custom hooks. There are even people who go as far to say:

“never make an API request in a component, always do it in a hook”

I don’t know if I’m as hardcore as that but I see the logic in it. If you make a custom hook for those sorts of things they become individually testable and do a good job to separate your display of data and your logic to acquire data.

Okay, so we want to add a “Pizza of the Day” banner at the bottom of our page. This necessitates calling a special API to get the pizza of the day (which should change every day based on your computer’s time). Let’s first write the component that’s going to use it.

import { usePizzaOfTheDay } from "./PizzaOfTheDay";

const intl = new Intl.NumberFormat("en-AU", {
  style: "currency",
  currency: "AUD",
});

const PizzaOfTheDay = () => {
  const pizzaOfTheDay = usePizzaOfTheDay(); // USING THE CUSTOM HOOK 🎉

  if (!pizzaOfTheDay) {
    return <div>Loading...</div>;
  }

  return (
    <div className="pizza-of-the-day">
      <h2>Pizza of the Day</h2>
      <div>
        <div className="pizza-of-the-day-info">
          <h3>{pizzaOfTheDay.name}</h3>
          <p>{pizzaOfTheDay.description}</p>
          <p className="pizza-of-the-day-price">
            From: <span>{intl.format(pizzaOfTheDay.sizes.S)}</span>
          </p>
        </div>
        <img
          className="pizza-of-the-day-image"
          src={pizzaOfTheDay.image}
          alt={pizzaOfTheDay.name}
        />
      </div>
    </div>
  );
};

export default PizzaOfTheDay;

Okay, let’s go make the hook! Make a file called usePizzaOfTheDay.ts (in a React project its common to just use JSX/TSX for all “React-y” things):

import { useState, useEffect } from "react";
import { PizzaType } from "./types";

export const usePizzaOfTheDay = () => {
  const [pizzaOfTheDay, setPizzaOfTheDay] =
    (useState < PizzaType) | (null > null);

  useEffect(() => {
    async function fetchPizzaOfTheDay() {
      const response = await fetch("/api/pizza-of-the-day");
      const data = await response.json();
      setPizzaOfTheDay(data);
    }

    fetchPizzaOfTheDay();
  }, []);

  return pizzaOfTheDay;
};

Notes:

  • The cool part here is the usePizzaOfTheDay(). We now just get to rely on that this going to provide us with the pizza of the day from within the black box of the hook working.
  • Custom hooks call other React hooks and follow the rules of hooks, such as being called in the same order and not being placed inside conditional statements or loops.
  • What the sell to using custom hook? They enable reusable, composable logic that can be shared across different components, reducing code duplication and improving modularity.
  • Custom hooks commonly use useState and useEffect to manage state and side effects within the hook’s logic.
  • Custom hooks usually start with the prefix ‘use’, such as usePizzaOfTheDay, which indicates that it is a hook and follows React’s hook naming conventions.
  • A handy debugging technique made especially for custom hooks is useDebugValue(pizzaOfTheDay ? ${pizzaOfTheDay.name} : "Loading..."). Now open your React Dev Tools and inspect our PizzaOfTheDay component. You’ll see our debug value there. This is helpful when you have lots of custom hooks and in particular lots of reused custom hooks that have differing values. It can help at a glance to discern which hook has which data inside of it.

useContext

Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many components in the middle, or if many components in your app need the same information. Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.

Let’s make a cart indicator on the top right of the page. Create a file called Header.tsx and put this in there.

export default function Header() {
  return (
    <nav>
      <h1 className="logo">Padre Gino's Pizza</h1>
      <div className="nav-cart">
        🛒<span className="nav-cart-number">5</span>
      </div>
    </nav>
  );
}

Use the new Header component in App.tsx:

import Header from "./Header";

<Header />
...

The count is hard-coded to 5 right now. But we want that number in .nav-cart-number to reflect how many items we have in our cart. How would we do that? We could move all of cart and its hooks to App.jsx and pass it into both Header and Order. In an app this small, that could be the right choice. But let’s look at another way to do it, context.

So let’s make it work. Make a file called contexts.tsx. It’s not a component so I tend to not capitalise it. The React docs do capitalise it. Up to you.

import { createContext } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { CartType } from "./types";

type CartContextType = [CartType[], Dispatch<SetStateAction<CartType[]>>];

const noop = (() => {}) as Dispatch<SetStateAction<CartType[]>>;

export const CartContext = createContext<CartContextType>([[], noop]);

The [[], function () {}] is that it’s a React hook: an array where the first value is an array (like our cart is) and the second value is a function (the setCart function). Next wire it up in App.tsx:

// at the top
import { StrictMode, useState } from "react"; // need useState
import { CartContext } from "./contexts";

// replace App
const App = () => {
  const cartHook = useState([]);
  return (
    <StrictMode>
      <CartContext.Provider value={cartHook}>
        <div>
          <Header />
          <Order />
          <PizzaOfTheDay />
        </div>
      </CartContext.Provider>
    </StrictMode>
  );
};

Let’s go make Order.tsx work now:

import { useState, useEffect, useContext } from "react"; // need useContext
import { CartContext } from "./contexts";

const [cart, setCart] = useContext(CartContext); // change cart hook to use context

Done. Lastly update the Header component to use context:

import { useContext } from "react";
import { CartContext } from "./contexts";

// top of function
const [cart] = useContext(CartContext);

// replace span number
🛒<span className="nav-cart-number">{cart.length}</span>

Forms and User Input

So now we want to be able to handle the user’s cart and submitting our order. Let’s go add what we need to Order.tsx:

// add import
import Cart from "./Cart";

// add another hook
const [cart, setCart] = useState([]);

// replace <form>
<form
  onSubmit={(e) => {
    e.preventDefault();
    if (!selectedPizza) return;
    const price =
      selectedPizza.sizes[
        pizzaSize as keyof typeof selectedPizza.sizes
      ] ?? 0;
    setCart([...cart, { pizza: selectedPizza, size: pizzaSize, price }]);
  }}
>
  []
</form>;

// just inside the last closing div
{
  loading ? <h2>LOADING </h2> : <Cart cart={cart} />;
}

Now lets make the cart component. Make a file called Cart.tsx and add:

import { CartItemType } from "./types";

const intl = new Intl.NumberFormat("en-AU", {
  style: "currency",
  currency: "AUD",
});

export default function Cart({
  cart,
  checkout,
}: {
  cart: CartItemType[],
  checkout: () => void,
}): JSX.Element {
  let total = 0;
  for (let i = 0; i < cart.length; i++) {
    const current = cart[i];
    if (current) {
      total += current.pizza.sizes[current.size];
    }
  }
  return (
    <div className="cart">
      <h2>Cart</h2>
      <ul>
        {cart.map((item, index) => (
          <li key={index}>
            <span className="size">{item.size}</span> 
            <span className="type">{item.pizza.name}</span> 
            <span className="price">{item.price}</span>
          </li>
        ))}
      </ul>
      <p>Total: {intl.format(total)}</p>
      <button onClick={checkout}>Checkout</button>
    </div>
  );
}

Push Cart to Server

So how do actually checkout on the server? Let’s do that! We probably want to do it as the Order level. It already has the Cart and we can just leave the Cart as a dumb display component. We can just pass a function to call into the Cart component as a prop, and call it and run the function at the Order level. In the Order component add:

// inside the render body
async function checkout() {
  setLoading(true);

  await fetch("/api/order", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      cart,
    }),
  });

  setCart([]);
  setLoading(false);
}

// pass the checkout function down to Cart as prop
<Cart checkout={checkout} cart={cart} />;

Now we can pass that checkout function in and whenever someone clicks inside the form, it will run the checkout function from the Order components. We’re doing a simple loading animation, doing a fetch, and then clearing the status once we’re all done. Not too bad!

React Ecosystem

TanStack Router

So now we have arrived to the point where we want multiple pages in our app. We need some sort of router tool to accomplish that.

We need to install the router itself and then its code generation tool (aka file-based routing) as well as its dev tools.

npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtools

Vite Setup

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { tanstackRouter } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [
    // Please make sure that '@tanstack/router-plugin' is passed before '@vitejs/plugin-react'
    tanstackRouter({
      target: "react",
      autoCodeSplitting: true,
    }),
    react(),
    // ...
  ],
});

routeTree.gen.ts

TanStack Router works by file-based conventions. You create routes in a specific way and TanStack Router will automatically glue it together for you. It does this by creating a file called routeTree.gen.ts. Even though this isn’t a TypeScript project, the fact that this is TypeScript means that VS Code can read the types from your routes and help you with suggestions and intelligent errors. I would suggest adding this file to your .gitignore, and formatters and linters such as Prettier, ESLint as well since it will get autogenerated with every build.

Router Setup

Create a routes directory in your src directory. Let’s make a __root.jsx file in there. This file will be the base template used for every route. Most of this will come from App.jsx:

import { useState } from "react";
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import PizzaOfTheDay from "../PizzaOfTheDay";
import Header from "../Header";
import { CartContext } from "../contexts";

export const Route = createRootRoute({
  component: () => {
    const cartHook = useState([]);
    return (
      <>
        <CartContext.Provider value={cartHook}>
          <div>
            <Header />
            <Outlet />
            <PizzaOfTheDay />
          </div>
        </CartContext.Provider>
        <TanStackRouterDevtools />
      </>
    );
  },
});
  • We added an <Outlet/> instead of our Order component. Now we can swap in new routes there! Notice that the Header will always be there as will the PizzaOfTheDay.
  • We added TanStack’s excellent dev tools. We’ll take a look at them in a bit, but they’re in the bottom left, you can click on them once your app loads.
  • <> and </> are for when you want to render two sibling components (our context and our dev tools) but don’t want to add a random div there. React requires you only return one top level fragment and we can do that with <> and </>

Great, let’s go modify App.tsx now:

// add at top
// remove useState import from react import
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

const router = createRouter({ routeTree });

// replace App
const App = () => {
  return (
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
};

This just imports the generated routeTree and then makes use of it in the project. This file really only should be used for rendering the file. Everything else should likely live in __root.tsx.

Okay, move Order.tsx from the base directory and into routes. If it asks, say yes to update paths. Rename it to order.lazy.tsx Let’s modify it now to make it a route.

// at top
import { createLazyFileRoute } from "@tanstack/react-router";

// make sure you modified the relative paths here – VS Code may have done this for you already
import { CartContext } from "../contexts";
import Cart from "../Cart";
import Pizza from "../Pizza";

export const Route = createLazyFileRoute("/order")({
  component: Order,
});

function Order() {
  // Order component code …
}
  • Here we’re making a new route. We define what URL it’s at, /order, and what component to render, Order.
  • We’re making it lazy. It will now code split this for us and lazy-load our route for us. This really helps in large apps. Take a gander at the network pane in browser devtools, you’ll see the router will make requests to order.lazy.tsx and its child components Cart.tsx and so on. Cool! Lazy loading is not always a “free lunch”, so measure it against the traditional approach of just bundling these routable components.

Let’s add a home page! Make a file called index.lazy.tsx in the routes folder:

import { createLazyFileRoute, Link } from "@tanstack/react-router";

export const Route = createLazyFileRoute("/")({
  component: RouteComponent,
});

function RouteComponent() {
  return (
    <div className="index">
      <div className="index-brand">
        <h1>Padre Gino's</h1>
        <p>Pizza & Art at a location near you</p>
      </div>
      <ul>
        <li>
          <Link to="/order">Order</Link>
        </li>
        <li>
          <Link to="/past">Past Orders</Link>
        </li>
      </ul>
    </div>
  );
}

Lastly let’s modify Header to be able to link back to the home page:

import { Link } from "@tanstack/react-router";

export default function Header() {
  // ...

  return (
    <nav>
      <Link to="/">
        <h1 className="logo">Padre Gino's Pizza</h1>
      </Link>
      ...
    </nav>
  );
}

TanStack Query

Let’s make make a past orders page, which interestingly contains highly cachable data. Create a new file, past.lazy.jsx. If your Vite server is already running, it will automatically stub it out for you!

npm i @tanstack/react-query
npm i -D @tanstack/react-query-devtools @tanstack/eslint-plugin-query

TanStack Query makes doing async API calls so much easier. The code is cleaner, easier to read, better cached, and less bug prone. It’s good for you to know how to use effects, but as you go forward just use TanStack Query for API calls.

Let’s start by adding their ESLint config to ours. In eslint.config.mjs:

// at top
import pluginQuery from "@tanstack/eslint-plugin-query";

// under reactPlugin.configs.flat["jsx-runtime"]
...pluginQuery.configs["flat/recommended"],

Let’s also add the dev tools, like we did for the router. In src/routes/__root.tsx:

// at top
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// under router dev tools
<ReactQueryDevtools />;

Finally, we need to add the QueryClient. In App.tsx, add:

// Add imports
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

// Create a property under the router
const queryClient = new QueryClient()

// Add the provider to the app
<QueryClientProvider client={queryClient}>
  <RouterProvider router={router} />
</QueryClientProvider>

So react-query makes interacting with APIs very simple and makes it easy to read. You just read a hook and it’ll either give you a isLoading status or the data. Once the data comes back, it’ll refresh the component with the data. So let’s start by writing our very simple fetch call. Create a folder called src/api and create getPastOrders.ts and add:

export default async function getPastOrders(page) {
  const response = await fetch(`/api/past-orders?page=${page}`);
  const data = await response.json();
  return data;
}

Let’s now go make past.lazy.tsx:

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import getPastOrders from "../api/getPastOrders";

export const Route = createLazyFileRoute("/past")({
  component: PastOrdersRoute,
});

function PastOrdersRoute() {
  const [page, setPage] = useState(1);
  const { isLoading, data } = useQuery({
    queryKey: ["past-orders", page],
    queryFn: () => getPastOrders(page),
    staleTime: 30000,
  });
  if (isLoading) {
    return (
      <div className="past-orders">
        <h2>LOADING </h2>
      </div>
    );
  }
  return (
    <div className="past-orders">
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>Date</td>
            <td>Time</td>
          </tr>
        </thead>
        <tbody>
          {data.map((order) => (
            <tr key={order.order_id}>
              <td>{order.order_id}</td>
              <td>{order.date}</td>
              <td>{order.time}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="pages">
        <button disabled={page <= 1} onClick={() => setPage(page - 1)}>
          Previous
        </button>
        <div>{page}</div>
        <button disabled={data.length < 10} onClick={() => setPage(page + 1)}>
          Next
        </button>
      </div>
    </div>
  );
}
  • We’re using the useQuery hook to make API calls and we provide it the queryFn of how to go fetch that data.
  • We’re giving it keys which act as cache keys. We’re giving past-orders as the key but it could be any unique key to this page. Then we give it the page. What’s cool about this is that while we will request the page 1 the first time we request it, the second time we request page 1 it’ll see it’ll see that we already have this in cache and not request it. How cool is is that?!
  • Now open the dev tools. You can see all the pages being loaded in.
  • Try taking out page from the query key. It’ll yell at you. This is the ESLint config we pulled in from Tanstack Query. Because we’re using that page in the request, we need to use it as a caching key. If you depend on a variable to make a request, it should be apart of the caching key.
  • We’re giving it a staleTime of 30 seconds (30,000 milliseconds). This allows someone using the page to browse around a bit and not bombard the API too much but the page won’t ever be too stale. If you omit staleTime, it will refetch every time.

Resources