Skip to main content

Command Palette

Search for a command to run...

Building a Browser-Based React Native Debug Lab from Scratch

Architecture, sandbox design, and the tradeoffs behind a reactLabs-style platform for mobile developers.

Updated
14 min read
Building a Browser-Based React Native Debug Lab from Scratch
S
Sr Software Engineer - All things web and mobile

Live: https://rn-codelab.vercel.app
Source: https://github.com/Subramanyarao11/rn-codelab


A few days ago I finished every challenge on Chai reactLabs by ChaiCode — Hitesh Choudhary’s free, in-browser React debugging platform. The format clicked immediately: read a bug report, fix real code in an editor, see it run live, pass automated checks. No setup. No “clone this repo and fix the webpack config first.”

The only thing missing, for me, was React Native.

Most RN interview loops still expect you to reason about FlatList, KeyboardAvoidingView, navigation params, and layout bugs — but there’s no equivalent of reactLabs for mobile. Simulators are slow to share. Expo projects don’t fit a “5-minute debugging screen” as cleanly as a URL.

So I built RN Debug Labs: the same “Fix the Bug” loop, but for React Native, entirely in the browser.

This post is a thorough walkthrough of why we built it, what it does, and how the architecture works — including the tradeoffs we accepted and the ones we deliberately avoided.


Table of contents

  1. The problem we wanted to solve

  2. Design goals

  3. Inspiration: Chai reactLabs

  4. What RN Debug Labs is

  5. Architecture overview

  6. The workspace UX

  7. Running React Native in the browser

  8. The automated test runner

  9. Challenge content model

  10. State, progress, and drafts

  11. Security and community submissions

  12. Production readiness

  13. Tradeoffs and lessons learned

  14. What’s next

  15. Try it and contribute


The problem we wanted to solve

React Native hiring and upskilling both suffer from the same friction:

  1. Environment cost — Xcode, Android Studio, Metro, pods, Gradle. Fine for day-to-day dev. Overkill for a 20-minute “find the bug” exercise.

  2. Sharing cost — Sending someone a Snack link works, but there’s no structured bug report → fix → verify loop baked in.

  3. Signal quality — Many take-home tasks test greenfield feature work, not the kind of subtle bugs that show up in real codebases: wrong prop on FlatList, stale closure in useCallback, useRoute vs useNavigation, keyboard covering a button on iOS.

We wanted a platform where:

  • A candidate (or learner) opens a single URL

  • Reads a realistic bug report

  • Edits broken starter code — not a blank file

  • Sees a live preview update

  • Clicks Check and gets objective pass/fail

No install. Desktop browser only (the IDE layout needs width), but otherwise zero ceremony.


Design goals

These goals drove almost every architectural decision:

Goal Implication
Zero native toolchain react-native-web for preview + tests, not a device farm
Interview-realistic bugs Curated challenges from common RN foot-guns, not trivia
Honest feedback loop Automated tests, not “looks fine to me”
Respect the learner Hints available, solution viewable, progress saved locally
Open source & extensible Community can propose challenges; accepted authors get credit
Safe-enough sandbox User code runs client-side; block obvious escape hatches
Ship fast Next.js on Vercel, no backend database for v1

Inspiration: Chai reactLabs

Chai reactLabs proved the format works:

  • Narrative first — bug report, symptoms, tasks

  • Editor second — you’re fixing code, not writing an essay

  • Verification third — tests confirm the fix

RN Debug Labs is explicitly inspired by that product and philosophy. We credit ChaiCode on the home page and in the README. The RN version is not a clone — the runtime, test harness, and challenge set are all mobile-specific — but the pedagogy is the same.

If reactLabs filters for “does this person actually understand React?”, RN Debug Labs aims to filter for “does this person understand React Native?”


What RN Debug Labs is

At a high level, the app is a Next.js 16 site with:

  • A home page — challenge grid, progress bar, contribute CTA

  • 10 core challenges at /problems/1/problems/10

  • A four-panel workspace per challenge:

    • Problem — bug report, symptoms, tasks, optional hint

    • Editor — Monaco, RN-aware typing, themes, autosave

    • Preview — live react-native-web render

    • Tests — run checks against your code

Optional: open the same code in Expo Snack in a new tab for real iOS/Android device testing.

Everything runs client-side except the optional GitHub issue API for community submissions.


Architecture overview

How RN Debug Labs works — simple view

The whole app in one sentence: you fix RN code in the browser → one engine runs your preview and tests → the site is hosted on Vercel.

Technical deep dive: Under the Hood

For developers who want the implementation story (still top-down, minimal arrows):

RN Debug Labs — Under the Hood (technical deep dive)
Step What happens
① App shell Next.js routes, Zustand persist, Monaco, resizable 4-panel layout
② Shared sandbox One evalUserCode() pipeline — Babel, allowlisted new Function, RN web
③ Two consumers Same component powers LocalPreview and TestPanel
④ Production Submit API + codeSandbox validation + optional GitHub + analytics

Stack summary:

  • Next.js 16 (App Router) + React 19 + TypeScript + Tailwind CSS

  • Zustand 5 with persistlocalStorage for progress and editor prefs

  • Monaco via @monaco-editor/react + custom RN type stubs

  • @babel/standalone to transpile JSX in the browser

  • react-native-web for preview and test rendering

  • @testing-library/dom for headless interaction tests

  • react-resizable-panels for the IDE layout

  • Framer Motion for polish (home page, onboarding tour, confetti on pass)

  • next-themes for light/dark mode

  • Vercel Analytics + Speed Insights in production


The workspace UX

The workspace is intentionally modeled after an IDE, not a tutorial page with a single code block.

Four resizable panels

We use react-resizable-panels with persisted autoSaveId keys so panel sizes survive reloads:

  • Main split: Problem + Editor (left) | Preview + Tests (right)

  • Each side splits vertically

This matches how engineers actually work: spec on one side, code below, output on the right.

Desktop-only gate

The layout breaks below ~1024px width. Rather than ship a compromised mobile layout, we show a MobileBlocker — honest messaging that this is a desktop lab. RN developers debugging layout issues on a phone-sized IDE would be fighting the tool, not the bug.

Onboarding tour

First-time visitors get a spotlight tour (sidebar, problem panel, editor, preview, tests, toolbar). Layout is computed with portals and DOM measurement so arrows point at real elements — including toolbar buttons that flex-wrap.

Toolbar actions

Per challenge:

  • Reset Code — back to broken starter

  • Show / Hide Hints

  • Show / Hide Solution — loads reference fix; completing tests with solution visible does not mark the challenge complete (you have to fix it yourself)

  • Prev / Next — navigate challenges

  • Run — refresh preview

  • Snack — open Expo Snack with current code

  • Theme toggle — light/dark for the chrome (editor has its own theme picker)

Challenge #7 (The Keyboard Hider) adds an iOS / Android / Web platform toggle and optional keyboard simulator overlay when KeyboardAvoidingView isn’t applied yet — so the bug is visible without a physical device.


Running React Native in the browser

This is the core technical bet.

Why not Expo Snack as the primary preview?

Snack is excellent for real devices. We still link to it. But for the default loop we wanted:

  • Instant updates as you type (debounced)

  • No iframe cross-origin messaging latency as the primary path

  • Same execution environment as the test runner (critical — tests must match preview)

Pipeline: user string → React component

  1. Strip imports — learners write familiar import { View } from 'react-native'; we remove import lines because the sandbox injects bindings manually.

  2. Transpile JSX@babel/standalone with preset-react (classic runtime).

  3. Evaluatenew Function(...) with an explicit allowlist of injected symbols:

    • React hooks: useState, useEffect, useCallback, useContext, useMemo, useRef, memo, createContext

    • RN primitives: View, Text, TextInput, FlatList, ScrollView, StyleSheet, etc.

    • AsyncStorage (in-memory web implementation)

    • React NavigationNavigationContainer, stack navigator, useNavigation, useRoute

    • Optional Platform mock when simulating iOS/Android

  4. RendercreateRoot into a phone-sized frame in LocalPreview.

Relevant code lives in lib/evalUserCode.ts.

Preview error handling

PreviewErrorBoundary catches render errors and surfaces readable messages. We special-case the classic useEffect(async () => ...) mistake with a hint — React 19 will crash on async effects, and interview candidates hit this constantly.

Debouncing and run keys

Preview re-runs are debounced (~400ms) on code changes. Toolbar Run bumps a refreshKey for immediate re-execution. Switching problems resets scroll, preview platform, and draft loading logic.


The automated test runner

Preview proves “it renders.” Tests prove “it fixes the bug.”

Test types

Each challenge defines declarative test cases in lib/problems.ts:

Type What it checks
dom_exists Element with testID is in the tree
dom_text Element contains expected text
interaction fireEvent press/type, then assert
no_crash Component mounts without throwing

Tests query the same react-native-web DOM (data-testid attributes) as the preview.

AsyncStorage seeding

For storage bugs, tests can seed AsyncStorage before mount via storageSeed on a test case — simulating “app reopened with saved data” without a real app lifecycle.

React act() environment

React 19 + Testing Library required explicitly setting globalThis.IS_REACT_ACT_ENVIRONMENT = true during test runs so state updates inside act() behave correctly. This was a real integration foot-gun; the test runner wraps mount/unmount in act().

Solution vs real completion

If Show Solution is active and all tests pass, we fire a solution_verified analytics event but do not persist completed: true. The product message is clear: verify the solution works, then hide it and fix the bug yourself.


Challenge content model

Each problem is a typed ProblemDefinition in lib/problems.ts:

interface ProblemDefinition {
  id: number
  slug: string
  title: string
  subtitle: string
  difficulty: 'beginner' | 'intermediate' | 'advanced'
  tags: string[]
  description: string        // bug report narrative
  symptoms: string[]
  yourTask: string[]
  hint: string
  howToTest?: string[]       // repro steps (e.g. keyboard bug)
  brokenCode: string
  solutionCode: string
  testCases: TestCase[]
  contributor?: ProblemContributor  // community challenges
  origin?: 'core' | 'community'
}

Core challenges merge with lib/community-problems/ at build time (currently empty, ready for accepted submissions).

The 10 core challenges

# Title Topic
1 The Frozen FlatList FlatList / keyExtractor / renderItem
2 The Ghost TextInput Controlled input / styling
3 The Invisible Style Flexbox / dimensions
4 The Runaway ScrollView ScrollView layout
5 The Stale AsyncStorage Persistence / effect deps
6 The Lost Navigation Params useRoute vs useNavigation
7 The Keyboard Hider KeyboardAvoidingView / iOS sim
8 The Phantom TouchableOpacity Pointer events / overlays
9 The Missing Memo React.memo / useCallback
10 The Confused Context Context provider value / hooks rules

Each is written as a real bug story, not “implement a todo app from scratch.”


State, progress, and drafts

We use Zustand with the persist middleware — no server database in v1.

Persisted shape:

  • Per-problem progresscompleted, solutionViewed, userCode draft

  • Editor settings — theme, font size, minimap, word wrap

  • UI — onboarding tour completed flag

Draft hygiene

We deliberately discard saved code when:

  • It matches the solution (anti-cheat for progress)

  • It contains crashy async useEffect patterns

Autosave debounces 500ms while typing. Showing solution temporarily disables persist so we don’t store the answer as “your work.”


Security and community submissions

User challenge code runs only in their browser — we never eval submissions on the server.

The contribute form (/contribute/submit) is different: untrusted strings hit our API. We hardened this in lib/codeSandbox.ts and lib/submission.ts:

Client + server validation

  • Field length limits (title, code blocks, etc.)

  • Plain text only in descriptions (no HTML)

  • Blocked patterns in code: eval, fetch, document, window, dynamic import, require, prototype tampering, etc.

  • JSX must transpile cleanly

  • Code must reference an App component (our sandbox entry convention)

  • Honeypot field for bots

GitHub issue integration

POST /api/submit-problem creates an issue with label problem-submission when configured:

GITHUB_TOKEN=<fine-grained PAT with Issues: Read/Write>
GITHUB_REPO=Subramanyarao11/rn-codelab

Without the token, the form falls back to opening a pre-filled GitHub issue in a new tab — still usable, slightly more friction.

Accepted community challenges get ContributorCredit on the problem panel.


Production readiness

Deployed at rn-codelab.vercel.app on Vercel.

SEO

  • Per-route metadata and canonical URLs

  • Dynamic titles for each challenge (Fix the Bug #3: The Invisible Style)

  • sitemap.xml, robots.txt

  • Open Graph image + favicon generated via Next.js ImageResponse

  • JSON-LD (WebSite + WebApplication) on the home page

Set NEXT_PUBLIC_SITE_URL=https://rn-codelab.vercel.app in Vercel for correct OG/sitemap URLs.

Analytics

Vercel Web Analytics for page views plus custom events:

  • challenge_complete, tests_checked, solution_viewed

  • preview_run, snack_opened, code_reset

  • tour_completed / tour_skipped

  • submission_sent, theme_changed

Speed Insights for Core Web Vitals — Monaco + RN web is heavier than a marketing page; we watch LCP and INP closely.

Theming

Light and dark mode via CSS semantic tokens (app-bg, app-fg, etc.) and next-themes. Default remains dark — matching the IDE aesthetic — with a sun/moon toggle in the header and workspace.


Tradeoffs and lessons learned

What worked

  1. One sandbox, two consumers — preview and tests share evalUserCode(). If tests pass, preview behavior is trustworthy.

  2. Declarative test cases in data — new challenges don’t require new test harness code if they fit existing types.

  3. Broken starter code — lowers blank-page anxiety; focuses attention on the diff that matters.

  4. localStorage progress — zero ops burden for v1; good enough for self-paced learning.

  5. Chai-style narrative — bug report + symptoms beats “fix line 42.”

What we gave up

  1. Not a real native runtimereact-native-web doesn’t catch every platform quirk. Hence the Snack escape hatch.

  2. No server-side code execution — can’t run RN on Node for “true” native APIs in tests.

  3. Desktop only — mobile web IDE was out of scope.

  4. new Function sandbox — pragmatic for a learning tool, not a multi-tenant untrusted-code platform. Submission validation is strict; in-editor code is the user’s own risk in their browser.

  5. Webpack for production build — Next 16 + Turbopack for dev, Webpack for build due to compatibility with some RN/babel paths.

Each of these is the kind of detail that separates a demo from something you’d actually send to a candidate.


What’s next

Short list of honest follow-ups:

  • [ ] Community challenges — first accepted PRs into lib/community-problems/

  • [ ] More test types — animations, network mocks (carefully), accessibility

  • [ ] Difficulty-based paths — beginner track vs interview track

  • [ ] Employer mode — shareable report links (would need backend)

  • [ ] Sync app theme with Monaco — optional rn-labs-light when switching to light mode

Contributions welcome — form or PR.


Try it and contribute

Play: https://rn-codelab.vercel.app

Star / fork: https://github.com/Subramanyarao11/rn-codelab

Propose a challenge: https://rn-codelab.vercel.app/contribute/submit

Inspired by: https://react.chaicode.com — thank you @ChaiCodeHQ and @Hiteshdotcom for keeping reactLabs free and raising the bar for interactive learning.


If you’re hiring RN developers: send them challenge #1 and #6 before the system design round. If you’re learning RN: do all ten, hide the solution until you’re stuck, and use Snack once when you want to feel the keyboard bug on a real phone.

Built with respect for the format ChaiCode pioneered — adapted for the mobile stack we work in every day.