Lesson 3 · Frontend for backend developers

A component is a function

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.

The smallest real one

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.

1. The name must be capitalised

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.

2. className, not class

class is a reserved word in JavaScript. JSX is JavaScript, so the DOM property name is used instead. Same for htmlFor instead of for.

3. Braces mean "back to JavaScript"

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.

4. The props type is the component's signature

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.

In your language
class BadgeProps(BaseModel):
    label: str
    tone: Literal['ok', 'warn']

def badge(props: BadgeProps) -> Html: ...

That is genuinely all a component is.

Using it

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?

State: the argument the component gives itself

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.

The rule that catches everyone

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.

Exercise

Build a <TaskRow> component in src/components/TaskRow.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.

Ask Martin. If the 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.