React Native Multi-Root Architecture
The rearchitecture that unblocked React Native feature development on the Echo Look app — multi-root packages, a real build system, and standards for state, persistence, and navigation.
By late 2017 the Echo Look app had reached a deadlock on React Native. Every React Native feature — however unrelated — lived in one package under a single giant root view, so each new feature inherited the problems of every existing one and two features could not be built in parallel without brutal merge conflicts. There were no standards for state management, data persistence, or navigation. And the build system had no way to consume JavaScript bundles at all: bundles were copied and pasted by hand into the iOS and Android native packages and checked in.
Most of the team considered it a lost cause. I made the case that it was solvable, and then solved it. The work made me the React Native architect for the Echo Look app.
The core decision: one root view or many
The immediate question was how to add a second React Native feature — the social feed that became Echo Look Style Feed — alongside the existing one.
Option 1 was to expand the single root view to encapsulate the new feature. It keeps one entry point, which looks simple. But it forces every future feature to share the same Redux store and navigation solution, requires store initialization even for trivial features, and tightly couples all new work to the existing Gallery architecture.
Option 2 was to isolate each feature in its own root view, with its own state, internal navigation, and sub-components, and move genuinely shared components into a common package.
For a green-field app where the React Native root is the app root, option 1 is defensible. Echo Look used React Native at the feature level inside a native app, which makes option 2 the only real choice. I deliberately framed it as avoiding a one-way door: we needed to make the right architectural call for the new feature without first being blocked on refactoring the old one.
Package structure
Three kinds of package, with clear rules about what belongs where:
- Common — shared and utility components, agnostic of state management and navigation strategy, so they can be reused anywhere.
- Feature — one package per feature, declaring its own dependencies and owning its own internal navigation, feature-level state, and persistence. A feature package only contains components that render inside its root view; anything rendered outside one is by definition a different feature and gets its own root.
- App — registers every root-view feature component and generates a single app bundle, integrating with iOS and Android across one bridge.
Standards for state, persistence, and navigation
The architecture was only half of it. The other half was writing down rules the team could actually follow.
State lives at exactly one of three levels. Component state goes through the React lifecycle (or a plain instance variable when it shouldn't trigger a re-render) and is never shared. State shared between components in a feature belongs in the feature's Redux store. State shared across features belongs in native state, reached through the bridge. The governing principle: state lives in a single place and is queried, not cached, so there is one source of truth.
Persistence follows the same shape, with caveats worth writing down — anything that persists data owns cleaning it up at logout; AsyncStorage is globally writable, so don't stomp another feature's keys; and AsyncStorage has real performance problems on Android because it runs on a single-threaded AsyncTask, which applies transitively to Redux-Persist and anything else built on it.
Navigation was constrained by the app being natively navigated: React Native navigation is used strictly within a feature. Feature-to-feature and feature-to-native transitions all go through native navigation.
Build system
Alongside the architecture I rewrote the iOS and Android build systems so JavaScript bundles were generated and consumed automatically as part of the native app builds, eliminating the hand-copied bundles and the human error that came with them. I then fought through JavaScript package dependency resolution to get Jest and ESLint running inside the build pipelines, so unit-test failures and lint violations would fail the build — the team went from zero coverage and zero coding standards to enforced ones.
The same build approach was reused for the React Native at Amazon conference app.
What it unlocked
The multi-root architecture let us build the Style Feed as a separate package, in parallel with continued maintenance of the existing gallery feature, and integrate a partner team's React Native WebSocket library as their first non-Shopping-app consumer — something that had only been hypothetically possible before. Updates to one feature stopped causing regressions in the other, and QA regression testing could be scoped to the feature that actually changed.
The coding standards and test frameworks outlived the app itself, carrying forward into the Style Hub work and the team's later Shopping-app features.