Skip to content

PATTERN Cited by 2 sources

Same React code for Web and Native via custom reconciler

Problem

A product is delivered as both a Web site and Native iOS/Android apps. The two experiences share most of the user journey — the same catalogue pages, the same product views, the same promotions — differing mainly in chrome, gesture handling, and platform-native widgets.

Naïvely, you have three choices:

  1. Fork the codebase. Two teams, two stacks — duplicated UI work, duplicated bug fixes, drift between platforms.
  2. Render Web inside a WebView. One codebase but the app loses native performance, gesture handling, and platform feel.
  3. Ship native view code bound to a shared domain model. One domain model, two view implementations — still roughly 2× the UI work per feature.

If you already have a React-based frontend platform and a declarative Renderer abstraction, a fourth option exists: ship one Renderer source and route its output through two different React host-tree targets.

Solution

Keep the Renderer source as a standard React component (JSX, hooks, the works). On the Web, use the default React host-tree target (React-DOM) to emit HTML. For the Native apps, build a custom React reconciler that consumes the same React element tree but emits app-compatible JSON instead of HTML. The apps interpret the JSON locally to produce native views.

              Renderer source (React + TypeScript)
                ┌───────────┴──────────────┐
                ▼                          ▼
           Web path                   Native path
        (standard React            (custom React
         host-tree target)           reconciler;
                                     custom elements)
                ▼                          ▼
        server-rendered HTML           app-compatible JSON
        + client hydration             consumed by iOS/Android
                                        SDKs → native views

The Renderer author writes one React component with one set of data-fetching declarations. The framework decides how to commit the element tree.

Canonical instance — Zalando

Zalando's Rendering Engine does exactly this. Quoting the Part 2 post (Source: sources/2021-09-08-zalando-micro-frontends-from-fragments-to-renderers-part-2):

"If Renderers were able to output JSON instead of HTML, we could reuse the same rendering core as for the web with the same benefits. Our Renderers relied on React for their output. To cover the app-specific use case, we added a custom React reconciler that consumed custom React elements, and output app-compatible JSON instead of HTML. Now, web developers are able to contribute Native apps features by reusing the same set of APIs as they were used to deliver web experiences and bring the web and native apps experiences closer together."

The existing Zalando app had server-side layout steering for parts of the experience (e.g. the App landing page) — the apps were already prepared to consume JSON layouts from a remote server. That pre-existing infrastructure is what makes the JSON-output reconciler a clean fit — the apps did not need to learn a new contract.

Mechanics

The mechanics rely on React's react-reconciler extension point (see concepts/react-custom-reconciler):

  1. Author writes a standard React component. Uses JSX, hooks, props — no special import or API.
  2. Custom React elements are defined for the Native tree's node types (e.g. <AppCard>, <AppList>, <AppImage>). These look like ordinary React components but their identity matters to the reconciler.
  3. Custom reconciler implements the host-config interface (createInstance, appendChild, commitUpdate, createTextInstance, etc.) so that when React walks the element tree, instead of creating DOM nodes it creates plain-object nodes representing the app's view hierarchy.
  4. Serialisation to JSON at the top of the reconciler's commit traversal emits the final JSON document.

Everything above the host tree — data fetching, state, effects, context — is reused unchanged from the Web path.

Why this is leverage

The architectural win the post calls out: "web developers are able to contribute Native apps features by reusing the same set of APIs as they were used to deliver web experiences." Concrete benefits:

  • One contributor model. A feature team writes one Renderer; Web + Native both get it.
  • Same data layer. GraphQL queries, persisted queries, DataLoader batching, circuit-breakered HTTP via Perron — all shared across platforms.
  • Same platform features for free. Monitoring, A/B testing, tracking, logging are wired in at the framework level, not re-implemented per environment.
  • Single source of truth for business logic. A bug fix in a Renderer fixes Web and Native in the same deploy.

Contrast with React Native

This pattern is not React Native. React Native's reconciler targets native view APIs directly via a bridge and commits are driven client-side as interaction happens. Zalando's reconciler emits JSON that travels over the network to apps that interpret it locally — rendering is server-authoritative, with the client as a renderer for the JSON layout.

React Native Zalando custom reconciler
Target Native view APIs (iOS/Android) Serialised JSON
Authority Client-side rendering Server-authoritative
Update shape JS bridge calls Network re-fetch
Native integration Full (any view) Subset the JSON spec supports
Cross-platform leverage Yes — one RN codebase Yes — one RE codebase

Zalando later (2025-10) also adopted React Native as the Native-apps client runtime, composing both patterns — see systems/zalando-rendering-engine for the 2025-10 extension. The custom reconciler remains the 2021-era bridge between Rendering Engine and the native clients.

When to reach for it

  • A React-based Web platform already exists with a declarative component contribution surface.
  • Native apps have an existing contract that can consume a JSON layout description (server-driven UI).
  • The delta between Web and Native feature sets is small enough that a single Renderer model is tractable.
  • Platform-team-owned framework is viable (the reconciler and the app-side JSON interpreter are platform code, not feature-team code).

When not to

  • Native apps need deep platform-native behaviour that can't be expressed in a constrained JSON schema.
  • Server-authoritative rendering is unacceptable (apps need to fully function offline, or need deep local state without server round-trips).
  • The Native apps' contribution model is strongly native-first (Swift + Kotlin teams, unfamiliar with React) and can't be re-pivoted.

Seen in

Client-side counterpart: Appcraft

The custom-reconciler → JSON path has a mobile-native consumer documented in Zalando's 2024-05-15 Appcraft retrospective (sources/2024-05-15-zalando-transitioning-to-appcraft-evolution-of-zalandos-server-driven-ui-framework): Appcraft is the client runtime that parses the JSON screen-configs the reconciler produces and renders them via Flex on top of Texture / Litho into native UIKit / RecyclerView objects.

Notably, Appcraft's client runtime is not React-based — it adopts the Elm architecture internally, so what the reconciler emits is plain JSON, not a serialised React tree. The "same React code for web and native" benefit still holds from the server author's perspective (one React authoring surface → web SSR + mobile JSON), while the client runtime on mobile is free to be platform-idiomatic (Elm + native views) rather than React Native. This is one valid architecture shape; React Native's "React all the way down" is another.

Last updated · 550 distilled / 1,221 read