background-shape
April Retro, React 18 and Next.js in Production
April 29, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — Upgraded two apps to React 18, one of them simultaneously to Next.js 12. Both shipped to staging in April, prod by month-end. Automatic batching: visible perf win. Strict Effects: caught four real bugs. Next.js migration: half a day. Concurrent features: deferred — adopting one at a time over Q2.

End of April. End of the first quarter+ of this blog. Time for the wrap-up.

Going into April I had two React apps on 17. By end of April: both on 18. One also migrated CRA → Next.js 12 along the way. This is the honest retro: what worked, what surprised me, what I’d do differently.

What shipped

Customer dashboard (Next.js 12 + React 18). Was already on Next.js 11 + React 17. Upgraded both. Total work: ~3 hours including the Strict Effects audit. Live in prod by April 13.

Internal admin (CRA → Next.js 12 + React 17 → 18). Two upgrades in one PR. Half a day of work + an afternoon of testing. Live in staging April 22, prod April 28.

Marketing site. Already on Next.js 12. Bumped React to 18 alongside the next-version bump. ~30 minutes. Live April 6.

Three apps, three upgrades. No production incidents. No rollbacks.

What worked

The React 18 upgrade itself was uneventful. Package upgrade, createRoot switch, types fix, ship. The horror stories I’d read from people in early 2022 didn’t apply by late April — the library ecosystem had caught up.

Strict Effects caught four real bugs.

  • A WebSocket subscription that didn’t unsubscribe on unmount — would have leaked connections over hours of use.
  • A setInterval that re-fired on re-mount, doubling background polls.
  • An IntersectionObserver that registered but never disconnected.
  • An effect that triggered an unconditional fetch + state set without cleanup — would have caused stale state in Strict Effects’ double-mount.

None of these were caught by tests. None were customer-visible (yet). All would have surfaced eventually as obscure bugs hard to attribute. The double-mount in dev surfaced them in 20 minutes.

Automatic batching showed up in metrics. RUM data showed ~5–7% reduction in average JS execution time per interaction. Not dramatic; consistent. Free.

Next.js 12’s SWC compiler is everything they promised. Cold dev startup dropped from 18s (CRA + Babel) to 3s. Hot reload feels instant. The compile-time win alone justifies the migration.

next/image paid off immediately. The marketing site’s mobile Lighthouse score went from 64 → 92 with no other changes. CLS dropped from 0.18 to 0.02. The kind of win you can’t ignore.

What surprised me

TypeScript’s React.FC deprecation pattern. Knew it was coming; didn’t expect every component file to need touching. Wrote a codemod (couple hours) to migrate the customer dashboard. Worth it for the cleaner types.

useRouter().query being undefined on first render. Hit this on a dynamic route, spent 20 minutes debugging “why is the user undefined?” before remembering the SSG hydration sequence. Now using getStaticProps for almost every dynamic route specifically to avoid this.

<Link> requiring <a> children in Next 12.x. Migration script wasn’t aware of this; warnings filled the console until I patched all the missed cases.

The middleware Edge runtime constraints. Tried to bring in our usual jsonwebtoken for session verification; build failed. Switched to jose. Trivial fix; surprising friction.

ISR’s per-instance behaviour on K8s. Three pods can have three slightly different cached versions of a page for a few seconds after revalidation. Acceptable in our case but worth knowing upfront.

What I deferred

Concurrent rendering features. useTransition, useDeferredValue, Suspense for data fetching — none of these went into the upgrade PRs. They’re separate adoption work. Plan: one feature per app per month for the rest of Q2.

React Server Components. Not production-ready. Revisit when Next.js 13 ships (expected later this year).

Migrating to the upcoming app/ directory. Same — wait for stable framework support.

Remix. It’s good. We’re not switching. Next.js wins on familiarity for our team.

What I’d do differently

Run Strict Effects audit BEFORE the React 18 upgrade. In hindsight I should have wrapped the existing app in <StrictMode> in dev, fixed the effect cleanups, then upgraded. Would have made the upgrade truly boring. Instead the upgrade and the cleanup happened together, which slowed the post-merge testing.

Adopt next/image in the CRA → Next.js migration PR. It was the lowest-hanging perf win and instead I shipped it a week later. Should have rolled into the migration.

Set up image domains in next.config.js upfront. Each missing domain crashes the build the first time someone references it. Audit + add all upfront.

Have the rollback ready. I didn’t, on the basis that “it’s just a major version bump.” If something had gone sideways in prod it would have been a frantic afternoon. Always have rollback rehearsed.

What May looks like

The plan for next month: Workflow Automation: Building internal developer tools using n8n workflows and Jira APIs. Pivot away from product code for a month, focus on tools that save the team time. Same shape: three articles per week, single-month theme.

Topics I’m planning:

  • n8n self-hosted setup
  • Jira API workflows (status changes, auto-assignment, slash commands)
  • Slack-triggered automations
  • Webhook patterns for keeping internal systems in sync
  • “ChatOps” for ops tasks (kicking off deploys, querying status)

The audience shift is real — May’s posts will land closer to “engineering productivity” than to “shipping code.” That’s deliberate; the engineering work that doesn’t ship as a feature is the work that compounds.

Quarter wrap

Four months in. ~50 posts. The pattern is sustainable: weekly cadence per theme, focus on production-shaped specifics over generic intros, lean on real numbers and real code over diagrams. Going to keep it going.

If you’ve been reading along, thanks. If you found something useful — any of the posts that changed how you’d ship something — I’d genuinely like to know. Drop a comment when comments ship (still on the roadmap), or email.

See you in May.