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 prettierornpm -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 TypeScripteslint-plugin-react: Provides React-specific linting ruleseslint-plugin-react-hooks: Enforces rules of hookseslint-config-prettier: Turns off all ESLint rules that are unnecessary or might conflict with Prettierglobalsis 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.jsoncalledlintthat runseslint . npm run lint -- --fixto auto fix lintsnpm run lint -- --debugto 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 port5173(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-runtimemodule (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
useStateis 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. useStatereturns 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
onChangehandler 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 thedivthat 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 forsubmitevents 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
asyncfunction to make it more readable. You can’t make the function provided touseEffectasync. - The
[]at the end of theuseEffectis 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 timesetPizzaListgets called it’d re-run render and all the hooks again. It’d run infinitely sincefetchPizzaTypescallssetPizzaList. - We’re using a
loadingflag to only display data once it’s ready. This is how you do conditional showing/hiding of components in React. - The
keyportion 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 whatkeyis for. Key tells React “this is a simple identifier of what this component is”. If React sees you just moved akeyto a different order, it will keep the component tree. Sokeyhere is to associate thekeyto 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
useStateanduseEffectto 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 ourPizzaOfTheDaycomponent. 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 ourOrdercomponent. Now we can swap in new routes there! Notice that theHeaderwill always be there as will thePizzaOfTheDay. - 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.tsxand its child componentsCart.tsxand 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
useQueryhook to make API calls and we provide it thequeryFnof how to go fetch that data. - We’re giving it keys which act as cache keys. We’re giving
past-ordersas 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
staleTimeof 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
- Complete Intro to React v9 (2024 refresh) by Brian Holt
- v8 of the course - for
react-router - citr-v9-project
- Keep render paths within components as lean as possible, as they are often hot paths being run many times. This is the most common cause of CLS (Cumulative Layout Shift) quantifies the amount of unexpected layout shifts that occur during the lifespan of a web page.