Skip to main content

React Native for Web Developers: A Translation Guide

· 24 min read
Pere Pages
Software Engineer
A browser window on the left connected by a bridge of dotted nodes to a smartphone on the right, illustrating the translation from web development to React Native

React Native runs the same React you already know — the components, hooks, and data layer come with you. This is a deep dive into the other part: the mobile-specific tools, application programming interfaces (APIs), and constraints, each mapped from the web equivalent you already reach for.

You have opinions about bundlers, you've argued about state management, and you write tests you actually trust. Most of that carries straight over: React Native (RN) is the same React — same components, same hooks, same JavaScript Extension (JSX) syntax, same TypeScript. useState is still useState; useEffect still fires too often when you forget the dependency array. Call it roughly 80% of your knowledge transferring unchanged.

The other 20% is where the interesting differences live, and that's what this post is about. Rather than teach React Native from zero, I walk through the web technologies you already use and map each one to its mobile equivalent — and, just as usefully, tell you when there is no equivalent so you don't waste an afternoon hunting for one.

This is deliberately the React-Native-only deep dive: one lane, with the code and detail to actually build something. If you want the broad cross-framework comparison — how these same concepts look in Swift/SwiftUI, Kotlin/Jetpack Compose, and Flutter alongside React Native — read the companion piece first: The Web Developer's Map to Native iOS & Android. Where that map surveys the whole territory in one table, this post drills into the React Native corner of it. I'll point back to the map whenever a topic is fully covered there, and spend the space here on the parts it doesn't.

A note on timing: this is written against the 2026 stack — React Native 0.85, Expo SDK 56, and React 19.2 — where the New Architecture (the modern rendering internals: Fabric, TurboModules, and the JavaScript Interface, JSI) is the assumed-stable default. If you're reading this much later, the version numbers will have moved, but the mental models below are stable.

The one-glance cheat sheet

Every mapping in this post, in one table. The sections that follow expand each row with the why, a code snippet, and the gotchas.

WebReact Native (mobile)Notes
Vite / webpackMetroThe bundler. You rarely configure it directly.
Create React App / Vite scaffoldcreate-expo-appExpo is "the Next.js of mobile."
Next.js App RouterExpo RouterFile-based routing, same idea.
React RouterReact NavigationThe imperative alternative to Expo Router.
<div> / <span> / <p><View> / <Text>No raw text outside <Text>.
<img><Image> / expo-image
<button> / onClick<Pressable> / onPressNo hover on touch devices.
CSS / Sass / CSS ModulesStyleSheet.createJS objects, camelCase, no cascade.
TailwindNativeWind / UniwindReal Tailwind classes on native.
fetch / Axiosfetch / AxiosIdentical.
TanStack Query / SWRTanStack Query / SWRIdentical.
Redux / Zustand / ContextRedux / Zustand / ContextIdentical.
localStorageAsyncStorage / MMKVAsync by default.
Cookies / secure storageexpo-secure-storeBacked by Keychain / Keystore.
React Hook FormReact Hook FormWorks, but inputs differ.
Virtualized lists (react-window)FlatList / FlashListRecycling is mandatory, not optional.
CSS transitions / animationsReanimatedNo CSS animations exist.
Jest + React Testing LibraryJest + React Native Testing LibrarySame API, RN renderer.
Cypress / PlaywrightMaestro / DetoxEnd-to-end on a simulator/device.
.env / import.meta.envprocess.env.EXPO_PUBLIC_*Baked into the bundle — not secret.
Browser DevToolsReact Native DevToolsChrome-DevTools-based.
Netlify / Vercel deployEAS Build + Submit + UpdatePlus an app store review step.
Media queriesuseWindowDimensionsNo CSS media queries.
The DOM(nothing)There is no DOM. This is the big one.
App store review(nothing)No web equivalent. Plan for it.

Before the details, hold this shape in your head — the whole post sorts into two piles:

Project setup and the toolchain

On the web you scaffold with Vite or a framework command-line interface (CLI). On mobile the community's clear default in 2026 is Expo — often described as "the Next.js of mobile," a comparison the companion map unpacks. The short version: Expo isn't a competing framework, it's the batteries-included layer on top of React Native that handles the native build configuration you don't want to think about.

npx create-expo-app@latest my-app
cd my-app
npx expo start # opens a dev server + QR code to run on your phone

That command starts a dev server and prints a Quick Response (QR) code you scan to launch the app on your own phone. A few pieces to place in your existing mental map:

  • Metro is the bundler — the Vite/webpack of this world. It handles module resolution, transforms, and hot-reloading. Unlike the web, there's no thriving marketplace of interchangeable bundlers; Metro is the standard and you'll mostly leave it alone.
  • Fast Refresh is your hot module replacement (HMR). Save a file, see it update on the device, keep component state — just like on the web.
  • Expo Application Services (EAS) is the cloud build-and-deploy layer, the closest thing to Vercel/Netlify (covered under shipping, below). Crucially, EAS Build produces an iOS build without you owning a Mac, which removes the historical biggest blocker for solo developers.

There's a "bare" workflow too (no Expo, raw native projects), but in 2026 the managed Expo workflow covers nearly everything a typical app needs — start managed and drop to native config only when you hit a real wall.

There is no DOM

This is the single biggest adjustment, so it goes first. On the web, everything bottoms out in HyperText Markup Language (HTML) elements and the Document Object Model (DOM). In React Native, there is no DOM and no HTML. You render a small set of core components that map to real native views (UIView on iOS, android.view on Android):

You reach for on the webYou write instead
<div><View>
<span>, <p>, <h1>, any text<Text>
<img><Image>
<button>, <a onClick><Pressable>
<input><TextInput>
scroll container<ScrollView>
import { View, Text, Pressable } from 'react-native';

export function Greeting({ name }: { name: string }) {
return (
<View>
<Text>Hello, {name}</Text>
<Pressable onPress={() => console.log('tapped')}>
<Text>Tap me</Text>
</Pressable>
</View>
);
}

Two rules trip up every web developer on day one:

  1. All text must live inside <Text>. You cannot drop a bare string into a <View> the way you would into a <div>. <View>Hello</View> throws.
  2. Events are onPress, not onClick. And there is no :hover — touchscreens have no hover state. Interaction is press-based (onPressIn, onPressOut, onLongPress).

Think of the core components as your new "HTML," except it's a deliberately tiny vocabulary and everything else you build by composing these.

Styling: no CSS, no cascade, no stylesheet files

There are no .css, .scss, or .module.css files. Styles are plain JavaScript objects, and the canonical API is StyleSheet.create:

import { View, Text, StyleSheet } from 'react-native';

function Card() {
return (
<View style={styles.card}>
<Text style={styles.title}>Title</Text>
</View>
);
}

const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 8,
backgroundColor: '#fff',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
});

Coming from CSS Modules this feels familiar — locally-scoped style objects, no global namespace to pollute. But the differences matter:

  • Properties are camelCased (backgroundColor, not background-color), because they're JavaScript keys.
  • There is no cascade and no inheritance. A color set on a parent <View> does not flow down to a child <Text>. Every component styles itself. This is jarring at first and liberating later — no specificity wars, no !important.
  • No media queries. For responsive layouts you read the current size at runtime with the useWindowDimensions() hook and branch in JavaScript, or use a library that abstracts it.
  • No units. Numbers are density-independent pixels; you write padding: 16, not 16px. Font sizes are unitless too.
  • No CSS Grid. You lay everything out with flexbox (though core styling is steadily gaining web-compatible features like box shadows, gradients, and filters).
The mental shift is that appearance is local and per-component — every element carries its own style, and nothing cascades in from a parent or a global sheet.

If you love Tailwind

You don't have to give it up. NativeWind compiles real Tailwind classes down to StyleSheet objects at build time, so className="flex-1 items-center justify-center bg-white" works on native:

import { View, Text } from 'react-native';

function Card() {
return (
<View className="p-4 rounded-lg bg-white">
<Text className="text-xl font-bold">Title</Text>
</View>
);
}

The 2026 styling landscape is lively; the main options break down like this:

OptionStyleSetupBest when
StyleSheet (built-in)JS style objectsNoneYou want zero dependencies and full control.
NativeWindTailwind utility classesLightYou have Tailwind muscle memory to reuse.
UniwindTailwind utility classesLightYou want NativeWind's model, faster (from the Unistyles team).
UnistylesTyped, theme-first APIMediumYou want shared tokens and variants, no utility strings.
TamaguiTyped components + tokensHeavierYou want an optimizing compiler and a full component kit.

Plain StyleSheet is perfectly fine — it's fast and has zero setup; the libraries mostly earn their keep as your codebase grows and you want shared tokens and variants. (styled-components/native also exists if you're attached to that pattern.)

Layout: flexbox everywhere, plus safe areas

You already know flexbox from the web, and it transfers almost perfectly — with two defaults flipped: flexDirection defaults to column (mobile screens are tall) rather than row, and box-sizing is always border-box, so no reset is needed. The companion map calls out the column default as the classic web-developer stumble; beyond that, justifyContent, alignItems, flex, and gap behave exactly as you expect. Prefer percentage- and flex-based sizing over hardcoded pixels — you're targeting a huge range of screen sizes.

The thing with no web equivalent: safe areas

Phones have notches, punch-hole cameras, rounded corners, and home-indicator bars, and your content has to avoid all of them. There's no browser equivalent to this problem; it's handled by safe area insets:

import { SafeAreaView } from 'react-native-safe-area-context';

export function Screen() {
return <SafeAreaView style={{ flex: 1 }}>{/* content */}</SafeAreaView>;
}

Wrap your screens (or read the insets directly with useSafeAreaInsets()) so your header doesn't hide under the notch. It's a small thing that instantly separates an app that "feels native" from one that doesn't.

Routing and navigation

Two live options map neatly onto tools you know. Expo Router is file-based routing — it's the Next.js App Router of mobile, the analogy the companion map draws out. A file at app/profile/[id].tsx becomes a route, layouts nest via _layout.tsx, and every screen gets a real internal path, so deep linking (opening myapp://profile/42 from outside the app) mostly just works:

app/
_layout.tsx # root layout (like a Next.js layout)
index.tsx # "/"
profile/
[id].tsx # "/profile/:id"
import { Link, useRouter } from 'expo-router';

// Declarative
<Link href="/profile/42">Go to profile</Link>;

// Imperative
const router = useRouter();
router.push('/profile/42');

React Navigation is the older, imperative, config-driven library — closer in spirit to React Router's programmatic API. Expo Router is built on top of it, so learning one helps with the other.

The concept with no direct web analogue: mobile navigation has native transition primitives — a stack navigator (push/pop screens with the platform's slide and back-swipe gesture), tabs (a bottom tab bar), and drawers. On the web you fake these with CSS and route state; on mobile they're first-class and animate natively.

Data fetching: basically identical

Networking barely changes. fetch is available globally and works exactly as it does in the browser, Axios works, and TanStack Query and SWR work identically — same hooks, same cache semantics. This is one of the areas you copy straight across (the companion map makes the same point about React Query moving over unchanged).

import { useQuery } from '@tanstack/react-query';

function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`https://api.example.com/users/${id}`).then((r) => r.json()),
});
}

The differences are environmental, not API-level, and they're where the mobile depth actually lives:

  • No Cross-Origin Resource Sharing (CORS). Native apps aren't browsers, so the cross-origin restrictions you fight on the web simply don't apply.
  • Connectivity is unreliable, and you must design for it. Mobile users go through tunnels. The professional pattern is to cache aggressively, show cached data on launch, queue mutations for retry, and give non-blocking feedback (a toast) rather than a blocking error screen. The @react-native-community/netinfo library gives you real-time connection status to build on.
  • Debugging network calls happens in React Native DevTools rather than a browser Network tab.

State management: identical

Nothing to relearn — this is the shortest section for a reason. Redux (and Redux Toolkit, RTK), Zustand, Jotai, and Context all work exactly as they do on the web, because they're plain JavaScript with no DOM dependency. Your existing store, selectors, and middleware move over unchanged; if you use RTK Query, it comes along too. The companion map lists the rung-for-rung equivalents against the native platforms — for React Native specifically, there genuinely isn't a catch.

Local storage and secrets

localStorage and sessionStorage don't exist. There are three replacements, and picking the right one matters — especially for the last one:

ToolWhat it isReach for it when
AsyncStorageAsync, string-based key-value storeYou want the general-purpose localStorage equivalent.
MMKVFast, synchronous key-value store (memory-mapped)AsyncStorage's async API is awkward, or performance matters.
expo-secure-storeOS-encrypted store (iOS Keychain / Android Keystore)Anything sensitive — tokens, credentials.

The direct localStorage replacement is AsyncStorage, with one important difference: it's asynchronous.

import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.setItem('theme', 'dark');
const theme = await AsyncStorage.getItem('theme'); // Promise<string | null>

The one rule to internalize: authentication tokens go in expo-secure-store, never in AsyncStorage — it's backed by real operating-system-level encryption (the iOS Keychain and Android Keystore), which is better than anything the browser offers and, as the companion map notes, has no clean web analogue. Cookies are largely a non-thing in native apps; authentication is token-based — store the token in the secure store and attach it to requests.

Forms and inputs

React Hook Form works, so if that's your web stack, keep it. What changes is the primitives underneath:

  • No <form> element and no native form submission. You wire a <Pressable> or button onPress to your submit handler.
  • No HTML input types (type="email", type="number"). Instead, <TextInput> takes a keyboardType prop ("email-address", "numeric", "phone-pad") that controls which on-screen keyboard appears — a mobile nicety with no web equivalent.
  • No built-in HTML5 validation (required, pattern). Validation is all in JavaScript — pair React Hook Form with Zod, exactly as you would on the web.
import { TextInput } from 'react-native';

<TextInput
keyboardType="email-address"
autoCapitalize="none"
placeholder="you@example.com"
value={email}
onChangeText={setEmail} // note: onChangeText gives you the string directly
/>

Note onChangeText hands you the value directly — no e.target.value dance. Two mobile-only concerns round it out: the on-screen keyboard covers the bottom of the screen (wrap forms in KeyboardAvoidingView), and dismissing the keyboard is something you handle explicitly.

Lists and performance

On the web you can .map() a few hundred rows into <div>s and mostly get away with it, reaching for react-window only when lists get large. On mobile, virtualization isn't optional — long non-recycled lists will jank and eat memory. The tools:

  • FlatList — the built-in virtualized list. It renders only what's on screen.
  • FlashList (from Shopify) — a faster, recycling list; the go-to for large or complex lists in 2026.
import { FlatList, Text } from 'react-native';

<FlatList
data={users}
keyExtractor={(u) => u.id}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>;

The mental shift: never map a big array into raw components — treat FlatList/FlashList as the default for any collection that could grow, and give it a stable keyExtractor (the key prop's cousin).

Animations

There are no CSS transitions or CSS animations — no transition: all 0.2s, no @keyframes. Animation is done in JavaScript, and the modern standard is Reanimated.

The important idea: Reanimated runs animations on the user-interface (UI) thread via "worklets," so they stay at 60/120 frames per second (fps) even when your JavaScript thread is busy — something CSS gives you for free on the web but which requires deliberate tooling here. There's also a simpler built-in Animated application programming interface for basic cases, and libraries like Moti that wrap Reanimated in a friendlier, declarative syntax.

If your web instinct is "just add a CSS transition," the mobile translation is "reach for Reanimated." Budget a little learning time; it's the one area where the replacement is more involved than the original.

Testing, part 1: unit and component tests

Good news for anyone who values testing: the pyramid is the same, and the top library is a near-clone of what you already use. Unit and component tests run on Jest (via the jest-expo preset, which sets everything up for you). The component layer uses React Native Testing Library (RNTL) — the direct sibling of React Testing Library (RTL), with the same philosophy and nearly the same API. render, screen, getByRole, getByText, fireEvent/userEvent, and the "query by what the user sees, not implementation details" mindset all carry over.

import { render, screen, fireEvent } from '@testing-library/react-native';
import { Counter } from './Counter';

test('increments on press', () => {
render(<Counter />);
fireEvent.press(screen.getByText('Increment'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});

The queries even nudge you toward accessibility: preferring getByRole/getByText over test IDs means your components have to be queryable by role, which usually means they're accessible too — and those accessibility labels then double as stable selectors for end-to-end tests.

An honest note if you're a Vitest person (I am too): the web ecosystem has largely moved to Vitest, and you can run it in React Native, but Jest is still the default in RN and it's not close. The presets, the mocks for native modules, the community examples, and the continuous-integration recipes are all Jest-first. For a mobile project in 2026, swimming against that current usually isn't worth it — use Jest here even if you use Vitest everywhere else. The RNTL API is identical regardless of runner, so your testing skills transfer even though the runner changes.

Testing, part 2: end-to-end

Your web end-to-end (E2E) tools — Cypress and Playwright — are browser drivers and don't apply to native apps (Playwright only enters the picture if you ship an Expo web build or have WebView-heavy screens). The mobile equivalents drive a real app on a simulator or device:

ToolTest styleSetup / flakinessWhere it fits
MaestroDeclarative YAMLLightest, least flakyThe default first reach in 2026.
DetoxGray-box, JS + JestMore setupTightest RN integration when you need it.
AppiumCross-platform driverHeaviest, flakiestMaximum device coverage; ubiquitous.

easiest / most stable middling hardest / flakiest

Maestro is the one I'd reach for first (the companion map reaches the same verdict over Detox). Its tests are declarative YAML — a human-readable configuration format that reads like plain English ("tap this, type that, assert this is visible") — it needs no native code changes, and it has a watch mode that reruns on save, the E2E equivalent of Jest's watch mode:

appId: com.example.myapp
---
- launchApp
- tapOn: "Log in"
- inputText: "you@example.com"
- assertVisible: "Welcome"

Detox is a "gray-box" framework (it syncs with the app's internals to reduce flakiness) built for React Native, using Jest as the runner — more setup, more power, tightest RN integration, historically the default but now often passed over for Maestro's simplicity. Appium is the cross-platform workhorse for maximum device coverage: heavier and flakier, but ubiquitous. The testing pyramid is the same as the web (roughly 70% unit, 20% component, 10% end-to-end) — you're just swapping the top layer's tooling.

Environment variables and config

The .env concept survives, with a mobile-specific footgun. Public values are exposed via the EXPO_PUBLIC_ prefix:

# .env
EXPO_PUBLIC_API_URL=https://api.example.com
const url = process.env.EXPO_PUBLIC_API_URL;

The footgun: anything with the EXPO_PUBLIC_ prefix is embedded into the app bundle at build time and can be extracted by anyone who downloads your app. This is the same reality as import.meta.env.VITE_* on the web, but the stakes feel higher because people forget apps are shippable, inspectable artifacts. Never put secrets here. API keys that must stay private belong on a backend you control, with the app calling your server rather than the third party directly.

Debugging and dev tools

You lose the browser DevTools you live in, but you gain a close cousin. React Native DevTools is built on Chrome DevTools, so the console, breakpoints, and network inspector feel familiar; React DevTools (the component tree and profiler) is available too, and Fast Refresh gives you the same edit-save-see-it loop as web HMR.

What's genuinely different: you're inspecting an app running on a simulator, emulator, or physical device, not a browser tab. You'll spend time in the iOS Simulator and Android Emulator, and eventually on real hardware for the things simulators can't fake (camera, real network conditions, performance). There's no "view source" and no browser extensions.

Building, shipping, and the over-the-air superpower

Web deployment is "push to main, Vercel builds and ships in seconds." Mobile has more moving parts, and one delightful surprise. The pipeline runs through EAS:

  • EAS Build compiles your iOS and Android binaries in the cloud (no Mac required for iOS).
  • EAS Submit uploads them to the App Store and Play Store.
  • App store review — the part with no web equivalent: Apple and Google review your app before it goes live, which can take hours to days, and they can reject it. Plan releases around it.

The delightful surprise is EAS Update — over-the-air (OTA) updates. For changes that are pure JavaScript (most bug fixes and UI tweaks), you push an update directly to users' devices without going through app store review at all; anything touching native code still needs a full rebuild and a review. It's the closest thing mobile has to "instant redeploy," and it's arguably better than the web's, because users get the fix without doing anything. The companion map has a decision diagram for exactly when a change reaches users in minutes (JavaScript only) versus days (native code, full review) — the rule of thumb is: if you touched native code, you're rebuilding and re-reviewing.

The stuff with no web equivalent

Keep this list handy — these are the things you'll go looking for a web analogy to and won't find one. Most are covered in depth in the companion map's "no web equivalent" section; the React-Native-specific handles are:

  • Platform.OS branching. Sometimes iOS and Android genuinely need different code or styling. Platform.select({ ios: ..., android: ... }) is your tool.
  • Safe area insets (notches, home indicators) — the layout wrinkle covered earlier.
  • On-screen keyboard management — it covers your UI, and you handle it explicitly.
  • Scalable Vector Graphics (SVG) isn't built in — you need react-native-svg. (<Image> handles raster images; custom fonts come via expo-font.)
  • Native permissions, push notifications, and the app lifecycle — camera/location/notification permission prompts, push as a first-class capability, and foreground/background transitions observed via AppState. These are platform concerns rather than React Native ones; the map covers them across all four frameworks.
The through-line: you're not learning a new framework, you're learning a new render target for React you already know.

Where to start

If you want the fastest path from "web developer" to "app running on my phone":

  1. Run npx create-expo-app@latest and pick the Expo Router template — it'll feel like Next.js.
  2. Build one screen with View/Text/Pressable and StyleSheet (or NativeWind if you want Tailwind muscle memory). Sit with the "no DOM, no cascade" thing until it stops feeling weird — usually a day.
  3. Fetch some data with TanStack Query. Notice it's identical. Enjoy that.
  4. Add a FlatList. Internalize that lists are always virtualized.
  5. Write a component test with Jest + React Native Testing Library — your React Testing Library habits transfer directly.
  6. Run it on your actual phone via the Expo Go app or a development build.

That last moment — your React code running as a real app in your hand — is the one that makes it click. The components, the data layer, the state, and the testing philosophy all come with you; what you're really picking up is a mobile platform's constraints and superpowers, and those are worth having in your toolkit.

References