A team rewriting its app in this month's framework to fix the speed,
a search box whose effects set state that sets more state, and a
marketing page that ships more JavaScript than a video game. None
of them is fixed by switching tools.
Every project grows a hand-rolled fetch wrapper: untyped, half-cached,
retried by accident. Here's one built on purpose, with types inferred
from an OpenAPI schema and every layer explained.
All of the site's copy moves out of components into one central
file, and components read it through `t('key')`. The keys are typed
against that file, so they autocomplete, and a typo or a deleted
line fails the type-check instead of shipping blank.
Click "Show all" on a collapsed block and the button unmounts, taking
keyboard focus with it: the next Tab starts from the top of the page.
`useCollapsible` moves focus to whatever replaced the button.
`line-clamp` cuts text off, but it can't tell your JavaScript whether
it did, so every short description gets a pointless "Show more".
`useOverflow` measures the element and answers that one question.
A "Copied" tick, a "Saved" badge, a shake on a wrong password: state
that should be true for a moment and then go back on its own.
`useMomentaryFlag` is that boolean, with the timer handled.
A `localStorage`-backed `useState` built on `useSyncExternalStore`:
no hydration mismatch under SSR, live updates when another tab
writes, and live updates when another hook on the same page writes.
One hook that lets a component be driven from outside (`value` +
`onChange`) or manage its own state (`defaultValue`), the same
two-mode contract a native `<input>` has, while the component body
only ever reads one value and calls one setter.
A boolean `useState` that hands back `on` / `off` / `toggle` instead
of a raw setter, so call sites read as intent. Every function, and
the controls object itself, stays referentially stable, safe to
pass as props or list as effect dependencies without retriggering
anything downstream.
A reusable fetch factory that infers request/response types from an
OpenAPI schema and adds in-memory caching, retry with backoff, and
AbortController support, no per-endpoint boilerplate, no `any`.
An App Router route is prerendered by default and silently turns
dynamic the moment you read something request-specific. Knowing which
bucket a route belongs in (and what forces it out) is the
difference between an instant page and a server render on every hit.
A team of five was releasing a payments frontend weekly off long-lived
branches, with no SLOs to say whether a release had actually
regressed anything. Moving to trunk-based development with feature
flags and Sentry-backed SLOs turned that into several releases a day.
I kicked off IGP's customer-facing portal at Argentex, a greenfield
React rebuild, and owned the frontend through the rest of it. After
IFX bought IGP, we moved it onto IFX's stack and ran it as ibanq v2
alongside IFX's own v1, migrating clients across cohort by cohort.
Lighthouse hit 95 while real visitors still waited four seconds for
the page. What actually fixed it, and roughly quadrupled organic
traffic, was never in the lab score.
Four product teams each built their own components, so the brand
looked different everywhere. A token-first design system, adopted
incrementally through workshops and pairing, reached 90% adoption
and cut brand-change rollout from weeks to hours.
A third of customers who chose pay by bank never finished paying,
and most of them were lost inside their own bank's login.
App-to-app redirects, a QR hand-off on desktop, a smarter bank
picker and a return page that survives a lost session took
completion from 64% to 82%.
A team rewriting its app in this month's framework to fix the speed,
a search box whose effects set state that sets more state, and a
marketing page that ships more JavaScript than a video game. None
of them is fixed by switching tools.
Every project grows a hand-rolled fetch wrapper: untyped, half-cached,
retried by accident. Here's one built on purpose, with types inferred
from an OpenAPI schema and every layer explained.
All of the site's copy moves out of components into one central
file, and components read it through `t('key')`. The keys are typed
against that file, so they autocomplete, and a typo or a deleted
line fails the type-check instead of shipping blank.
Click "Show all" on a collapsed block and the button unmounts, taking
keyboard focus with it: the next Tab starts from the top of the page.
`useCollapsible` moves focus to whatever replaced the button.
`line-clamp` cuts text off, but it can't tell your JavaScript whether
it did, so every short description gets a pointless "Show more".
`useOverflow` measures the element and answers that one question.
A "Copied" tick, a "Saved" badge, a shake on a wrong password: state
that should be true for a moment and then go back on its own.
`useMomentaryFlag` is that boolean, with the timer handled.
A `localStorage`-backed `useState` built on `useSyncExternalStore`:
no hydration mismatch under SSR, live updates when another tab
writes, and live updates when another hook on the same page writes.
One hook that lets a component be driven from outside (`value` +
`onChange`) or manage its own state (`defaultValue`), the same
two-mode contract a native `<input>` has, while the component body
only ever reads one value and calls one setter.
A boolean `useState` that hands back `on` / `off` / `toggle` instead
of a raw setter, so call sites read as intent. Every function, and
the controls object itself, stays referentially stable, safe to
pass as props or list as effect dependencies without retriggering
anything downstream.
A reusable fetch factory that infers request/response types from an
OpenAPI schema and adds in-memory caching, retry with backoff, and
AbortController support, no per-endpoint boilerplate, no `any`.
An App Router route is prerendered by default and silently turns
dynamic the moment you read something request-specific. Knowing which
bucket a route belongs in (and what forces it out) is the
difference between an instant page and a server render on every hit.
A team of five was releasing a payments frontend weekly off long-lived
branches, with no SLOs to say whether a release had actually
regressed anything. Moving to trunk-based development with feature
flags and Sentry-backed SLOs turned that into several releases a day.
I kicked off IGP's customer-facing portal at Argentex, a greenfield
React rebuild, and owned the frontend through the rest of it. After
IFX bought IGP, we moved it onto IFX's stack and ran it as ibanq v2
alongside IFX's own v1, migrating clients across cohort by cohort.
A personal portfolio on a monorepo architecture, built around fast page loads, rich MDX content, and reusable UI components.
Making TrueLayer's Website Rank and Convert
Case study · Mar 01, 2022
Lighthouse hit 95 while real visitors still waited four seconds for
the page. What actually fixed it, and roughly quadrupled organic
traffic, was never in the lab score.
Four product teams each built their own components, so the brand
looked different everywhere. A token-first design system, adopted
incrementally through workshops and pairing, reached 90% adoption
and cut brand-change rollout from weeks to hours.
A third of customers who chose pay by bank never finished paying,
and most of them were lost inside their own bank's login.
App-to-app redirects, a QR hand-off on desktop, a smarter bank
picker and a return page that survives a lost session took
completion from 64% to 82%.