April 3, 2026·10 min read
WebViews and postMessage: treat the page as untrusted
If you control both sides, it’s tempting to skip validation. Don’t. WebViews load remote content, marketing swaps tags, and XSS in the web layer becomes RN bridge weirdness fast. Assume the page is hostile; your bridge is a security boundary.
Origin and schema
Whitelist origins and message shapes. Reject unknown event types. Version your message protocol.
Never eval arbitrary strings from the web.
Injection
Injected JS runs in the page context—keep it minimal. Prefer `postMessage` bridges with explicit contracts.
Handle navigation requests to block phishing or accidental store links.
Cookies and storage
Third-party cookies are a moving target. If auth lives in WebView, plan migration to native session or system browser flows (ASWebAuthenticationSession / Custom Tabs) when possible.
Clear WebView storage on logout.
Incident-style questions to ask
What happens if an attacker posts a crafted message before your web team deploys a fix? Can users navigate to arbitrary URLs inside the WebView? Is mixed content allowed? Answers belong in a short threat model doc next to the bridge code.
Prefer OAuth-style flows in the system browser for login when you can—fewer cookies to fight, clearer security story.
Threat modeling WebViews
Treat loaded pages as untrusted—even if you control them today, CDNs, tag managers, and future edits change that. XSS in web becomes arbitrary messages to RN if bridges are permissive. Define allowed origins and message schemas strictly.
Message protocol design
Version your protocol; reject unknown versions explicitly. Use typed payloads; validate shapes at runtime. Never eval strings from the web. Log parse failures with safe detail—no secrets. Rate-limit messages if needed to prevent abuse loops.
Injection and navigation control
Minimize injected JavaScript; prefer `postMessage` contracts. Intercept navigation requests to block unwanted URLs or app-store phishing. Handle file downloads carefully—malicious files are a vector. Clear cookies and storage on logout.
Auth patterns
Prefer system browsers or ASWebAuthenticationSession/Custom Tabs for OAuth over embedding login solely in WebView when possible—cookies are fragile and user trust is higher in system UI. If you must use WebView auth, plan token lifecycles and rotation carefully.
Testing and monitoring
Fuzz message inputs in tests. Monitor for unexpected origins in production logs—could indicate injection attempts. Pen-test bridges periodically—attackers target hybrid apps. Keep WebView engines updated via platform upgrades.
Performance and memory
WebViews are heavy—lazy load when possible, destroy on unmount, and limit concurrent instances. Profile memory on Android low-RAM devices. Disable unnecessary JavaScript features if you control pages.
Incident response
If a bridge vulnerability is found, ship hotfixes fast—feature-flag WebView features off if needed. Communicate with security stakeholders. Postmortem: add static analysis or schema validation to prevent recurrence.
Shipping and reliability habits (1)
Expo SDK upgrades are integration projects: `expo doctor`, aligned community packages, regenerated native projects, and device smoke tests for camera, push, and IAP. Freeze unrelated native refactors during the upgrade window and keep rollback paths hot. Document surprises for the next upgrade while memory is fresh.
Hermes versus JSC is not a lifestyle choice—profile your app. Hermes usually wins on startup; some libraries still assume JSC quirks. Engine toggles are not substitutes for fixing quadratic renders in your own code. Upgrade notes matter: Intl support and debugging tooling evolve.
Platform differences worth rehearsing (2)
Design tokens and semantic colors make dark mode and rebrands feasible. Mixing three styling systems doubles migration cost—pick a primary approach and draw boundaries. Runtime CSS-in-JS can cost frame time on hot screens—profile before adopting wholesale.
ScrollView versus FlatList is a data-volume question. Small static content belongs in ScrollView; long feeds belong in virtualized lists. Nested scrollables need explicit height contracts—redesign beats fighting physics. Document intentional choices so future refactors do not ‘optimize’ blindly.
Security, privacy, and data handling (3)
Shipping React Native features is less about any single API and more about the system around it: typed boundaries, predictable navigation, and telemetry that tells you what broke in production. Prefer boring, explicit modules over clever metaprogramming that the next hire cannot grep. When platform vendors change behavior in point releases, your defense is automated smoke tests on real devices and a short internal changelog of native assumptions you rely on.
Performance work should start with measurement, not instinct. Watch JS thread versus UI thread separately; they bottleneck differently. Lists, images, and animations dominate most regressions—optimize those before micro-optimizing pure functions. Hermes, JSC, and bridge internals evolve; re-profile after every major upgrade instead of trusting last year’s numbers. Battery and thermal throttling on mid devices reveal issues flagship phones hide.
Performance and measurement discipline (4)
Native modules are product decisions disguised as engineering tasks. You inherit Xcode and Gradle upgrades, store review scrutiny, and security obligations. Prefer maintained Expo modules and config plugins before writing JNI or Swift glue from scratch. When you must go native, budget pairing time with platform specialists and write runbooks for on-call—crashes in native code bypass many JS safeguards.
Deep links are a cross-team system: marketing URLs, hosted association files, entitlements, router params, and analytics query preservation. Debug with structured logging of raw URLs (scrub secrets) and reproduce cold-start races with auth hydration. Staging and production should be obviously separated—accidentally opening prod from a QA link erodes trust and pollutes data.
Team process and long-term maintenance (5)
WebViews are untrusted browsers inside your app. Validate `postMessage` payloads, lock navigation to expected hosts, and prefer system-browser auth flows when OAuth security demands it. Third-party JavaScript can change without your deploy—treat XSS in web as bridge compromise risk. Clear storage on logout and rate-limit message handlers.
E2E tests should protect revenue paths, not every permutation. Stable selectors (`testID`) beat text that marketing rewrites weekly. Flake management is a feature: quarantine, fix root causes, and keep smoke suites green on CI devices. Five reliable tests beat fifty flaky ones that everyone ignores.
Shipping and reliability habits (6)
Splash screens and launch gates should reflect honest readiness: fonts, theme, session, critical remote config—not every SDK under the sun. Infinite splash is worse than a slightly longer branded hold. Match native and JS background colors to avoid flashes; respect reduced motion for animations.
JWT and session refresh flows need single-flight refresh, clear logout semantics, and secure storage for refresh tokens when appropriate. Parallel 401s should not stampede refresh endpoints. Clock skew and biometrics policies belong in explicit product decisions, not accidental implementation details.
Platform differences worth rehearsing (7)
Reanimated and gesture libraries earn their place when profiling proves UI-thread work and your team can maintain native upgrades. Worklets have constraints—read errors carefully. Respect reduced motion and test Android timing differences—identical JS does not guarantee identical feel.
Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.
Security, privacy, and data handling (8)
Metro cache issues masquerade as logic bugs. Establish a documented reset ladder: dev server restart, cache flags, Watchman, derived data, then dependency reinstalls. Compare platforms when only one breaks—native steps diverge. Keep CI caches deterministic with lockfiles and pinned toolchains.
Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.
Performance and measurement discipline (9)
Security and privacy expectations move faster than roadmaps. Treat analytics, crash, and attribution SDKs as part of your threat model: initialize them deliberately, document data flows, and verify ‘off’ truly stops network calls. Client-side secrets are public secrets—anything shipped in an APK or IPA should be assumed extractable. Pair mobile changes with backend policies so authorization remains consistent across platforms.
Accessibility is compatibility. Labels, focus order, and dynamic type are not polish—they determine whether users can complete tasks at all. Test with VoiceOver and TalkBack on hardware; simulators miss focus bugs. When designs prioritize minimalism, negotiate text alternatives for icon-only controls. Accessibility regressions often follow navigation redesigns—add checklist items to those PRs specifically.
Team process and long-term maintenance (10)
Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.
Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.
Shipping and reliability habits (11)
JWT and session refresh flows need single-flight refresh, clear logout semantics, and secure storage for refresh tokens when appropriate. Parallel 401s should not stampede refresh endpoints. Clock skew and biometrics policies belong in explicit product decisions, not accidental implementation details.
In-app purchases require server validation, restore flows, and support tooling that respects privacy. Sandbox quirks are normal—budget QA time. Subscriptions interact with family sharing, regional pricing, and refunds; engineering must stay aligned with finance and legal narratives users see in receipts.
Platform differences worth rehearsing (12)
Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.
Testing onboarding changes with funnel metrics beats debating opinions. Segment by acquisition channel and platform; back behavior differs. Skip paths must be genuine—dark patterns may win short metrics and destroy brand trust. Localization length tests prevent clipped CTAs in verbose languages.
Security, privacy, and data handling (13)
Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.
Project structure should make ownership obvious: routes as backbone, feature folders for product areas, thin screens, and shared infrastructure that is deliberately named. Refactor in vertical slices with device-tested releases—big-bang rewrites without tests are how teams lose weeks.
Performance and measurement discipline (14)
Accessibility is compatibility. Labels, focus order, and dynamic type are not polish—they determine whether users can complete tasks at all. Test with VoiceOver and TalkBack on hardware; simulators miss focus bugs. When designs prioritize minimalism, negotiate text alternatives for icon-only controls. Accessibility regressions often follow navigation redesigns—add checklist items to those PRs specifically.
Internationalization is a product feature, not a string swap. Plural rules, RTL layout, and locale-aware formatting change behavior—not just copy length. Pseudolocale helps find clipping early, but real Arabic and German QA catches nuance. Avoid concatenating translated fragments; context matters. Document glossary terms so translators do not invent inconsistent product names.
Team process and long-term maintenance (15)
Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.
Error boundaries catch render failures, not native crashes or async mistakes. Pair them with platform crash reporting and structured client logs. Fallback UI should include build identifiers and humane copy—never raw stack traces for end users. Test fallbacks with screen readers; a broken error screen is still broken UX.
Shipping and reliability habits (16)
Keyboard and form UX separate polished apps from ‘works on desktop simulators.’ Platform differences in soft input modes matter; test smallest phones and Android gesture navigation. Primary actions must remain reachable when the keyboard is visible—scroll containers and keyboard controllers exist because this problem is universal.
Type-safe navigation pays off when routes multiply. Keep param lists near navigators, validate external URLs, and avoid serializing non-JSON-safe values through params. Renaming routes is a cross-cutting change—update analytics, push payloads, and E2E selectors in the same release train.