Skip to content
SojournerLet’s talk
← All guides

Sojourner® Guides

One codebase, two real apps

React Native gets you both platforms. It does not get you native. Those are different promises, and conflating them is how you end up with an app that technically runs on iOS and feels like a website in a costume.

The gap is small. It is also the entire thing users notice.

Where "one codebase" stops

Shared logic, shared components, shared state — genuinely shared. That part works, and it's most of the app by line count.

What isn't shared is the physics people already learned from their OS:

  • Scroll deceleration. iOS decelerates slower and overshoots with a bounce. Android stops shorter and shows a stretch. Ship one and half your users feel friction they can't articulate.
  • Back navigation. Android has a hardware/gesture back that must work. If your modal doesn't handle it, your app is broken on half the market, and it will never show up in your crash logs.
  • The keyboard. KeyboardAvoidingView needs behavior="padding" on iOS and usually nothing on Android, where the window resizes for you. This is the single most common "why does this look wrong" bug I see.
  • Type. San Francisco and Roboto have different x-heights and metrics. The same fontSize is optically different sizes. Same for line-height defaults.
  • Elevation. iOS uses shadowColor/shadowOpacity/shadowRadius. Android ignores all of it and uses elevation. Set both or one platform is flat.
// not a hack — a statement that the platforms differ on purpose
const card = {
  borderRadius: 16,
  ...Platform.select({
    ios: { shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { height: 6, width: 0 } },
    android: { elevation: 6 },
  }),
};

The rule I use

Share the logic. Fork the feel.

Business rules, API calls, validation, state machines, formatting — one implementation, no exceptions. If it can be wrong, it should be wrong in exactly one place.

Motion, gesture, haptics, elevation, and type metrics — fork them without apology. Platform.select is not technical debt. It's the acknowledgment that your user has spent years learning one of these two languages and did not install your app to learn a third.

What this costs

Maybe 5% more code, concentrated in a handful of primitives. If your <Card>, <Sheet>, <Button>, and navigation transitions each know what platform they're on, everything built from them inherits it for free.

That's the leverage: fork at the primitive, share at the feature. Get the bottom layer right and the other 95% genuinely is one codebase.

Users don't grade you on architecture. They grade you on whether the scroll felt right, and they grade in the first four seconds.