OTFotf
All posts

Expo SDK 56 killed the platform-specific file — one component renders on iOS, Android, and web

D
DaveAuthor
5 min read
Expo SDK 56 killed the platform-specific file — one component renders on iOS, Android, and web

For years, the price of "write once, run on iOS, Android, and web" was a folder full of forks: Button.tsx, Button.ios.tsx, Button.web.tsx, sometimes Button.native.tsx. The bundler picked the right one per platform. It worked, but it meant the thing you called <Button> was three (or four) different files that had to be kept in sync by hand. Change the API in one, forget the others, ship a bug that only happens on Android.

Expo SDK 56 (beta, May 2026) quietly retires that pattern. Its new universal components — Host, Row, Column, Text, and friends — render on all three platforms from a single definition. No .ios.tsx. No .web.tsx. One file, three targets.

This is a bigger deal than a release note suggests. It moves "one component, every platform" from a thing you bolt on with a build trick to a thing the platform hands you natively. And it validates a bet some of us made a while ago.

What SDK 56 actually ships

The headline numbers first, because they matter for whether you can adopt it: React Native 0.85.2, React 19.2.3, and Hermes v1 as the default engine. Hermes v1 is the part that makes universal rendering tractable — a faster, more predictable JS runtime across platforms means the same component code behaves the same way on each.

On top of that runtime, the universal components are the story:

// One file. Renders on iOS, Android, and web — no platform suffix.
import { Host, Column, Row, Text } from 'expo/ui'

export function StatRow() {
  return (
    <Host>
      <Column>
        <Text>Revenue</Text>
        <Row>
          <Text>$4,200</Text>
          <Text>+12%</Text>
        </Row>
      </Column>
    </Host>
  )
}

Host is the bridge to the native view tree. Row / Column are layout primitives that map to real native layout on mobile and to flexbox on web. Text is a Text node on native and a span-equivalent on web. You write the structure once; the platform resolves the primitive.

The takeaway: the platform-specific file is no longer the default unit of cross-platform UI. The component is.

Why the file-split was a tax, not a feature

The .ios.tsx / .web.tsx convention was always a workaround for the same underlying problem: web and native have different primitives (<div> vs UIView), so the same component needs two implementations. The convention let you colocate those implementations under one import name. Useful — but it pushed the cost onto you:

  • Drift. Two files, one API. The moment you edit one, the other is stale until you remember it exists.
  • Doubled surface. Every prop, every variant, every edge case, written twice.
  • Illegibility for AI. A coding agent that opens Button.tsx sees half the truth. The web behaviour lives in Button.web.tsx, which it may not even read. It generates a call that works on one platform and silently breaks on the other.

That last one matters more every month. If your codebase is going to be read and extended by Claude Code or Cursor, a single-file component is not just cleaner — it's the difference between the agent getting it right and the agent guessing.

Universal components collapse the two-file tax into one. That's the win SDK 56 is shipping.

This is the same-API thesis, validated from the platform side

Here's why this release is worth a whole post instead of a footnote: the universal-component model is the exact bet behind cross-platform component libraries that expose one name, one set of props, two implementations underneath.

That's how @otfdashkit/ui (web) and @otfdashkit/ui-native (mobile) work:

import { Card, Button, Text } from '@otfdashkit/ui'        // web → Radix + Tailwind
import { Card, Button, Text } from '@otfdashkit/ui-native' // native → Tamagui
// Identical screen in both files:
<Card>
  <Text>Revenue</Text>
  <Button onPress={refresh}>Refresh</Button>
</Card>

The thesis was always: the platform difference belongs inside the library, resolved once and tested, not scattered across your app as .ios / .web forks. SDK 56 makes that the platform's official position too. When the framework itself ships Host / Row / Column as universal primitives, "same component, every platform" stops being an opinion and becomes the grain you're working with.

Primitives are the floor, not the ceiling

A fair pushback: Host / Row / Column / Text are primitives. Real apps need a DataTable, a Command palette, a Sheet, a date picker, charts. SDK 56 gives you the universal floor; it doesn't give you the 137-component surface a product actually ships with.

That's the gap a component library fills on top of the universal primitives: the opinionated, accessible, themed components — same API web and native — built on the floor the framework now provides. SDK 56 makes the foundation universal; a library makes the whole UI universal, and ships the AI-config layer (CLAUDE.md, tested prompts) so an agent extends it without inventing conventions.

What this unlocks

When the component is the unit and the file-split is gone:

  • One mental model. Learn <Row> once; it's the same on the phone and in the browser.
  • One thing to fix. A layout bug is fixed in one place, not "in one place and also the .web variant."
  • Agent-legible by default. A single-file universal component is fully visible to a coding agent in one read — no hunting for a platform variant it might miss.
  • A real expo run:ios / expo run:android / web build from one source — which was the promise the file-split convention only half-delivered.

The platform-specific file had a good run. SDK 56 is the signal that the era of forking your components per platform is ending — and that building on a same-API component layer, rather than around it, is now the path with the grain instead of against it.

react-nativecross-platformarchitectureai-tools

On this page