SSiddiq.in
A senior React engineer's notebook

Most React apps aren't slow. They're just re-rendering everything, all the time.

We've spent six years shipping React into production — dashboards used by 40 support agents at once, editors saving every 2 seconds, tables scrolling 50k rows without dropping a frame. This page is what we've learned to keep that from turning into a maintenance disaster.

The core mental model

The four state territories.

Every state bug we've ever debugged is because someone put a piece of state in the wrong territory. Get this right and your whole app calms down.

01

Server state

The rows in Postgres. Owned by the server. Cached and revalidated by TanStack Query. Never — never — copied into useState.

e.g. user list, orders, invoices
02

URL state

Filters, tabs, pagination, the item you have open. Lives in the URL so refresh, back, and share links all keep working.

e.g. ?tab=billing&sort=desc
03

Form state

Draft values before submit. Local to the form. React Hook Form + Zod resolver. Never global.

e.g. the invoice you're editing
04

UI-local state

Is this dropdown open, which row is hovered, is the sidebar collapsed. useState in the component that owns it. That's it.

e.g. modals, tooltips, hover
Re-render economics

Why your React app feels heavy on a mid-range laptop.

The context tax

A single value in a giant context re-renders every consumer on every change. Split contexts by write frequency, or use a store (Zustand) and select what you need.

The inline-prop tax

`<Row onClick={() => …}/>` creates a new function every render. Memoized children re-render anyway. Hoist the handler or pass ids and look up in the handler.

The list-key tax

`key={index}` is not a key — it's a lie. Reordering, deleting, and animating all break. Use a stable id from your data.

Diagnostic rule of thumb — if the React Profiler shows >20 components rendering when a user types one character in an input, something is wrong at the tree shape, not the component code.
Forms are the product

Every serious app is 60% forms. Ours don't hurt to use.

Multi-step wizards, autosave every 2 seconds, offline-tolerant, keyboard-first, screen-reader-honest.

  1. →
    Schema first

    Zod schema is the contract. Types, backend validation, and form validation all derive from one source.

  2. →
    React Hook Form

    Uncontrolled by default — inputs don't cause parent re-renders while you type.

  3. →
    Field-level errors

    Errors appear on blur, disappear on valid keystroke. Submit re-validates the whole schema.

  4. →
    Autosave, not manual

    Debounced 800ms mutation with optimistic UI. Draft rows in the DB, promoted on submit.

  5. →
    Keyboard flow

    Tab order matches visual order. Enter submits from the last field. Esc cancels dialogs.

  6. →
    Accessibility

    Labels tied via `htmlFor`, errors via `aria-describedby`, live regions for save-status.

Testing pyramid we actually ship

Coverage % is a vanity metric. This is what we test.

Playwright E2E
~10% of the suite

The 8–15 flows a paying customer must complete. Login, create invoice, checkout, export CSV. Runs on every PR against a real preview.

Component tests (Testing Library)
~30% of the suite

Every form, every table, every state-heavy widget. Tested through the DOM the way a user uses it — not through implementation details.

Unit tests (Vitest)
~60% of the suite

Every pure function: money formatters, date math, permission checks, the Zod schemas themselves. Fast, deterministic, run in milliseconds.

The day we hand it over

Code you can staff on Fiverr and not regret.

README explaining the run/build/deploy trilogy in 6 commands
ARCHITECTURE.md — the 3 decisions we'd defend and the 3 we'd revisit
Storybook covering every reusable component with real props
Type-safe API client generated from the OpenAPI spec
ESLint + Prettier + TypeScript strict, all wired to pre-commit
Loom walkthrough of the weird parts (there are always weird parts)
Onboarding checklist: from `git clone` to first PR in under 90 minutes
30-day advisory line for whoever inherits the codebase

React FAQ

Blog-only sites, brochure sites without any product surface, and anything under 5 static pages. If there's no interactive state, ship HTML + a sprinkle of JS. React earns its weight when you have forms, tables, real-time data, or long sessions.

Where state lives

Six places state can go. Pick the wrong one and every render becomes a bug.

Local (useState)
One component reads and writes. Default. Don't lift unless a sibling needs it.
Lifted (parent-owned)
Two siblings share it. Owner is the nearest common ancestor. Prop-drill exactly one level — beyond that, reconsider.
Context
Cross-cutting, rarely-changing data: theme, locale, current user. Do NOT put frequently-updating state in context — every consumer re-renders.
Server cache (TanStack Query / SWR)
Anything fetched from a server. Get caching, refetch, background revalidation, mutation invalidation for free. Never `useState` a fetch response.
URL state (search params)
Filters, tabs, pagination — anything a user might bookmark or share. `useSearchParams` from the router, not React state.
Form state (react-hook-form / Conform)
Fields, validation, submit. Isolated re-renders per field. Native `<form>` semantics respected. Don't build this from `useState`.

Bring us a React problem, not a spec.

A dashboard that lags. A form that no one wants to fill in. A legacy admin nobody will touch. Those are the good conversations.

Talk to a React engineer
Right tech, honest advice

Not sure if this is the right stack for you?

Tell us your goals — we'll recommend the right technology honestly, even if it isn't this one.

  • Personal reply from Siddiq within 24 hours
  • Honest advice — no pushy sales, no fake urgency
  • Clear next steps, timeline, and estimate

We only use your details to reply to this enquiry. Privacy.