Skip to content

PATTERN Cited by 1 source

RN as consumable npm Entry Point

Problem

You want to integrate React Native into an existing large native iOS/Android app without:

  • Coupling the RN code to the legacy app's source tree.
  • Forcing every RN contributor to build the full legacy app to iterate.
  • Leaking RN's native dependencies into the legacy app's dependency graph in a way that triggers version conflicts.

Naïve approaches (git submodules, direct-source integration) fail on all three. See concepts/brownfield-rn-integration for the full failure analysis.

Pattern

Package the React Root component + RN initialisation logic into a dedicated npm package — call it the Entry Point. Design the Entry Point so it has two independent consumers:

  1. Greenfield Developer App (a standalone RN app) — see patterns/standalone-developer-app-for-rn.
  2. Native Framework SDK (iOS + Android) — a native library that exposes RN functionality behind a simple native-API-shaped interface. E.g. ReactNativeViewFactory.loadView(deepLinkProps, launchOptions) on iOS.

The legacy native app links the Framework SDK; it does not link the Entry Point directly, and it does not link RN source at all.

Entry Point (npm)
    ├── Developer App (greenfield RN)
    └── Framework SDK (native iOS/Android library)
            └── Legacy Native App (consumes like any framework)

Why this works

  • Dep resolution happens in one place — the Entry Point's package.json. Native deps that RN or RN community packages pull in are resolved inside the Entry Point's boundary; conflicts with the legacy app's deps are contained.
  • Separation is enforced by the package boundary. The legacy app has no way to accidentally import RN internals — only the Framework SDK's public API surface is reachable.
  • Developer experience is restored via the Developer App. Contributors iterate against a standard RN environment; the legacy native app build is out of their loop.
  • Dual consumers force clean API design. The Entry Point can't rely on legacy-app-specific context because the Developer App doesn't have one. This forces dependencies on legacy-app state to be expressed through Turbo Module + DI contracts (see patterns/turbo-module-plus-di-contract-for-native-interop).

Canonical implementation

@callstack/react-native-brownfield is the open-source package that generalises this pattern, produced by Zalando's partners at Callstack.

Canonical production case

Zalando's [[sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native|2025-10-02 post]] describes building out this architecture end to end:

  • Entry Point npm package with React Root + init logic.
  • Developer App with custom dev menu supporting JS-bundle switching (released, PR, local).
  • Framework SDK exposing iOS ReactNativeViewFactory.loadView and Android ReactNativeViewFactory.createViewHostedIn*.
  • Legacy Zalando mobile apps (iOS + Android) link the SDK and progressively mount RN-backed screens.

The validation evidence: Zalando successfully migrated Discovery Feed — the new media-heavy front screen — and several other screens using this architecture.

When to reach for this pattern

  • You have an existing large native app (90+ screens in Zalando's case) that you can't rewrite.
  • You want RN to be progressively adoptable screen-by-screen rather than big-bang.
  • Your RN contributors include web engineers whose productivity is hurt by requiring full native-app familiarity.

When not to

  • You're building RN-first from scratch (greenfield). Don't pay the package-boundary cost for no return; just ship a standard RN app.
  • You only need a single RN screen. The packaging overhead outweighs the benefit for small integrations.

Seen in

Last updated · 507 distilled / 1,218 read