PATTERN Cited by 1 source
Turbo Module + DI contract for native interop¶
Problem¶
In a brownfield RN integration, some features span the RN/native boundary: RN code needs to trigger effects in the legacy native app (e.g. RN wishlist-add updates the legacy-native wishlist-badge counter), and the legacy app needs to notify RN about state changes.
Direct coupling (RN imports legacy-app code, or vice versa) violates the package-boundary isolation that makes the brownfield architecture work (see patterns/rn-as-consumable-npm-entry-point). The Developer App also needs to be able to exercise these interop flows without needing the full legacy-app context.
Pattern¶
Define the RN ↔ native-app boundary as a three-language API contract, implemented by the legacy app at runtime and mocked by the Developer App:
-
TypeScript turbo-module Spec — the RN-side interface:
-
Swift protocol — iOS interface mirroring the TS spec:
-
Kotlin interface — Android equivalent.
-
DI injection point — a static delegate slot the host app sets at startup:
-
Legacy app registers at startup:
-
Developer App registers a mock at startup that updates local mock state when RN calls through.
Runtime flow¶
RN TS code → Framework SDK → Host app impl
(calls Spec methods) (dispatches through (legacy app's
Swift/Kotlin protocol real impl OR
via DI delegate) Developer App's
mock)
Why "three-language" matters¶
Getting this right across all three languages requires up-front contract alignment. Zalando explicitly flags this as a lesson-learned ( sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native):
"Especially when combining three environments into one (TypeScript, Swift and Kotlin) it's crucial to first properly define these API contracts and ensure that all involved environments are compatible with this contract as early as possible. Otherwise, you run into challenges where the API design might not be feasible on all platforms, requiring you to undo work that has already been done."
The contract-first discipline is itself a pattern — see patterns/api-contract-first-across-three-languages.
Why DI, not static coupling¶
The DI slot (WishlistConfig.delegate) is the seam that
enables the Developer App. Without DI:
- The Framework SDK would have to
importthe legacy app's wishlist implementation directly. - The SDK couldn't link without the legacy app, so the Developer App couldn't link either.
- The two-consumers model (concepts/react-native-as-a-package) would break.
With DI: SDK exposes the protocol slot; consumers (legacy app, Developer App) inject implementations at startup. SDK depends only on the protocol, not on any particular implementation.
When to reach for this pattern¶
- You have a brownfield RN integration with a Framework SDK + Developer App architecture.
- You have interop-dependent features where RN needs to call or be called by legacy-app code.
- You need the Developer App to be able to exercise those interop flows without the legacy app's full state.
When not to¶
- The RN layer is fully self-contained (no legacy interop). Then you don't need the DI seam at all — just ship the Entry Point.
Seen in¶
- sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native — canonical wiki first source. Wishlist-badge increment is the worked example.