LCM Logo
Web Development

React

A concept-first tour of React — components, props, state, effects, and hooks — each idea explained before the code.

React is a library for building user interfaces out of components: small, reusable pieces of UI that describe what the screen should look like for a given set of data. You write what you want rendered, and React keeps the actual DOM in sync as your data changes. The examples here use function components and hooks, the modern style, against React 19.

React on its own handles the UI layer. For routing, data fetching, and a production build, it's usually paired with a framework — see the Next.js guide, which builds directly on these concepts.

Components and JSX

A component is a function that returns markup. That markup is JSX — an HTML-like syntax that compiles to JavaScript. A component's name must start with a capital letter so React can tell it apart from a plain HTML tag.

tsx
function Greeting() {
  return <h1>Hello, world</h1>;
}

// Use it like an HTML tag
function App() {
  return <Greeting />;
}

Props: passing data in

Props are the inputs to a component — read-only values passed from parent to child, just like attributes on an HTML element. They let you reuse one component with different data. A component should never modify its own props.

tsx
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}

function App() {
  return <Greeting name="Ada" />; // renders "Hello, Ada"
}

That function signature looks like it says name twice for no reason, but the two halves do completely different jobs:

  • { name } is JavaScript destructuring. React always calls your component with a single object of props — here { name: "Ada" } — and { name } reaches into that object and pulls the name field out into its own variable. It's shorthand for function Greeting(props) { const name = props.name; … }.
  • : { name: string } is a TypeScript type annotation. It isn't code that runs; it tells the type checker "this component expects a prop called name, and it must be a string." If a caller forgets it or passes a number, you get an error before the app ever runs.

In plain JavaScript (no TypeScript) you'd drop the second half entirely and just write function Greeting({ name }) {. So: the first name extracts the value, the second describes its type.

State: data that changes over time

State is data a component owns and can change in response to interaction. Calling the useState hook gives you the current value and a setter; calling the setter tells React to re-render the component with the new value. Never reassign the variable directly — always use the setter.

tsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

The line const [count, setCount] = useState(0) packs a lot in, so read it both ways:

  • Right side: useState(0) sets the starting value to 0 and hands back a two-item array — the current value and a function that updates it.
  • Left side: const [count, setCount] = … is array destructuring: it unpacks that two-item array into two named variables, in order. The names are your choice; the convention is something and setSomething. So count is the current value and setCount is how you change it.

It's exactly equivalent to the longer form:

tsx
const state = useState(0);
const count = state[0];     // the current value
const setCount = state[1];  // the updater function

Calling setCount(count + 1) is what tells React "the value changed — re-render with the new number." Assigning count = count + 1 directly would do nothing, because React wouldn't know anything happened.

Handling events

Event handlers are functions you pass to JSX props like onClick or onChange. They receive a React event object and typically update state, which triggers a re-render.

tsx
function Toggle() {
  const [on, setOn] = useState(false);
  return <button onClick={() => setOn((prev) => !prev)}>{on ? "ON" : "OFF"}</button>;
}

The setOn((prev) => !prev) part deserves a beginner's pause. Instead of handing the setter a new value, you can hand it a function that receives the latest state — here named prev — and returns the next one. !prev is JavaScript's "not" operator flipping the boolean, so true becomes false and back again. You could write setOn(!on) and it would usually work, but the function form is the safe habit: it always builds on the current value, even when several updates happen in quick succession.

Rendering lists

To render a collection, map an array to an array of elements. Give each element a stable, unique key so React can track items efficiently across re-renders — use a real id, not the array index where you can.

tsx
function PenguinList({ penguins }: { penguins: { id: number; species: string }[] }) {
  return (
    <ul>
      {penguins.map((p) => (
        <li key={p.id}>{p.species}</li>
      ))}
    </ul>
  );
}

Conditional rendering

Because JSX is just JavaScript, you render different UI with ordinary expressions — a ternary for either/or, or && to render something only when a condition holds.

tsx
function Status({ loading, count }: { loading: boolean; count: number }) {
  return (
    <div>
      {loading ? <p>Loading…</p> : <p>{count} results</p>}
      {count === 0 && <p>No matches.</p>}
    </div>
  );
}

Side effects with useEffect

Rendering should be pure — it just computes UI from props and state. Anything that reaches outside React (fetching data, subscriptions, timers, manual DOM work) is a side effect and belongs in useEffect. The dependency array controls when it re-runs; return a cleanup function to undo the effect.

tsx
import { useEffect, useState } from "react";

function Clock() {
  const [now, setNow] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id); // cleanup on unmount
  }, []); // empty deps → run once after first render

  return <time>{now.toLocaleTimeString()}</time>;
}

Custom hooks: reusing logic

When several components need the same stateful logic, extract it into a custom hook — a function whose name starts with use that calls other hooks. It bundles behavior, not markup, so each component gets its own independent state.

tsx
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn((v) => !v);
  return [on, toggle] as const;
}

function Panel() {
  const [open, toggle] = useToggle();
  return <button onClick={toggle}>{open ? "Hide" : "Show"}</button>;
}

Two small pieces of that hook are easy to trip over:

  • initial = false is a default parameter. If a caller writes useToggle() with no argument, initial is false; useToggle(true) starts it switched on.
  • return [on, toggle] as const returns the value and its toggler as a pair — the same [value, updater] shape useState gives you, which is why Panel can destructure it with const [open, toggle] = useToggle(). The as const is a TypeScript hint. Without it, TypeScript would describe the returned array loosely as "a list of (boolean or function)" and lose track of which slot is which; as const locks in the order and types so open is known to be a boolean and toggle a function. In plain JavaScript you'd simply write return [on, toggle];.

Controlled forms

In a controlled input, React state is the single source of truth: the input's value comes from state, and onChange writes back to it. This keeps the UI and your data perfectly in sync and makes validation straightforward.

tsx
function NameField() {
  const [name, setName] = useState("");
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name || "stranger"}</p>
    </>
  );
}

The rules of hooks

Only call hooks (useState, useEffect, …) at the top level of a component or another hook — never inside loops, conditions, or nested functions. React relies on hooks being called in the same order every render. This is the one rule that trips up most newcomers.

On this page