Building a Browser-Based React Native Debug Lab from Scratch
Architecture, sandbox design, and the tradeoffs behind a reactLabs-style platform for mobile developers.

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
The problem we wanted to solve
React Native hiring and upskilling both suffer from the same friction:
Environment cost — Xcode, Android Studio, Metro, pods, Gradle. Fine for day-to-day dev. Overkill for a 20-minute “find the bug” exercise.
Sharing cost — Sending someone a Snack link works, but there’s no structured bug report → fix → verify loop baked in.
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 inuseCallback,useRoutevsuseNavigation, 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/10A four-panel workspace per challenge:
Problem — bug report, symptoms, tasks, optional hint
Editor — Monaco, RN-aware typing, themes, autosave
Preview — live
react-native-webrenderTests — 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
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):
| 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
persist→localStoragefor progress and editor prefsMonaco 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
Strip imports — learners write familiar
import { View } from 'react-native'; we remove import lines because the sandbox injects bindings manually.Transpile JSX —
@babel/standalonewithpreset-react(classic runtime).Evaluate —
new Function(...)with an explicit allowlist of injected symbols:React hooks:
useState,useEffect,useCallback,useContext,useMemo,useRef,memo,createContextRN primitives:
View,Text,TextInput,FlatList,ScrollView,StyleSheet, etc.AsyncStorage (in-memory web implementation)
React Navigation —
NavigationContainer, stack navigator,useNavigation,useRouteOptional Platform mock when simulating iOS/Android
Render —
createRootinto a phone-sized frame inLocalPreview.
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 progress —
completed,solutionViewed,userCodedraftEditor 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, dynamicimport,require, prototype tampering, etc.JSX must transpile cleanly
Code must reference an
Appcomponent (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.txtOpen Graph image + favicon generated via Next.js
ImageResponseJSON-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_viewedpreview_run,snack_opened,code_resettour_completed/tour_skippedsubmission_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
One sandbox, two consumers — preview and tests share
evalUserCode(). If tests pass, preview behavior is trustworthy.Declarative test cases in data — new challenges don’t require new test harness code if they fit existing types.
Broken starter code — lowers blank-page anxiety; focuses attention on the diff that matters.
localStorage progress — zero ops burden for v1; good enough for self-paced learning.
Chai-style narrative — bug report + symptoms beats “fix line 42.”
What we gave up
Not a real native runtime —
react-native-webdoesn’t catch every platform quirk. Hence the Snack escape hatch.No server-side code execution — can’t run RN on Node for “true” native APIs in tests.
Desktop only — mobile web IDE was out of scope.
new Functionsandbox — 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.Webpack for production build — Next 16 + Turbopack for dev, Webpack for
builddue 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-lightwhen 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.


