There’s something deeply satisfying about watching your code evolve, not just grow, but mature. Over the past few months, I had the opportunity to guide our frontend at Kakbima through one of its biggest transformations since we first launched: upgrading our entire React stack from version 17 to 19.
Now, I know what you’re thinking. “Why skip React 18?” We didn’t. We staged it. But we never deployed it fully into production because we made a strategic decision: wait for React 19 to solidify, then move. And do it without downtime. Without broken user flows. Without freaking out our product team. Here’s how it happened.
Why upgrade?
Let’s start with the “why.”
The Kakbima platform serves a pretty diverse audience from policyholders, agents, brokers, micro-insurers, even the insurance providers themselves. That means stability is everything. But so is performance. We had grown confident in our current setup, but you know how it goes, tech doesn’t wait.
We started seeing pain points:
- Increasing bundle sizes
- Complicated state management in large components
- Challenges with concurrency and server-side rendering
- Dependency conflicts when integrating newer packages
And most importantly, we wanted to tap into the latest React features like Actions, Form improvements, use(), and improved Suspense behavior that could drastically simplify code and improve user experience.
The promise: No downtime. No broken features.
This was non-negotiable.
Kakbima is live 24/7. People file claims, manage policies, and manage products in real time. We couldn’t afford even an hour of “sorry, we’re upgrading.”
So the goal was clear: incremental modernization. Behind the scenes, everything could change. But on the surface, everything had to feel the same or better.
1. The foundation: Audit and isolation
Before we touched a single version number, we audited the codebase.
Kakbimas’ frontend was largely built in React 17 with GraphQL API communication. We used standard libraries React Router, Apollo Client, ESLint and Prettier, etc. But over time, parts of the UI became tightly coupled to older patterns.
We started by isolating legacy components and figuring out:
- Which features used class components
- Where we had outdated lifecycle methods
- How deeply nested certain render chains were
We created a heatmap of “risk zones” in the UI and began isolating them using feature flags and lazy loading. This made it safer to touch even the most critical user flows claims and policy renewals.
2. Bridging the gap with React 18
React 18 introduced Concurrent Rendering, automatic batching, and the new Suspense model. These were not just under-the-hood improvements, they required careful opt-in.
So we introduced React 18 behind a feature flag, enabling it only in isolated routes/components. This let us test rendering behavior in the wild, without flipping the switch for everyone.
We wrote tiny components with useTransition, tested Suspense boundaries with fallback UIs, and watched closely how users behaved when interactions started getting faster.
Our infra team also helped us monitor metrics like Time To Interactive (TTI) and CLS (Cumulative Layout Shift), giving us confidence that we weren’t just upgrading, we were improving.
3. Adopting the new architecture of React 19
When React 19’s beta dropped, I knew this was the future.
We had been preparing for this moment by modularizing our components. Each module products, policies, claims, dashboard, user settings was version-controlled and loosely coupled using a microfrontend-esque setup (but not full microfrontends, just smart separation).
That allowed us to do something magical: dual-rendering.
We ran parts of the app in React 17/18 and parts in 19 at the same time. React’s new scoped rendering API (available through unofficial polyfills at the time) allowed us to test isolated routes without tearing the whole app down.
Yes, it took discipline.
Yes, it meant we wrote adapters, bridges, and fallback UIs.
But it also meant we could run battle-tested code next to experimental code… live.
4. Refactoring with emotion, Not just logic
This part is personal.
When you work on a product that helps people protect their futures and most priced assets; cars, health, homes, families, it hits different. Every pixel you render, every interaction you design, is part of someone’s safety net.
So we didn’t just refactor for the sake of code quality. We refactored for clarity. For empathy.
React 19 allowed us to adopt async-first thinking using use() and actions. We rewrote parts of our form handling (claims filing/reporting, KYC, payment info) to use the new APIs and found something surprising: our code was not only faster, it was simpler. Easier to test. Easier to reason about. Easier to care about.
When engineers read code and feel calm, you know you’re doing something right.
5. Testing: The invisible hero
We already had solid test coverage, but this upgrade demanded more.
We focused heavily on:
- Visual regression testing with Chromatic
- End-to-end flows in Cypress (including simultaneous sign ins from broker and policyholder views)
- Load tests in pre-production with 10k simulated policies being filtered, sorted, and updated
We also set up synthetic monitoring with custom alerts. If even a single button disappeared or latency spiked beyond a certain threshold, we’d know before our users did.
6. The final cutover
The night we flipped the switch to fully React 19, it was… quiet.
Too quiet.
No alerts. No errors. No traffic drop.
Our dashboards lit up green, and slowly, the team realized: we did it. No downtime. Just a faster, leaner, more modern Kakbima.
And a quieter night than any of us expected.
So what lessons have I learned
- You don’t upgrade React, you upgrade a system. Think modularly. Start small.
- Test every assumption. Especially the “harmless” ones.
- Empathize with your future self. Leave breadcrumbs in code, docs, and commit messages.
- Measure everything. Gut feelings are great, but real data catches what you don’t feel.
- Celebrate the small wins. Every isolated test, every green PR, every happy product manager.