Back
10 min read

Migrating a Xamarin Government App to React Native: What Went Wrong and What Went Right

React NativeXamarinMobile DevelopmentGovernmentMigration

The Project

A US state government needed to migrate their construction inspection app from Xamarin to React Native. The original app was written in C#, heavily coupled to platform-specific APIs, and the team that built it had mostly moved on.

My job: convert the logic, preserve the functionality, and ship a working React Native version without breaking the inspection workflows that field inspectors relied on daily.


What I Expected vs What Happened

Expected: Rewrite the UI in React Native, port the business logic, done.

What actually happened: The business logic was deeply embedded in Xamarin forms code-behind. There was no clean separation of concerns. The "business logic" was spread across event handlers, custom renderers, and platform-specific projects.

I spent the first two weeks just mapping out what the app actually did. The documentation was outdated. The original developers were unreachable. The only source of truth was the Xamarin code itself.


Patterns That Didn't Transfer

1. Dependency Injection in Xamarin vs manual wiring in React Native

Xamarin (especially with Prism or Unity) encourages constructor injection everywhere. React Native doesn't have a built-in DI container. I considered using InversifyJS but decided against it — the team wasn't familiar with it and it would add complexity.

Instead, I used a simple service locator pattern with React Context:

tsx
const InspectionServiceContext = createContext<InspectionService | null>(null);

export function InspectionServiceProvider({ children }: { children: React.ReactNode }) {
  const service = useMemo(() => new InspectionService(), []);
  return (
    <InspectionServiceContext.Provider value={service}>
      {children}
    </InspectionServiceContext.Provider>
  );
}

Simple. Testable. No magic.

2. Custom Renderers vs React Native Components

Xamarin custom renderers let you write platform-specific UI code with full native access. React Native's equivalent is native modules, but for this project, most of the custom rendering was for maps, signature capture, and camera.

React Native has well-maintained libraries for all three. The tricky part was matching the exact UX the inspectors were used to — muscle memory matters when you're in the field typing on a wet construction site.

3. MVVM vs Hooks

Xamarin MVVM with two-way data binding is elegant for form-heavy apps. React doesn't have two-way binding by default. I used controlled components, which worked fine but required more boilerplate for complex forms with validation.


What Broke

SQLite migration. The Xamarin app used SQLite with a specific schema and migration strategy. Porting the data layer to react-native-sqlite-storage seemed straightforward until I realized the Xamarin version used raw SQL for complex queries that didn't translate cleanly.

I ended up writing a compatibility layer that preserved the existing database structure so field inspectors could migrate their devices without data loss.

Offline sync. The app needs to work in remote areas with intermittent connectivity. Xamarin had a custom sync engine that queued inspections locally and pushed them when online. I rebuilt this using Redux Persist with a custom sync middleware:

tsx
const syncMiddleware: Middleware = (store) => (next) => (action) => {
  const result = next(action);
  if (action.type?.startsWith('inspection/')) {
    syncQueue.add(action);
    if (navigator.onLine) {
      processSyncQueue(store);
    }
  }
  return result;
};

It's not as elegant as the Xamarin version, but it works.


What Went Right

TypeScript caught bugs early. The original Xamarin code was C# (typed), so converting to TypeScript preserved type safety. Union types and discriminated unions mapped naturally to the inspection state machine.

Hot reload saved days. The Xamarin build cycle was 30-60 seconds per change. React Native's Fast Refresh made UI iteration nearly instant.

Package ecosystem. React Native has a library for almost everything. The Xamarin ecosystem was shrinking — finding maintained packages was increasingly difficult.


Performance After Migration

  • App size: 45 MB (Xamarin) → 28 MB (React Native with Hermes)
  • Cold start: 3.2s → 1.8s
  • Scroll performance: comparable (both use native components)
  • Memory usage: slightly higher in React Native due to the JS bridge

The Hermes engine made a real difference for startup time.


What I'd Do Differently

  1. Spend more time on the data layer. The SQLite migration cost more than expected. I'd write a comprehensive test suite for the data access layer first, before touching the UI.

  2. Skip the animation polish. I spent a week making transitions smooth. Field inspectors don't care. They want the app to load fast and capture data reliably.

  3. Involve inspectors earlier. Some UX decisions I made in isolation were wrong. A quick demo to real users would have caught them in days instead of weeks.


Final Thoughts

Would I recommend migrating Xamarin to React Native? Yes — if the Xamarin app is actively maintained and you have budget for a thorough migration. No — if the app is stable and the team knows Xamarin well. The migration cost is real, and you need a good reason to justify it.

For government apps specifically, the long-term maintainability of React Native (larger community, more libraries, easier hiring) was the deciding factor. Three years from now, finding a Xamarin developer will be hard. React Native developers are everywhere.