Lesson 3 · Frontend for backend developers
Not a class, not a template, not a widget. A function from data to a description of UI.
Everything in this lesson follows from one equation, which is worth memorising because it makes React's rules stop feeling arbitrary:
UI = f(state)
A component is f. You give it data, it returns a description of
what should be on screen. React calls it and reconciles the result with the real
DOM.
src/components/Badge.tsx
type BadgeProps = {
label: string
tone: 'ok' | 'warn'
}
export function Badge({ label, tone }: BadgeProps) {
const toneClasses =
tone === 'ok'
? 'bg-green-100 text-green-800'
: 'bg-amber-100 text-amber-800'
return (
<span className={`rounded px-2 py-1 text-xs font-medium ${toneClasses}`}>
{label}
</span>
)
}
Read it as Python and almost nothing is surprising: a function taking keyword arguments, destructured, returning a value. Four things are worth naming.
Not style — mechanics. In JSX, <badge /> compiles to the
string 'badge' (a real HTML tag), while <Badge />
compiles to a reference to your function. A lowercase component silently becomes
an unknown HTML element that renders nothing.
className, not classclass is a reserved word in JavaScript. JSX is JavaScript, so the
DOM property name is used instead. Same for htmlFor instead of
for.
Inside JSX you are writing markup; inside { } you are writing an
expression again. An expression, note — {if (x) ...} is a
syntax error, which is why React code uses ternaries and &&
so heavily.
This is where TypeScript earns its place. tone: 'ok' | 'warn' is a
union of literal values — pass "warning" and the build fails before
the page ever loads. It is the same guarantee an Enum or a Pydantic
model gives you, applied to UI.
class BadgeProps(BaseModel):
label: str
tone: Literal['ok', 'warn']
def badge(props: BadgeProps) -> Html: ...
That is genuinely all a component is.
src/App.tsx
import { Badge } from './components/Badge'
export function App() {
return (
<div className="p-8 flex gap-2">
<Badge label="Paid" tone="ok" />
<Badge label="Overdue" tone="warn" />
</div>
)
}
Attributes in, arguments out. label="Paid" arrives as the
label property of the object your function receives. Non-string
values need braces: count={3}, items={rows},
onSave={handleSave}.
Why does <badge label="Paid" /> render nothing?
useState lets a component hold a value that survives between
calls, and tells React to call the function again when it changes.
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button
onClick={() => setCount(count + 1)}
className="rounded bg-slate-900 px-3 py-1.5 text-white"
>
Clicked {count} times
</button>
)
}
The destructuring is a fixed idiom: [currentValue, setterFunction].
The argument to useState is the initial value, used on the first
call only.
Never assign to state directly. count = count + 1
changes a local variable and nothing else — React has no way to know it
happened, so nothing re-renders. Only the setter tells React the state is
stale. Same for objects and arrays: build a new one rather than mutating the
existing one, because React compares by identity, and a mutated array is still
the same array.
Predict: what does this print, and what appears on screen after one click?
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
console.log(count)
}
It logs 0, and the screen shows 1.
count is a const inside this call of the function.
setCount does not reassign it — it schedules a re-render, and the
next call to the function gets 1 from React. The old call's
variable never changes.
This is the single most common source of confusion in early React, and it
is a consequence of UI = f(state): each render is a separate
invocation, with its own frozen snapshot of the arguments.
Build a <TaskRow> component in
src/components/TaskRow.tsx:
title: string, priority: 'low' | 'high'.<Badge> from above showing
the priority — tone warn for high, ok for low.done state, toggled by a checkbox. When done,
the title gets line-through text-slate-400.App.tsx.Two things this is quietly teaching: a component using another component, and a component owning state that nothing above it knows about. Lesson 4 is what happens when something above it needs to know.
console.log prediction above
surprised you, stop and dig into it with him before moving on. Everything in
Lessons 4 and 5 rests on understanding that each render is a separate call.