Skip to content
DevSaheb

technology

React developers who know when not to use React

React is the default choice for interactive interfaces, and defaults deserve scrutiny. We use it when an interface has genuine client state to manage — and we will tell you when a page would be faster and cheaper as server-rendered HTML with a little JavaScript on top.

How we build React that stays maintainable

The failure mode of a React codebase is not performance. It is that after eighteen months nobody can change one component without breaking another, because state ended up everywhere.

So we are strict about where state lives. Server state — anything that came from an API — is cached and invalidated in one place, never copied into component state where it can go stale. Genuine UI state stays as local as it can be. Global state is a last resort, not a starting architecture.

Components are typed end to end. Props are not optional-by-default, and we avoid the pattern where a component takes fifteen booleans and renders six different things. If a component needs a mode flag, it is usually two components.

Rendering: the decision that actually matters

The choice between client rendering, server rendering, and prerendering has more effect on real user experience than any amount of component optimisation, and it is made once at the start.

A client-rendered React app ships an empty HTML document. Search engines will eventually render it, but social crawlers — LinkedIn, WhatsApp, Slack — never execute JavaScript, so shared links preview as bare URLs. For anything marketing-facing that alone settles it.

We make that call explicitly, per project, and write down why. This site is prerendered to static HTML for exactly that reason.

Testing behaviour, not implementation

We test what a user can do, not what a component's internals look like. A test that asserts on state variables breaks every time you refactor and catches nothing; a test that fills the form and checks the result survives the refactor and catches the regression.

That keeps the suite useful past the first rewrite, which is the only test suite worth having.

TypeScript · src/components/InvoiceTable.tsx

The most common avoidable bug in React codebases, and the fix. Copying props into state and syncing with an effect means every change renders once with stale data before the effect corrects it — visible as a flicker, and the source of filters that reset themselves. Anything derivable from props and state is computed during render instead, which removes the effect, the duplicated state and the entire class of bug. Only genuine synchronisation with something outside React is left as an effect.

// Before: three pieces of state that are all the same information, kept in
// step by effects. Each render shows the old value first, then corrects it.
//
//   const [visible, setVisible] = useState(invoices);
//   const [total, setTotal] = useState(0);
//   useEffect(() => { setVisible(invoices.filter(matches)); }, [invoices, query]);
//   useEffect(() => { setTotal(visible.reduce(sum, 0)); }, [visible]);

export function InvoiceTable({ invoices }: { invoices: Invoice[] }) {
  // The only real state: what the user typed. Everything else follows from it.
  const [query, setQuery] = useState('');
  const [sort, setSort] = useState<SortKey>('dueOn');

  // Derived during render. No effect, no stale first paint, no flicker.
  const visible = useMemo(
    () => invoices.filter((i) => matches(i, query)).sort(by(sort)),
    [invoices, query, sort],
  );

  // useMemo is a performance hint, not correctness: this would still be right
  // computed inline. It is here because the list is long, which is a reason.
  const totalMinor = useMemo(
    () => visible.reduce((sum, i) => sum + i.totalMinor, 0),
    [visible],
  );

  // An effect that is genuinely one: synchronising with something outside
  // React, with a cleanup, rather than deriving state from other state.
  useEffect(() => {
    const title = document.title;
    document.title = `${visible.length} invoices — ${title}`;
    return () => { document.title = title; };
  }, [visible.length]);

  return <Table rows={visible} footerTotalMinor={totalMinor} onQuery={setQuery} />;
}

Who works on this

  • Nayemul Saheb

    CEO & Software Engineer

    Builds our React work, including this site.

The rest of the team

When React is the wrong tool

A content site with a contact form does not need React. It will be faster, cheaper, and easier to maintain as server-rendered HTML with a small amount of JavaScript. We would rather tell you that than bill for the framework. React earns its weight when there is real client-side state — an editor, a dashboard, a multi-step flow with live validation. If your interface is mostly reading, the framework is overhead you pay for on every page load.

Common questions

React or Next.js?
Next.js is React plus a rendering and routing opinion. If you need server rendering, file-based routing, and image handling out of the box, take the opinion. If you are embedding an interface into an existing application, plain React with your own build is usually less to fight.
Can you work in our existing React codebase?
Yes, and we do not start by proposing a rewrite. We read it first. Most codebases described as unmaintainable have three or four specific problems, and fixing those is far cheaper than starting again.
What about React Native for our mobile app?
Sharing React knowledge across web and mobile is real, but sharing code between them is mostly not — the UI layer is where the work is and it does not transfer. Choose React Native because you want one mobile codebase for iOS and Android, not because you already use React on the web.
How do you keep bundles small?
A gzipped budget enforced in CI, so a regression fails the build rather than being discovered in a Lighthouse report months later. Heavy dependencies — editors, chart libraries, admin tooling — are route-split so they never load for visitors who do not reach that route.

Related services

Built with

Tell us the constraint. We will tell you honestly if we fit.