Library
00/07 · ~36 min
GUIDEDECK · building UIs that stay simple as they grow

Modern
React, from the
mental model up.

A 36-minute working session on how React actually thinks — components and one-way data flow, the hooks you reach for daily, the effects you usually don't need, and where your code runs now that the server is back in the picture.

~36 MINBEGINNER → INTERMEDIATEREACT 19 ERA
SCROLL
01 · The mental model 4 min

Your UI is a function of state.

React's big idea is small: describe what the screen should look like for the current data, and let React work out the DOM changes. You never reach into the page and edit nodes by hand — you change state and re-describe. Three concepts carry the whole model: components, JSX, and one-way data flow.

Component a function that takes inputs (props) and returns a description of UI (JSX). The same inputs always produce the same output, so you can read a screen as a tree of small, predictable functions instead of one big pile of DOM-poking code.

UI = f(state)

  • You don't tell React how to update the DOM (imperative); you declare what it should be for the current state (declarative).
  • Change the state, and React re-runs your component, compares the new description to the old, and patches only what differs.
  • That single rule — render is a pure function of state — is what makes a React app possible to reason about.
state the data render() pure function UI what you see event → setState → re-render

State goes in, a description of the UI comes out. An event sets new state and the loop runs again.

// a component is just a function returning JSX function Greeting({ name }) { return <h1>Hello, {name}</h1> } // JSX is sugar for function calls — this compiles to: React.createElement("h1", null, "Hello, ", name) // so {name} is plain JavaScript, not a template string
App Cart Total props ▾ events ▴ via callbacks

One-way flow: data goes down as props; events come up as callbacks. No child reaches sideways into a sibling.

JSX

Markup that is JavaScript

It looks like HTML but it's an expression: {} drops any JS value in, className replaces class, and every tag must close.

Props

Inputs, read-only

A parent passes data down. A component nevermutates its props — they're the arguments to its render function.

Composition

Trees, not templates

Build screens by nesting small components. The children prop lets a component wrap whatever you put inside it.

02 · State, props & the rules of hooks 6 min

Props come from above;
state is the memory a component owns.

If a value is handed in by a parent, it's a prop. If a component needs to remember something across renders — a count, a form field, whether a menu is open — that's state, and you reach for a hook. Hooks are the functions starting with usethat let a plain function component hold state and tap into React's features.

State data a component remembers between renders, which triggers a re-render when it changes. You never assign to it directly; you call its setter, and React schedules a fresh render with the new value. State is a snapshot: within one render the value is fixed.
import { useState } from "react" function Counter() { const [count, setCount] = useState(0) // [value, setter] return ( <button onClick={() => setCount(count + 1)}> Clicked {count} times </button> ) }
onClick setCount schedule re- render never mutate count directly

The setter is the only door: it tells React the value changed and a re-render is due.

useState vs useReducer

  • useState — perfect for independent values: a toggle, an input, a count. Reach for it first.
  • useReducer — when several values change together by clear rules, move the logic into one reducer(state, action) function. The component just dispatches actions.
  • A reducer makes complex transitions testable in isolation and keeps the update logic out of your event handlers.
function reducer(state, action) { switch (action.type) { case "add": return { ...state, items: state.items + 1 } case "reset": return { items: 0 } } } const [state, dispatch] = useReducer(reducer, { items: 0 }) dispatch({ type: "add" }) // describe what happened
The Rules of Hooks call hooks only at the top level of a component (or another hook), and only from React functions. Never call a hook inside a condition, loop, or nested function. React tracks state by call order, so that order must be identical on every render.
Hook inside a condition
function Profile({ user }) { if (user) { const [name, setName] = useState(user.name) // ✕ } // call order changes when user is null → // React loses track of which state is which }
Top level, always
function Profile({ user }) { const [name, setName] = useState(user?.name ?? "") // same hooks, same order, every render ✓ if (!user) return <Empty/> return <Form value={name} onChange={setName}/> }

Like numbered lockers — React hands out state by the order you ask for it, so you must ask in the same order every time. Typing props and hook return values is its own skill; see the TypeScript deck.

03 · Effects — and when not to use one 6 min

An effect synchronizes with
the world outside React.

useEffectis not "run some code after render" — it's a way to keep React in sync with an external system: a network connection, a browser API, a non-React widget, a subscription. If the work is purely about turning props and state into JSX, it belongs in render, not in an effect.

Effect code that synchronizes your component with an external system, run after render and cleaned up before the next sync or on unmount. It takes a dependency array: React re-runs the effect only when one of those values changes.
Component useEffect external system socket · timer · API setup — connect cleanup — disconnect re-syncs only when a dependency changes

Setup connects to the outside system; the returned cleanup disconnects. Every effect that subscribes must unsubscribe.

useEffect(() => { const conn = connect(roomId) // setup conn.on("msg", addMessage) return () => conn.close() // cleanup }, [roomId]) // re-sync when roomId changes // no deps array → runs every render (rarely what you want) // [] → run once on mount, clean up on unmount

The dependency array is a promise to React: "these are the only values this effect depends on." Keep it honest.

You probably don't need an effect

Effect to derive state
const [items, setItems] = useState([]) const [total, setTotal] = useState(0) useEffect(() => { setTotal(items.reduce(sum, 0)) // extra render + drift risk }, [items]) // total can briefly disagree with items
Just compute it in render
const [items, setItems] = useState([]) const total = items.reduce(sum, 0) // derived, always correct // no second state, no effect, no extra render // if it's ever slow, wrap it in useMemo — not an effect
Skip the effect for…
  • Data you can derive from props/state while rendering.
  • Responding to a user action — do it in the event handler.
  • Resetting state on a prop change — pass a key instead.
An effect earns its place for…
  • Subscriptions and connections (sockets, event listeners).
  • Driving non-React widgets (a map, a chart library).
  • Browser APIs — title, focus, localStorage.
Watch for

A missing cleanup leaks listeners; a dishonest dependency array causes stale closures. In dev, Strict Mode runs effects twice on mount on purpose — to surface a missing cleanup.

04 · Context & composition 5 min

Share state without
threading it through every prop.

When a value is needed deep in the tree — the current theme, the logged-in user, a locale — passing it down through every intermediate component is prop drilling: tedious, and every middle component is now coupled to data it doesn't use. Two tools fix this: better composition, and Context.

Context a way to publish a value from a provider and read it from any descendant, skipping the props in between. It's for low-frequency, widely-needed values — theme, auth, locale — not as a general-purpose store for fast-changing state.
prop drilling App Page Toolbar Avatar context Provider Avatar useContext

Left: the value is handed down link by link. Right: the Provider publishes once and the deep child reads it directly.

const ThemeCtx = createContext("light") // publish near the top <ThemeCtx.Provider value={theme}> <App/> </ThemeCtx.Provider> // read anywhere below — no props in between const theme = useContext(ThemeCtx)

Try composition first

Much "drilling" disappears if you pass JSX as childreninstead of passing data through. A layout component doesn't need to know what it wraps.

// Layout never touches user — it just renders children <Layout> <Toolbar><Avatar user={user}/></Toolbar> </Layout>

When to use which

  • Composition — the value is needed one or two levels down, or by a specific subtree. Cheapest, no coupling.
  • Context — the value is genuinely global and stable (theme, auth, locale).
  • A real store— for fast-changing or server-derived state, Context re-renders too broadly. That's a job for State Management.

Like a building's PA system — the front desk announces once and every floor hears it, instead of passing a note desk to desk. Use it for announcements, not for chatter.

05 · Performance — re-renders & memo 6 min

Re-renders are cheap.
Prematurememoization isn't.

React re-renders a component when its state changes or its parent re-renders. That's usually fast and fine — re-rendering computes JSX, it doesn't touch the DOM unless something actually changed. Most performance work is about two things: stable list keys, and not re-doing expensive work needlessly. Reach for memoonly after you've measured.

Re-render React calling your component function again to get a fresh description of the UI. When a component re-renders, so do its children by default. This is normal; it only becomes a problem when a render does heavy work or a big subtree updates on every keystroke.
Parent ● List ● memo(Chart) ✓ Row ● skipped ● = re-rendered · memo halts the cascade

A parent re-render flows to every child. memo lets a child skip re-rendering when its props are unchanged.

The three memo tools

  • memo(Component) — skip a re-render when props are shallow- equal to last time.
  • useMemo(fn, deps) — cache the result of an expensive calculation between renders.
  • useCallback(fn, deps) — cache a function so a memo'd child doesn't see a "new" prop every render.

All three trade memory and complexity for fewer renders. Used everywhere by reflex, they make code harder to read and slower to write — for no measured gain.

The React Compiler an official build-time tool that auto-memoizes components and values for you. Where it's adopted, most manual useMemo / useCallbacksimply isn't needed — you write plain code and the compiler inserts the caching. Treat manual memoization as the exception, not the habit.

Keys: the one you must get right

Array index as key
{todos.map((todo, i) => ( <Todo key={i} item={todo}/> // ✕ index ))} // reorder or delete a row and React reuses the // wrong DOM node — checkbox state jumps rows
Stable, unique id
{todos.map((todo) => ( <Todo key={todo.id} item={todo}/> // ✓ identity ))} // key tells React which item is which across // renders, so it moves nodes instead of rebuilding

A keyis React's identity tag for a list item. Use a stable id from your data — never the array index for a list that can reorder, insert, or delete.

06 · Server Components & Suspense 6 min

What runs on the server,
what ships to the browser.

Modern React splits components by where they run. A Server Component runs only on the server, can fetch data directly, and sends zero JavaScript to the browser. A Client Component is the interactive part — state, effects, event handlers — and is the only kind that hydrates on the device.

React Server Component (RSC) a component rendered on the server whose output is streamed as data, shipping no JS for itself to the client. It can be async and awaitdata inline, but it can't use state, effects, or browser APIs. Mark the interactive boundary with "use client".
// Server Component — runs on the server, async OK async function Page() { const posts = await db.posts() // no API route needed return <Feed posts={posts}/> } // Likes.tsx — opt into the client for interactivity "use client" function Likes() { const [n, set] = useState(0) }
SERVER Page · Feed fetch · no client JS BROWSER Likes · "use client" state · effects · hydrate stream

Server renders and streams; only the "use client" islands ship JS and hydrate.

Suspense — declarative loading

  • Wrap a slow part in <Suspense fallback=...> and React shows the fallback until the content is ready — then streams it in.
  • No isLoading flags threaded through your tree; the boundary owns the loading state.
  • The use() hook lets a component read a promise (or context) and suspend until it resolves.
<Suspense fallback={<Skeleton/>}> <SlowFeed/> // streams in when its data is ready </Suspense> // inside a component, read a promise directly: const user = use(userPromise) // suspends until resolved

Where the HTML is ultimately built — server, client, or statically — is the subject of Rendering Strategies.

The React landscape

Server Components need a framework to render and route them. Pick on how much server you actually want.

Next.js

Pro — most complete RSC + App Router story; huge ecosystem and deploy options.

Con — opinionated and large; the caching model has a learning curve.

Choose when you want server rendering and data fetching handled for you, end to end.

React Router v7

Pro — the Remix lineage; explicit loaders/actions, works as a framework or a plain router.

Con — smaller RSC surface; fewer batteries than Next.

Choose when you want web-standards data flow and room to start as an SPA and grow.

Vite SPA

Pro — simplest model: a pure client-rendered app, fast dev server, no server runtime.

Con — no Server Components; you own SEO, data fetching, and code-splitting.

Choosefor internal tools and dashboards behind a login, where SEO and first-paint don't rule.

The honest default: an internal app is happiest as a Vite SPA; a public, content-heavy productwants a server framework. Don't adopt RSC for an app that never needed a server.

React ships the primitives — elements, state, effects. It has no built-in buttons, modals, or design system. That gap is where component libraries live.

Radix / React Aria

Pro — unstyled, accessible behavior primitives (menus, dialogs) you style yourself.

Con — you bring all the visuals; more upfront work.

Choose when you need a bespoke design system but not to reinvent accessibility.

shadcn/ui

Pro — copy styled components (built on Radix) into your repo; you own and edit the code.

Con— it's a starting point, not a versioned dependency; you maintain it.

Choose when you want a head start and full control of the source.

MUI / Mantine

Pro — batteries-included design system; ship a polished app fast.

Con— heavier bundle; harder to escape the library's look.

Choose for internal tools or when speed beats a custom brand.

Primitives end where design opinions begin: a primitive gives you correct, accessible behavior; a UI library adds the visual system on top. State tooling is a separate axis — see State Management.

07 · Data fetching patterns + recap 3 min

Fetch where it's cheapest;
then walk out with five rules.

Where you fetch follows where you render. On the server, a component just awaits its data. On the client, you don't roll your own — you reach for a server-state library that handles caching, refetching, and staleness for you.

Server-first

Fetch in a Server Component and pass data down. No loading flags, no client waterfall — the data arrives with the HTML.

Client server-state

For data fetched in the browser, use TanStack Query or SWR: caching, dedupe, and background refetch. Don't store server data in useState.

Mutations

Actions and useActionState handle form submissions and pending/optimistic UI without a hand-rolled fetch-and-setState dance.

The split between server state (data you cache from a backend) and client state (UI-only) is the heart of the State Managementdeck — most "React is hard" pain is really server state in disguise.

1UI is a function of state. Change state, re-describe the screen — never mutate the DOM by hand.
2Props down, events up. One-way data flow, and hooks always at the top level in the same order.
3Effects sync with the outside world.If you can derive it in render or handle it in an event, you don't need one.
4Measure before you memoize. Stable keys matter always; memo matters rarely — and the compiler does most of it.
5Know where code runs. Server Components for data and zero-JS; "use client" islands for interactivity.
  • Public, content or SEO-driven product? Next.js (or React Router v7) — server rendering pays off.
  • Internal tool or dashboard behind a login? → a Vite SPA; don't take on a server you don't need.
  • Fetching on the client? → TanStack Query / SWR, not raw effects.
  • Tempted to add memo everywhere? → enable the React Compiler and measure instead.
Knowledge check

Did it stick?

Five quick questions on the mental model, hooks, effects, performance, and Server Components — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library