Bridging Web and Mobile: Sharing Logic Between React and React Native
Betterment needed a web dashboard and a React Native app to agree on everything. Here's the shared-logic structure that survived a hackathon.

At Build for Pakistan, our civic-reporting app Betterment had two faces: a React Native app for residents reporting issues, and a web surface for review and moderation. Two renderers, one truth — validation rules, the API client, and the report domain model had to be identical on both, or the weekend would dissolve into reconciliation bugs.
The rule: share everything except pixels
We organized the repo as three layers: a platform-agnostic core package, and two thin app and web shells. Core holds the API client, validation, i18n dictionaries (Urdu first) and the domain types. The shells hold navigation and UI. The discipline that made it work: core never imports from React or React Native.
// core/report.ts — runs identically on web and native
export function validateReport(r: ReportDraft) {
const errors: FieldError[] = [];
if (!r.title.trim()) errors.push(field("title", "required"));
if (r.category === "sanitation" && !r.photoUri)
errors.push(field("photo", "required_for_category"));
return { ok: errors.length === 0, errors };
}The seams that earned their keep
- One validation, zero drift. The web form and the native form rendered different UIs but failed identically — a rule changed once, in core.
- An isomorphic API client.
fetchworks in both worlds; only the auth token storage needed a two-line platform adapter each. - Urdu strings as data. i18n dictionaries lived in core; web picked RTL-aware layouts, native picked RTL-aware navigation — same words, same tone.
- Offline tolerance in core. The queue-and-retry logic for flaky mobile networks was written once and reused by the web shell's optimistic submissions.
Where it got hard
Dates, links and images each wanted platform treatment — the moment logic touched a rendered artifact, it left core. The boundary is learnable: if it produces pixels or interacts with native modules, it lives in the shell. Everything else — the rules, the truth, the boring correctness — ships once and runs everywhere.