React: Building UIs
Components, state, and the art of reactive interfaces
Imagine you have a webpage. A user clicks a button, and one small number needs to change -- say, a like count going from 11 to 12. In the old days, you would have to manually find that exact element in the page, grab it, and update its text. And if that number also appeared in a sidebar? Find that element too. And the page title? Find that one as well. Now imagine a page with hundreds of these interconnected pieces. It becomes a nightmare. What if the page could just... figure it out? What if you could say 'the like count is now 12' and every place that displays it updates automatically? That is the problem React solves. Instead of building a web page as one giant monolithic file and manually managing every update, React lets you snap your UI together from small, independent, reusable pieces -- like LEGO blocks. Each block is self-contained, knows how to display itself, and knows how to respond when its data changes. You can rearrange blocks, swap them out, or reuse the same block in dozens of places. This simple idea -- components as building blocks -- has fundamentally changed how the world builds user interfaces.
Components Are Just Functions
Here is the twist that changed everything about building UIs: what if your interface was just a function?
Not a class. Not a configuration file. Not an XML template in some separate folder. A plain JavaScript function. You give it some data, and it returns what should appear on screen. That is a React component. No magic, no special ceremony.
Before reading on, think about this: what if building a web page worked the same way as calling a function? You pass in some inputs, and you get back a result. If the inputs change, you call the function again and get a new result. That is the entire mental model of React.
Older tools made this harder than it needed to be. jQuery required you to manually hunt through the DOM, grab elements by their IDs, and surgically update them. Class-based frameworks had complex lifecycle methods and inheritance chains that were hard to follow. React threw all of that away and said: "A UI is just a function of your data." You give it inputs (called props), it returns outputs (the UI). When the inputs change, the function runs again and the UI updates. Done.
Here is where the LEGO analogy kicks in. Just like you can snap LEGO blocks together to build something bigger, components can render other components. A `Page` component might render a `Header`, a `Sidebar`, and a `MainContent`. The `MainContent` might render a list of `PostCard` components. Each block is independent. Each block is testable. Each block is reusable. You can pull one out and snap in a different one without the whole structure collapsing. This is how teams at every scale -- from solo developers to thousand-person engineering orgs -- build modern web applications.
React Component Tree
Components nest in a tree structure. Props flow down, state lives in components.
// A component is just a function that returns JSX
function WelcomeBanner({ username }: { username: string }) {
return (
<div className="rounded-lg bg-blue-50 p-6">
<h1 className="text-2xl font-bold">
Welcome back, {username}!
</h1>
<p className="mt-2 text-gray-600">
Ready to continue learning?
</p>
</div>
)
}
// Components compose together like building blocks
function DashboardPage() {
return (
<main className="mx-auto max-w-4xl p-8">
<WelcomeBanner username="Alice" />
<div className="mt-8 grid grid-cols-2 gap-4">
<CourseCard title="React Basics" progress={75} />
<CourseCard title="Node.js" progress={30} />
</div>
</main>
)
}JSX: HTML That Lives Inside JavaScript
The first time you see HTML sitting inside a JavaScript file, your brain says "that is wrong." It looks like someone accidentally pasted markup into their code. But it is not wrong. It is JSX, and it is one of React's most powerful ideas.
JSX stands for JavaScript XML. It lets you describe your UI structure right inside your JavaScript code -- and the key insight is that this is actually where it belongs. Think about it: the code that decides WHAT to show and the code that decides HOW it looks are almost always tightly connected. If you split them into separate files (an HTML template over here, JavaScript logic over there), you end up bouncing back and forth constantly. JSX says: keep related things together.
Here is a question: what if you need to show a list of 100 users, and each user's card looks slightly different based on their role? With plain HTML, you would need some kind of templating language with special syntax. With JSX, you just use JavaScript. Map over an array. Write an if statement. Call a function. Every JavaScript expression you already know works inside JSX -- you just wrap it in curly braces `{}`.
There are a few small differences from regular HTML you will bump into. You write `className` instead of `class` (because `class` is a reserved word in JavaScript). Event handlers use camelCase (`onClick` instead of `onclick`). Style attributes take objects instead of strings. These feel weird for a day or two, then they become second nature.
Remember in Chapter 2 when we explored how JavaScript handles data with objects and arrays? All of that comes alive here. JSX is where your JavaScript skills become visual.
function UserList({ users }: { users: User[] }) {
// You can use variables and logic directly in JSX
const isLoaded = users.length > 0
return (
<section className="space-y-4">
<h2>Team Members ({users.length})</h2>
{/* Conditional rendering */}
{!isLoaded && <p>Loading users...</p>}
{/* Mapping over arrays to render lists */}
{users.map((user) => (
<div key={user.id} className="flex items-center gap-3">
<img
src={user.avatar}
alt={user.name}
className="h-10 w-10 rounded-full"
/>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.role}</p>
</div>
</div>
))}
{/* Inline styles use objects, not strings */}
<div style={{ marginTop: 16, color: "#666" }}>
Total: {users.length} members
</div>
</section>
)
}Props: Passing Data Down
You just built a beautiful card component. It looks perfect. Now your designer says: "Great, we need 50 of those on the page, each showing different data." Are you going to copy and paste the component 50 times and hardcode different text into each one? Of course not. That is what props are for.
Props (short for "properties") are how you pass data from a parent component to a child component. Think of them exactly like function arguments -- because that is literally what they are. When you write `<UserCard name="Alice" role="admin" />`, React calls the UserCard function with `{ name: "Alice", role: "admin" }` as its argument. Same card, different data. One LEGO block design, used in fifty different places.
Here is something crucial: props flow in one direction only. Parent to child. Down the tree. Never sideways, never upward. This one-way data flow might feel restrictive at first, but it is secretly a superpower. When something looks wrong on screen, you can trace the data back up through the component tree like following a river upstream. You will always find the source.
Before reading on, think about this: if a child component cannot modify its parent's data, how does a delete button in a list item tell the parent "hey, remove me"? The answer is elegant. The parent passes a callback function down as a prop. The child does not delete anything itself -- it just calls `onDelete(itemId)`, like pressing a buzzer. The parent hears the buzz, handles the actual deletion, and re-renders with the item gone. Each component stays focused on its own job. Clean, predictable, traceable.
// Define the shape of props with TypeScript
interface CourseCardProps {
title: string
progress: number
description?: string // optional prop
onStart: () => void // callback function prop
}
function CourseCard({
title,
progress,
description,
onStart,
}: CourseCardProps) {
return (
<div className="rounded-lg border p-4 shadow-sm">
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="mt-1 text-gray-600">{description}</p>
)}
<div className="mt-3">
<div className="h-2 rounded-full bg-gray-200">
<div
className="h-2 rounded-full bg-blue-500"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-sm text-gray-500">
{progress}% complete
</span>
</div>
<button
onClick={onStart}
className="mt-3 rounded bg-blue-500 px-4 py-2 text-white"
>
{progress > 0 ? "Continue" : "Start"}
</button>
</div>
)
}
// Parent passes data down as props
function CoursesPage() {
return (
<CourseCard
title="React Fundamentals"
progress={65}
description="Learn the core concepts"
onStart={() => console.log("Starting course!")}
/>
)
}State: Data That Changes Over Time
Props come from the outside -- a parent hands them down. But what about data that belongs to THIS component? Data that changes because of something the user did right here?
A user types into a search box. A toggle flips from off to on. Someone adds an item to a cart. These changes do not come from a parent. They originate inside the component itself. The component needs to remember what happened and update what is on screen. This is state, and it is the beating heart of every interactive React application.
The `useState` hook is how you create state. It returns two things: the current value, and a function to update it. When you call that update function, something remarkable happens. React re-runs your component function with the new value, and the UI updates to match. You do not manually find elements and change their text. You just say "the count is now 12" and React figures out what needs to change on screen. That is the magic of reactivity.
Here is where beginners get tripped up, and it is worth understanding why. You should never modify state directly. Do not push items into a state array. Do not change properties on a state object. Why? Because React is watching for changes by comparing references -- it asks "is this the same object as before?" If you mutate an existing array by pushing into it, the reference stays the same. React looks at it and thinks "same array, nothing changed" and skips the re-render. Your data changed, but your screen did not. It is one of the most common bugs in React.
The fix is simple: always create a new value. Use the spread operator to make a new array. Use filter or map to produce a new array. Pass that new value to the setter. React sees a different reference, knows something changed, and re-renders. This pattern -- immutable updates -- will feel like an extra step at first. Soon it will be automatic.
import { useState } from "react"
function TodoList() {
// useState returns [currentValue, setterFunction]
const [todos, setTodos] = useState<string[]>([])
const [inputValue, setInputValue] = useState("")
const addTodo = () => {
if (inputValue.trim()) {
// Create a NEW array (don't mutate the existing one)
setTodos([...todos, inputValue.trim()])
setInputValue("") // Clear the input
}
}
const removeTodo = (index: number) => {
// Filter creates a new array without the removed item
setTodos(todos.filter((_, i) => i !== index))
}
return (
<div className="mx-auto max-w-md p-4">
<h1 className="text-xl font-bold">My Todos</h1>
<div className="mt-4 flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTodo()}
placeholder="Add a todo..."
className="flex-1 rounded border px-3 py-2"
/>
<button
onClick={addTodo}
className="rounded bg-blue-500 px-4 py-2 text-white"
>
Add
</button>
</div>
<ul className="mt-4 space-y-2">
{todos.map((todo, index) => (
<li key={index} className="flex justify-between rounded border p-2">
<span>{todo}</span>
<button
onClick={() => removeTodo(index)}
className="text-red-500"
>
Delete
</button>
</li>
))}
</ul>
<p className="mt-2 text-sm text-gray-500">
{todos.length} todo{todos.length !== 1 ? "s" : ""} remaining
</p>
</div>
)
}Effects: Syncing with the Outside World
Your component lives in its own little world of props and state. But at some point, it needs to reach outside. Fetch data from a server. Listen for window resize events. Update the browser tab title. Start a timer. These are all conversations with the outside world, and that is where things get interesting.
The `useEffect` hook is React's way of saying "after you render, go do this thing." It takes two arguments: a function to run (the effect), and an array of dependencies that controls WHEN it runs.
That dependency array is the key to everything. Pass an empty array `[]`, and the effect runs once -- when the component first appears on screen. Include variables in the array, and the effect re-runs whenever any of those variables change. Omit the array entirely, and the effect fires after every single render, which is almost never what you want. Think of the dependency array as you telling React: "Only re-run this effect if one of these specific values has changed since last time."
Here is a pattern you will use constantly: fetching data when a component loads. You call `useEffect` with a function that hits an API and saves the result to state, with an empty dependency array so it only runs once. In Chapter 4, you will learn all about those APIs and how the data gets from server to browser. The `useEffect` hook is the React side of that conversation -- the moment your component says "I need something from out there."
One more critical detail. Effects can return a cleanup function. React calls this cleanup when the component disappears from the screen, or right before re-running the effect. This is how you prevent memory leaks -- if you set up an event listener, the cleanup removes it. If you start a timer, the cleanup clears it. If you open a WebSocket, the cleanup closes it. Always clean up after yourself. Future you will be grateful.
import { useState, useEffect } from "react"
interface Post {
id: number
title: string
body: string
}
function PostList() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Effect: fetch data when the component mounts
useEffect(() => {
async function loadPosts() {
try {
const response = await fetch("/api/posts")
if (!response.ok) throw new Error("Failed to fetch")
const data = await response.json()
setPosts(data)
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error")
} finally {
setLoading(false)
}
}
loadPosts()
}, []) // Empty array = run once on mount
// Effect: update document title when posts change
useEffect(() => {
document.title = `Posts (${posts.length})`
}, [posts.length]) // Re-run when posts.length changes
// Effect with cleanup (e.g., for event listeners)
useEffect(() => {
const handleResize = () => {
console.log("Window resized:", window.innerWidth)
}
window.addEventListener("resize", handleResize)
// Cleanup function: runs when component unmounts
return () => {
window.removeEventListener("resize", handleResize)
}
}, [])
if (loading) return <div>Loading posts...</div>
if (error) return <div>Error: {error}</div>
return (
<div className="space-y-4">
{posts.map((post) => (
<article key={post.id} className="rounded border p-4">
<h2 className="font-bold">{post.title}</h2>
<p className="mt-2 text-gray-600">{post.body}</p>
</article>
))}
</div>
)
}- A React component is a function that takes data in (props) and returns UI out (JSX). When the data changes, React re-runs the function and updates only what changed on screen.
- Props are one-way: parent to child, always. When a child needs to talk back to its parent, the parent passes down a callback function as a prop.
- State is data that belongs to a component and changes over time. Always update state immutably -- create new arrays and objects instead of modifying existing ones, or React will not know anything changed.
- useEffect is how your component talks to the outside world: fetching data, setting up listeners, updating the document. The dependency array controls when the effect runs, and the cleanup function prevents memory leaks.
- The component tree model -- snapping small, focused components together like LEGO blocks to build larger interfaces -- is the foundation of every modern React application.
You push a new item into a state array with `state.push(newItem)`, but the screen does not update. What went wrong?