React Build Book
- Read the notes firstEach build points back to a section of the React introduction notes.
- Build alongLook at the finished model, then follow the steps in your own project.
- Tick off each stepFinished builds snap into the row above. Your progress is saved in this browser.
Set up your project
- Node.js
- Vite
- JSX
Every build in this book lives in one React project on your laptop. You'll create it with Vite, a tool that sets React up for you and refreshes the page each time you save.
Hello from React 👋
If you can read this in your own browser, you're ready for Build 1.
Steps
-
Check that Node.js is installed
Open a terminal (in VS Code: Ctrl + `) and run:
You need Node 22.12 or newer. If the command isn't found or the number is lower, install the LTS version from nodejs.org, then close and reopen the terminal.
-
Create the project
Go to the folder where you keep your coursework and run:
This makes a folder called
react-playgroundwith a small React app inside. If npm asks "Ok to proceed?", type y and press Enter. -
Install and start it
Open the address it prints, usually
http://localhost:5173. You'll see the Vite + React demo page with a counter. Keep this terminal running while you work. Ctrl + C stops it. -
Look around
Open the
react-playgroundfolder in VS Code. These are the files that matter:File What it does index.htmlThe only HTML page. It has an empty <div id="root">that React fills in.src/main.jsxStarts React and puts <App />inside#root. You'll rarely touch it.src/App.jsxYour root component, the top of the tree. You'll edit it in every build. src/index.cssStyles for the whole app. package.jsonThe project's name, its scripts (like npm run dev) and the libraries it uses.node_modules/Downloaded libraries. Never edit it and never upload it. -
Clear out the demo
Replace everything in
src/App.jsxwith this:Then delete
src/App.cssand thesrc/assetsfolder. The new App doesn't use them. -
Add the starter styles
Replace everything in
src/index.csswith the styles below. They give your builds a clean look, so you can spend your time on React. Change them as much as you like. -
Save the API address
Create a new file,
src/api.js:If your instructor gives you a new address later, this is the only line you change.
Stuck?
"npm is not recognized" or "command not found": Node isn't installed, or the terminal was opened before you installed it. Close it and open a new one.
"Port 5173 is in use": Vite picks the next free port, like 5174. Use whichever address the terminal prints.
A blank white page: open DevTools (F12), go to the Console tab and read the first red error. It names the file and the line.
Quick check
npm run dev is running and you save a change to App.jsx. What do you need to do to see it?
Vite watches your files. When you save, it swaps the changed code into the open page. You only restart it if you stop the terminal.
Page skeleton
- Components
- JSX
- import / export
Cut the page into boxes. You'll make a Header and a Footer component and snap them into App. Every later build goes in the space between them. Notes: sections 3 and 4.
😂 Joke Hub
My React playground
Your builds will appear here.
Steps
-
Make a components folder
Inside
src, create a folder calledcomponents. Every component you build goes in here, one file each. -
Build the Header
A component is a function that returns JSX. Its name starts with a capital letter, and
export defaultlets other files import it. In JSX you writeclassNamewhere HTML usesclass. -
Build the Footer
Put your own name in. Anything inside
{ }in JSX is plain JavaScript, so{year}shows the current year. It's worked out once, outside the component, because it doesn't change while the page is open. -
Snap them into App
Once a component is imported, you use it like a custom HTML tag:
<Header />. -
Check the browser
You should see the header at the top and the footer at the bottom, like the finished model. From now on, each build goes inside
<main>.
Stuck?
Does each component file end with
export default ComponentName;?Import paths start from the file you're in. From
App.jsxit's./components/Header, with no.jsxneeded.Blank page? Open the Console (F12). The red error usually names the file and the line.
Show the solution Give it a real try first.
The Header, Footer and App code in the steps above is the full solution. Here's the level-up:
Quick check
Why do we write <Header /> in App and not <header />?
React looks at the first letter. <header> becomes a real HTML header tag; <Header> runs your Header function.
Profile cards
- Props
- Lists and keys
- Components
One card design, many people. Props are the inputs you hand a component, so a single ProfileCard can show Riya, then Arjun, then Meera. Notes: section 5.
Steps
-
Make a card that takes props
{ name, role, city, avatar }pulls each prop out of the props object, so you can writenameinstead ofprops.name. -
Use it three times
Text props go in quotes. Everything else (numbers, variables, template strings) goes in curly braces. Then show it in App:
-
Turn the copies into a list
Three nearly identical blocks is a sign you want an array and
.map(). ReplaceProfileGallery.jsxwith:keyhelps React tell the cards apart when the list changes. Use something unique that doesn't change, like an id. -
Add a fourth person
Copy Kabir Khan from the API into the
peoplearray. One new object gives you one new card, and you didn't write any new JSX. That's the point of props.
Stuck?
Props are written like HTML attributes,
<ProfileCard name="Riya Sharma" />, and arrive in the card as{ name }.A string with a variable inside uses backticks:
`${API_URL}/api/avatars/riya-sharma.svg`..map()has to return the JSX. With=> (…)the return is automatic. With=> {you need to writereturn.
Show the solution Give it a real try first.
ProfileCard.jsx and the list version of ProfileGallery.jsx in the steps are the solution. Here's the card with both level-ups:
Quick check
A ProfileCard wants to change the name it was given when someone clicks it. What's the right move?
Props are like your name on a birth certificate: you use it, you don't rewrite it. Changing data is state, which you'll meet in the next build.
A joke card with a memory
- useState
- Events
- Showing things conditionally
This card remembers two things: whether its punchline is showing, and how many likes it has. That memory is state. When it changes, React redraws the card for you. Notes: sections 6 and 7.
Steps
-
Start with a static card
Make it look right first, with props only and no interaction (step 3 of Thinking in React).
Show two of them in App:
-
Give it a memory
Read it aloud: "I need a piece of state called
showPunchline, starting atfalse, andsetShowPunchlineis how I'll change it." -
Hide the punchline until clicked
a && bshowsbonly whenais true.a ? b : cpicks one of two things. Both are plain JavaScript. -
Count likes
Add a second piece of state, and a second button next to the first inside a
<div className="row">: -
Prove each card has its own memory
Like the first card three times. The second one still says 0. Every JokeCard you place gets its own state.
Stuck?
Import the hook at the top:
import { useState } from "react";Give
onClicka function:onClick={() => setLikes(likes + 1)}. WritingonClick={setLikes(likes + 1)}calls it straight away during drawing, which causes a "Too many re-renders" error.!showPunchlineflipstruetofalseand back.Adding a class only sometimes:
className={liked ? "btn btn-secondary liked" : "btn btn-secondary"}.
Show the solution Give it a real try first.
Quick check
You click ❤️ three times on the first JokeCard. What does the second JokeCard show?
A component is a blueprint. Each <JokeCard /> you place is a separate copy with its own memory.
Random joke machine
- useEffect
- fetch
- Loading state
Your first call to the API. The card loads a random joke as soon as it appears, and a button fetches another. Fetching is a side effect: work that reaches outside the screen. Notes: section 8.
Steps
-
Set up the component
fetchRandomJokesits outside the component because it doesn't need any state.jokestarts asnull: nothing has arrived yet. -
Load a joke when the card appears
The empty list
[]means "run once, after the component first appears on screen". -
Show "Loading", then the joke
The first time the component draws,
jokeis stillnull, so it shows the loading message. When the data arrives,setJoketriggers a redraw with the real joke. -
Make the button fetch another
Add this inside the component, above the
if:Clearing the joke first brings the loading message back while the next one is on its way. Then put
<RandomJoke />in App's<main>. -
Watch it happen
Add
?delay=2000to the address infetchRandomJoketo make the API wait two seconds, so you can see the loading message. Open DevTools → Network, click the button, and click the request to see the JSON. Take the delay out when you're done.
Stuck?
Import both hooks:
import { useEffect, useState } from "react";Cannot read properties of null (reading 'setup') means you read
joke.setupbefore the joke arrived. Return the loading message whilejokeisnull.Failed to fetch usually means a wrong address. Check
API_URLinsrc/api.js: no slash at the end, and the same address as the one at the top of this page.-
For the error level-up:
Show the solution Give it a real try first.
Quick check
Why does the fetch go inside useEffect instead of straight in the component body?
The component function runs on every redraw. An effect with [] runs once, after the first draw, which is exactly when you want to start loading.
Weather by city
- Dependency list
- Controlled inputs
- fetch
Pick a city, get its weather. The effect has to run again every time the city changes, and the dependency list is how you tell React. Notes: section 8, "When does an effect run again?"
Steps
-
Three pieces of state
-
Load the list of cities once
-
Load the weather whenever the city changes
[city]means "run after the first render, and again whenevercityis different from last time". Two effects is fine: each one does one job. -
Add the dropdown
value={city}plusonChangemakes this a controlled input: state decides what the dropdown shows, and every change goes through state. -
Show the weather
Replace the comment with:
Put
<WeatherWidget />in App and switch between cities.
Stuck?
Inside
onChange, the chosen city isevent.target.value.Stuck on "Loading weather…"? Check that the second effect's list is
[city].Empty dropdown? Check the first effect calls
setCities, and that each<option>has akeyand avalue.
Show the solution Give it a real try first.
Quick check
A weather widget should load new weather every time the selected city changes. What goes in its dependency list?
List what the effect reads that can change: here, city. [weather] would loop, because the effect itself sets weather.
Search the jokes
- Lifting state up
- Functions as props
- Calculated values
A search box and a list are siblings, and siblings can't pass props to each other. So the search text lives in their parent and flows down to both. Notes: section 6, "Where should state live?"
Steps
-
Plan who owns what
JokeSearch owns jokes and query ├─ SearchBar gets query, and onQueryChange to ask for changes └─ JokeList gets only the jokes that matchData flows down as props. Requests to change it go up through a function the parent passes down.
-
Build the search bar
SearchBar can't change
queryitself, because it doesn't own it. It callsonQueryChange, and the parent does the changing. -
Build the list
-
Build the parent
visibleJokesis not state. It's worked out fromjokesandqueryon every render, so it can never fall out of sync. -
Try it
Put
<JokeSearch />in App and type "bug", then "state". The count and the list update together on every key press.
Stuck?
You can pass the setter itself as the prop:
onQueryChange={setQuery}."Hello".toLowerCase().includes("ell")istrue. Lower-casing both sides makes the search ignore capitals.An empty search matches everything, because every string includes
"".Filter by category first:
category === "all" || joke.category === category.
Show the solution Give it a real try first.
The three files in the steps are the full solution.
Quick check
SearchBar and JokeList both need the search text. Where should it live?
State lives in the lowest component that every user of it sits under. Two copies kept in sync is exactly the "out of sync" bug from section 1 of the notes.
Campus store
- Arrays in state
- Replace, don't modify
- Calculated values
Add snacks and stationery to a cart and see the total. You'll practise the habit from section 7 of the notes, replace, don't modify, and see why the total is not state.
Steps
-
Load and show the products
It won't work until the next step adds
addToCart. Put<CampusStore />in App. -
Add to the cart, the right way
Add this inside the component, above the
return:[...cart, x]makes a brand-new array with everything fromcartplusx.{ ...product, quantity: 1 }makes a new object: a copy of the product with one extra field. -
Same item twice? Raise the quantity
mapbuilds a new array. The matching item is swapped for a new object withquantity + 1; everything else is kept as it is. The old array is never touched. -
Show the cart and let items be removed
Then add the cart next to the grid, inside
<section className="store">: -
Work out the total
Above the
return:And at the bottom of the cart:
<p className="cart-total">{itemCount} items, ₹{total}</p>.The total is calculated from
carton every render. If it were its own state, you'd have to remember to update it in add, in remove, and in every feature you add later.
Stuck?
[...cart]copies an array and{ ...item }copies an object. Put your change after the copy:{ ...item, quantity: 2 }..some()answers yes or no: is there any item that matches?reducestarts at0(its last argument) and adds each item's amount to the running sum.
Show the solution Give it a real try first.
Quick check
In a shopping cart, should the total price be its own piece of state?
If you can work a value out from other data, it isn't state. Calculating it means it can never disagree with the cart.
Quote of the moment
- Effect cleanup
- Timers
- Dependency list
A quote that changes every ten seconds, with a pause button. A timer keeps going until someone stops it, so this build is about cleaning up after yourself: when you leave a room, switch off the fan. Notes: section 8, "Cleaning up".
Steps
-
Two pieces of state
-
Start a timer, and clean it up
The function you return is the cleanup. React calls it before running the effect again (when
pausedchanges) and when the component leaves the screen. -
Show the quote and a pause button
-
Watch the timer in the Network tab
Put it in App and open DevTools → Network. A request every ten seconds. Pause: they stop. Resume: one straight away, then every ten seconds again.
Stuck?
setIntervalreturns an id.clearInterval(id)stops that one timer.Return the cleanup from the effect itself, not from
loadQuote.if (paused) return;leaves the effect early, so no timer starts while paused.
Show the solution Give it a real try first.
And the clock from the level-up:
Quick check
An effect starts a setInterval but never returns a cleanup. What happens when the effect runs again?
React doesn't know what your effect started. Only your cleanup can stop it.
Capstone: Joke Hub
- Components
- Props
- State
- Lifting state up
- Effects
Put every brick together. This time there's no step-by-step code: you get the requirements, a suggested component tree and some hints. Tick each requirement off as you meet it, then show your Hub to your instructor.
Requirements
-
A joke from the chosen category
A "Get a joke" button loads
/api/jokes/random?category=food(or whichever is selected). With "any", leave the category out. -
Hidden punchline
Show the joke with your
JokeCardfrom Build 3, so the punchline stays hidden until clicked. A new joke starts hidden again. -
Save favourites
A "⭐ Save" button adds the current joke to a favourites list under it. Saving the same joke twice does nothing. Each favourite has a Remove button.
-
Count in the header
The Header shows how many jokes are saved, like "⭐ 3 saved".
-
Favourites survive a refresh
Save a few, reload the page, and they're still there.
Suggested component tree
Stuck?
Where does each piece of state live? Ask who needs it. Header, JokeHub and Favourites all need
favourites, so it lives in App and goes down as props, along with functions to change it.Fetching a joke happens because of a click, so it belongs in the click handler. You only need an effect for loading the categories when the Hub first appears.
Skip duplicates:
if (favourites.some((saved) => saved.id === joke.id)) return;A new joke should start with its punchline hidden. Give the card a key:
<JokeCard key={joke.id} … />. A new key tells React it's a different card, so its state starts fresh.-
Saving to the browser uses
localStorage, which only stores text, henceJSON:The function you pass to
useStateruns only on the very first render, so the saved list is read once.
More to build on your own
- Team directoryFetch every profile and filter by skill with chips. Uses
/api/profiles. - Three-city dashboardGive WeatherWidget a
cityprop and show three side by side. - Show / hide passwordOne true/false state that switches an input between
passwordandtext. - To-do listThe walkthrough from section 9 of the notes, saved with
localStorage. No API needed.
Class API
Every endpoint answers GET requests with JSON, so you can open any of them in the browser. Lists come back as plain arrays; single things come back as objects. Open one and press Send request to see the answer.
When something breaks
First, open DevTools (F12) and read the first red message in the Console. Then find it below.
"Failed to fetch", or a CORS error
The browser couldn't reach the API. Check API_URL in src/api.js: it should match the address at the top of this page exactly, with no slash at the end. If the dot next to it is red, the API itself is down, so tell your instructor.
If the API is on a free host, it may fall asleep when nobody uses it. The first request can then take up to a minute; after that it's quick.
Cannot read properties of null (reading '…')
You're reading from data that hasn't arrived yet. On the first render your state is still null. Return a loading message while it's null, as in Build 4.
Objects are not valid as a React child
You put a whole object in JSX, like {joke}. React can show text and numbers, not objects. Show a field instead: {joke.setup}.
Each child in a list should have a unique "key" prop
Every element you return from .map() needs key={something unique}, usually an id from the data.
Too many re-renders
You're calling a setter while drawing, often onClick={setLikes(likes + 1)}. Wrap it in a function: onClick={() => setLikes(likes + 1)}.
I changed a variable but the screen didn't update
Only state changes made through a setter make React redraw. For arrays and objects, hand the setter a new copy instead of changing the old one (Build 7).
My effect runs over and over
Check the dependency list. No list means "after every render". If the effect sets a piece of state that's also in its list, it triggers itself.
Every request shows up twice in the Network tab
That's React's development mode running effects twice on purpose, to catch missing cleanups. It's normal, and it doesn't happen in a built app.
The page is blank and there's no error
Check that the terminal running npm run dev is still open, that you're on the address it printed, and that App.jsx ends with export default App;.
React Build Book. Your ticks are saved in this browser only, so they won't follow you to another computer.