Engineering Strategy

Migrating React Native to React Web:
A High-Fidelity Stack Strategy

Why rewriting from scratch is a mistake, and how to build a unified architecture that respects platform differences without duplicating logic.

Architecture Diagram (Native vs Web Stack)

"The gap between a mobile app and a web app isn't just about screen size. It's about data persistence, navigation stacks, and platform capabilities."

React Native (RN) and React Web share the same DNA, but they speak different dialects. When porting Word Genie 6.0—an app defined by its "Content First" pivot and strict offline capabilities—we faced a critical decision: rewrite the data layer or abstract it? A naive migration would replace local SQLite calls with REST API hooks, breaking the offline-first requirement.

We chose a **Zero-Regression Translation Strategy**. This means carrying over 100% of the business logic (hooks, state machines, SRS algorithms) while surgically replacing the UI layer. This case study details how we achieved parity across three demanding technical pillars: the "Heavy" Data Client, the "Glimpse" Monetization Model, and Skia-based Viral Loops.

1. The Catalyst: Why We Couldn't Just Stay Native

Word Genie began as a strictly mobile-first experience. But as adoption grew in educational settings, we hit a wall that no amount of app store optimization could fix: The Hardware Reality of Schools.

"My students have school-issued Chromebooks. They can't install apps."

"I need to project the Daily Quiz on my smartboard, but AirPlay is blocked on the school network."

Our users—tutors, teachers, and students—demanded a web version not as a convenience, but as a requirement for access. The web is the universal interface of education. But porting a "Heavy Client" app that relies on local SQLite databases to a browser environment presented a massive engineering hurdle.

2. The "Abstracted Data Layer" Pattern

Word Genie's core value is its Adaptive Spaced Repetition System (SRS). The algorithm runs locally to ensure zero-latency reviews. On mobile, this relies on `react-native-nitro-sqlite`. The browser has no direct SQLite equivalent that performs identically without heavy WASM overhead.

Instead of forcing SQLite on the web, we implemented the Service Adapter Pattern. We kept `DataService.js` 95% identical but swapped the import based on the environment.

Figure 1: The Unified Data Interface

flowchart LR UI[UI Components] -->|Calls| DS[DataService Adapter] subgraph Mobile DS -->|Uses| SQL[React Native SQLite] SQL -->|Persists to| FS[Device File System] end subgraph Web DS -->|Uses| IDB[WebDataService] IDB -->|Persists to| IndexedDB[Browser IndexedDB] IDB -->|Encrypts with| WebCrypto[Web Crypto API] end style UI fill:#3b82f6,stroke:#2563eb,color:#fff style DS fill:#10b981,stroke:#059669,color:#fff style IndexedDB fill:#f59e0b,stroke:#d97706,color:#fff

The UI never knows which DB it is talking to. It just asks for `DataService.getDecks()`.

On Web, `WebDataService.js` wraps IndexedDB but exposes a relational-like API. Crucially, to match the security of the mobile filesystem, we implemented Client-Side Encryption using the Web Crypto API. User progress blobs are encrypted with a session-derived key before being stored, preventing casual tampering with the "Glimpse" counters.

3. Porting the "Glimpse" Logic

Our monetization isn't a simple 7-day free trial. We use a 'Glimpse' model where users can try specific features a set number of times (e.g., creating 3 decks or reading 3 passages) before hitting a paywall.

  • Deck Creation: Limit of 3 Lifetime uses.
  • Reading Passages: Limit of 3 Lifetime passages.
  • AI Voice Feedback: Limit of 5 responses per session.

In the mobile app, we track this by running SQL queries against the local database. Rewriting this logic for the web would be risky—if the counts are off, users might get premium features for free. By using the Virtual Data Layer, we kept the original `FeatureGate.js` logic entirely intact. The code still asks 'How many decks exist?', but on the web, the Data Service transparently retrieves that count from our secure IndexedDB wrapper. This guaranteed that our business rules remained identical on both platforms without writing new code.

4. Handling Viral Loops (Skia vs Canvas)

Growth is driven by "Ego-Bait"—shareable images generated after a Daily Quiz. On mobile, Word Genie uses `react-native-skia` for high-performance, layer-based image generation. Since Skia isn't native to the DOM, we established a strict translation rule:

// Strategy: Visual Fidelity
Source: <Canvas style={{width, height}}>...</Canvas> (Skia)
Target: HTML5 <canvas> wrapped in a utility that mimics the Skia declarative syntax.

For simpler layouts, we utilized `html2canvas`, but for the complex "Status Badges" (e.g., "Top 1% Vocabulary"), we ported the Skia drawing commands directly to the HTML5 Canvas API, preserving the exact gradients and typography that drive the viral loop.

5. The "Zero-Regression" Prompt

The success of this migration hinged on a specific AI System Prompt. Large Language Models often try to "fix" code they perceive as clunky, inadvertently stripping out critical logic like `MENU_OPTIONS` arrays or legacy flag checks. We countered this with the **Zero Regression Mandate**.

**THE PRIME DIRECTIVE: ZERO REGRESSION**
Your absolute priority is to preserve **all** existing business logic, conditional rendering, and features. You are a translator, not a designer.

1. Styling Strategy
- **Source:** React Native (`style={tw'p-4'}`)
- **Target:** Tailwind CSS Variables (`className="p-4 bg-background"`). Do NOT use hex codes.

2. Logic & State (DO NOT TOUCH)
- Keep all `useState`, `useEffect`, `useCallback` exactly as they are.
- Keep all conditional arrays (e.g., `MENU_OPTIONS`). If logic seems "clunky," reproduce it exactly.

3. Platform Specifics
- `ActivityIndicator` -> `<Loader2 className="animate-spin" />`
- `@env` -> `import.meta.env`
- `Linking.openURL` -> `window.open`

6. Code Hierarchy & Structure

To minimize cognitive load for developers switching between Mobile and Web repositories, we enforced an identical directory structure. The only difference lies in the implementation details of the `data/` and `components/` layers.

graph TD Root["src"] Root --> Components["components"] Root --> Screens["screens"] Root --> Data["data"] Root --> Contexts["contexts"] Components --> C1["DeckCard.jsx"] Components --> C2["DailyCalendar.jsx"] Screens --> S1["HomeScreen.jsx"] Screens --> S2["DecksScreen"] Data --> D1["DataService.js"] Data --> D2["WebDataService.js (IDB Wrapper)"] Data --> D3["SyncManager.js"] Contexts --> Auth["AuthContext.jsx"] Contexts --> Theme["ThemeContext.jsx"] style Root fill:#f1f5f9,stroke:#64748b,color:#0f172a style Data fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a

Conclusion

Migrating from Native to Web isn't about rewriting your app; it's about translating your intent. By creating strict boundaries around the Data Layer and enforcing a rigid styling strategy via AI prompts, we successfully ported the entire Word Genie 6.0 ecosystem—including its complex viral and monetization loops—in days, not months.