At Edelta Corporation, we needed to ship a field inspector mobile app alongside the existing web-based claims portal — and we needed to do it without doubling the engineering team. The decision: shared React codebase, deployed as React Native for mobile and as a web app, with Capacitor bridging native device features.
Here's how it worked, where it struggled, and what I'd do differently.
Why Not Just React Native (Pure)?
React Native is excellent for mobile-first experiences. But we had an existing React web codebase that field inspectors also used on their office desktops. Maintaining two separate codebases (React web + React Native) would have meant:
- Two component libraries to maintain
- Two deployment pipelines
- Two sets of bugs to fix
- ~2× the surface area for a 3-person frontend team
The team had strong React web expertise but limited native mobile experience. Capacitor let us use our existing React components and add native capabilities where needed.
The Stack
What Capacitor Made Easy
Camera Access for Evidence Photos
Field inspectors photograph damage at job sites. The Camera plugin worked out of the box:
import { Camera, CameraResultType } from '@capacitor/camera';
const captureEvidence = async () => {
const photo = await Camera.getPhoto({
resultType: CameraResultType.Base64,
quality: 80,
allowEditing: false,
});
// Upload to S3 via our document service
await uploadEvidencePhoto({
claimId: currentClaim.id,
imageData: photo.base64String,
mimeType: 'image/jpeg',
});
};
On web, this falls back to a standard <input type="file" accept="image/*" capture> element. Same code path, different underlying implementation.
GPS for Inspection Location
import { Geolocation } from '@capacitor/geolocation';
const getInspectionLocation = async () => {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
});
return {
lat: position.coords.latitude,
lng: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: position.timestamp,
};
};
We stamped every inspection record with GPS coordinates + timestamp. This became important for verifying that inspectors actually visited the claimed location.
Push Notifications
import { PushNotifications } from '@capacitor/push-notifications';
// Register for push
await PushNotifications.register();
// Handle incoming
PushNotifications.addListener('pushNotificationReceived', (notification) => {
// Navigate to the relevant claim
if (notification.data.claimId) {
router.push(`/claims/${notification.data.claimId}`);
}
});
AWS SNS → FCM/APNs → Capacitor Push → React state update. Inspectors got notified of new assignments without opening the app.
Where Capacitor Struggled
Performance vs. Pure Native
Capacitor renders your React app in a WebView. For data-heavy screens (think: a list of 500+ inspection records with photos), scrolling performance lagged on mid-range Android devices.
Fix: We added content-visibility: auto to long list items and paginated all large queries. This brought scroll performance to acceptable levels, but it required explicit CSS work that a pure native app wouldn't have needed.
Platform-Specific UI Expectations
iOS users expect swipe-to-delete. Android users expect the back button. React doesn't naturally handle these.
Fix: Platform detection + conditional UI:
import { Capacitor } from '@capacitor/core';
const isIOS = Capacitor.getPlatform() === 'ios';
const isAndroid = Capacitor.getPlatform() === 'android';
// Show swipe actions only on iOS
{isIOS && <SwipeableRow onDelete={handleDelete} />}
Not ideal, but manageable for a handful of cases.
Offline is Hard
We needed inspectors to work in areas with no cell signal (rural inspection sites). Capacitor has a SQLite plugin for offline storage, but offline-first architecture requires careful thought about conflict resolution when the device comes back online.
We kept it simple: offline read-only (inspectors can view claim details) + queued write (photo uploads queue locally, flush when signal returns). We used a React Query + localStorage approach rather than SQLite for this scope.
Capacitor vs. React Native (Expo) Tradeoff Table
| Factor | Capacitor + React | Expo / React Native |
|---|---|---|
| Web code reuse | ✅ Excellent | ❌ Separate components |
| Native performance | ⚠️ WebView limited | ✅ Native rendering |
| Plugin ecosystem | Good, growing | Mature |
| Team learning curve | Low (React skills) | Medium (RN specific) |
| App store deployment | ✅ Standard | ✅ Standard |
| Best for | Web teams going mobile | Mobile-first products |
For us — a web-first team adding mobile — Capacitor was the right call. For a consumer app where UX polish matters most, I'd choose Expo/React Native.
Deployment
The same React build goes three ways:
- Web:
npm run build→ static files → AWS S3 + CloudFront - iOS:
npx cap sync ios→ Xcode build → App Store - Android:
npx cap sync android→ Android Studio → Play Store
The Capacitor sync copies the web build into the native projects. Both GitHub Actions pipelines (web CI/CD + mobile CI/CD via Fastlane) trigger from the same codebase on main.
One codebase, three ships. For a team our size, the productivity multiplier was real.