Skip to main content

The Web Developer's Map to Native iOS & Android (2026 Edition)

· 20 min read
Pere Pages
Software Engineer
A translation map linking web development concepts to their native iOS and Android equivalents

You know the web. This post maps that knowledge onto native iOS and Android — the platforms, languages, tooling, and cross-platform options — so you can find your footing without starting from zero.

You already know how to build user interfaces (UIs). You think in components, state, and declarative rendering; you reach for a router, a data-fetching layer, a bundler, a test runner, and a continuous-integration (CI) pipeline without thinking. The good news: almost every one of those concepts has a direct counterpart on iOS and Android. The friction isn't the ideas — it's the vocabulary, the tooling, and a handful of places where the web analogy genuinely breaks.

This post is a translation dictionary. For each concern you care about on the web, it gives you the iOS-native equivalent (Swift/SwiftUI), the Android-native equivalent (Kotlin/Jetpack Compose), and the cross-platform option (React Native (RN)/Expo, with Flutter and Kotlin Multiplatform noted). Where there's no clean equivalent, it says so explicitly.

Throughout, "the web" means your current stack: Vite/Webpack, React + TypeScript, a router, React Query, Cascading Style Sheets (CSS)/Sass/CSS Modules, Vitest + Playwright, ESLint/Prettier, GitHub Actions.


Part 0 — The five mental-model shifts (read this first)

Before the tool-for-tool tables, internalise the differences that no mapping can paper over.

1. There is no Document Object Model (DOM), and no browser. On the web, the browser is a runtime you ship into. On mobile you ship a compiled binary that talks directly to the operating system (OS). There's no global document, no CSS cascade, no window. Your UI is a tree of native view objects (or, for Flutter, pixels drawn on a canvas). This is why styling is local and per-component, not global.

2. Declarative UI already won — your React instincts transfer directly. The single most important thing to know: SwiftUI, Jetpack Compose, React Native, and Flutter are all built on React's core idea. State drives the view; you describe what the UI should look like for a given state; the framework diffs and updates. If you understand useState and re-renders, you understand @State, remember, and setState. The counter example below is deliberately trivial because it should feel boringly familiar.

3. Two OSes means two of almost everything — unless you pick a cross-platform layer. Native means writing (and maintaining) the app twice: once in Swift, once in Kotlin. Cross-platform (React Native/Expo, Flutter, or shared logic via Kotlin Multiplatform) trades some native fidelity and platform-lag for one codebase. For a React developer, Expo is the natural on-ramp and is often described as "the Next.js of mobile."

4. You can't "just deploy." No git push to production. Native apps go through App Store / Play Store review, code signing, provisioning profiles, and staged rollouts. The one escape hatch is React Native's over-the-air (OTA) updates (Expo Application Services (EAS) Update), which can hot-swap the JavaScript layer without a store review — but never the native layer.

5. The OS is a hostile, resource-constrained environment. Apps get killed in the background. Permissions must be requested at runtime and can be revoked. There are real threads (not one event loop), a strict app lifecycle, memory pressure, and two entirely different design languages (Apple's Human Interface Guidelines (HIG) vs Google's Material). These have no web equivalent and are covered in Part 3.


Part 1 — The one code example you need

The same counter, five ways. Notice how little conceptual distance there is.

// React (web)
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// SwiftUI (iOS)
struct Counter: View {
@State private var count = 0
var body: some View {
Button("\(count)") { count += 1 }
}
}
// Jetpack Compose (Android)
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
// React Native (cross-platform)
function Counter() {
const [count, setCount] = useState(0);
return <Button title={`${count}`} onPress={() => setCount(count + 1)} />;
}
// Flutter (cross-platform) — inside a StatefulWidget
Widget build(BuildContext context) => TextButton(
onPressed: () => setState(() => count++),
child: Text('$count'),
);

@State / remember { mutableStateOf() } / setState are all useState. The rest of this document is mostly vocabulary on top of that shared foundation.


Part 2 — The tool-for-tool map

Each section is: a one-line framing, a comparison table, then the gotcha where the analogy leaks.

Language & type system

You're leaving TypeScript for two languages that are, honestly, nicer — statically typed, null-safe, compiled, no undefined footguns.

WebiOS (native)Android (native)Cross-platform
TypeScript / JSSwift 6.2Kotlin 2.xRN: TypeScript · Flutter: Dart
Structural typingNominal typing, value types (struct)Nominal typing, data classSame as web (RN)
null/undefinedOptionals (String?) enforced by compilerNullable types (String?) enforced by compilerRN keeps TS's looser model
tsc (erased at runtime)Types compile to machine codeTypes compile to JVM/native bytecode

Gotcha: Swift and Kotlin null-safety is enforced by the compiler at build time — not a lint rule you can // @ts-ignore away. Both are remarkably easy for a TS developer to read; the concepts (generics, extensions, first-class functions, sum types via enum/sealed class) all map. Dart is the outlier — a whole new language, which is part of Flutter's cost.

UI framework & rendering

WebiOS (native)Android (native)Cross-platform
React (modern)SwiftUI (declarative)Jetpack Compose (declarative)RN maps JSX → real native views
Legacy imperative DOMUIKit (older, imperative)Android Views/XML (older, imperative)Flutter draws its own pixels (Impeller/Skia)
Virtual DOM diffingSwiftUI diffs its view treeCompose recompositionRN's Fabric renderer (New Arch)
Renders to DOM nodesRenders to real UIViewsRenders to real android.view.ViewsRN → real native views · Flutter → canvas

Gotcha: The old imperative frameworks (UIKit, Android Views) still exist and underpin a huge amount of production code — treat them like jQuery-era knowledge you may need to read but shouldn't start new projects with. New native work in 2026 is SwiftUI + Compose. Note the deep divide in cross-platform: React Native renders real native components (your JSX becomes an actual UIView/View), whereas Flutter paints everything itself on a canvas — pixel-perfect and consistent, but not "native" widgets.

Components, styling & layout

The biggest adjustment. There is no stylesheet, no cascade, no className soup.

WebiOS (native)Android (native)Cross-platform
CSS / Sass / CSS ModulesView modifiers (.padding().foregroundStyle())Modifier chains (Modifier.padding().background())RN: StyleSheet objects · Flutter: widget props
Tailwind / utility classes(no direct equivalent)(no direct equivalent)NativeWind = Tailwind for RN
Flexbox / GridVStack / HStack / ZStack + SpacerColumn / Row / BoxRN: flexbox (via Yoga) · Flutter: Row/Column/Flex
theme / CSS variablesEnvironment, semantic colorsMaterialTheme, design tokensRN: theme context/libs · Flutter: ThemeData
@media queriesSize classes / GeometryReaderWindowSizeClassDimensions API / responsive libs

Gotchas:

  • Styling is local and typed. You attach appearance to each element via modifiers/props. No global stylesheet means no cascade bugs — but also no "one line in :root themes the whole app" unless you build that abstraction yourself.
  • Layout is flexbox-ish but not flexbox in SwiftUI/Compose. Stacks resemble flex containers; you'll learn Spacer, alignment, and frame/size modifiers instead of justify-content.
  • React Native is flexbox (it uses the Yoga engine) — but the default flexDirection is column, not row like the web. This trips up every web dev at least once.

State management

Your useStateContext → Zustand/Redux → React Query ladder has a rung-for-rung equivalent.

WebiOS (native)Android (native)Cross-platform
useState / useReducer@State, @Bindingremember { mutableStateOf() }RN: same hooks · Flutter: setState
Context API@Environment / @EnvironmentObjectCompositionLocalRN: Context · Flutter: InheritedWidget
Zustand / Redux / global store@Observable class (Observation) + @StateViewModel + StateFlowRN: Zustand/Redux/Jotai · Flutter: Riverpod/Bloc
React Query / SWR (server state)Custom @Observable + async loadersViewModel + repository + FlowRN: React Query works as-is · Flutter: dio + providers
Redux DevTools time-travel(no first-party equivalent)(no first-party equivalent)Flipper / RN DevTools

Gotcha: On iOS, @Observable (the Observation framework, iOS 17+) replaced the older ObservableObject + @Published pattern — expect to see both in tutorials; prefer @Observable for new code. Android's canonical answer is the ViewModel + StateFlow pairing (a ViewModel survives configuration changes like screen rotation — a concept with no web equivalent). Crucially, in React Native your entire web state stack — Zustand, Redux, React Query — runs unchanged, which is a major reason RN feels like home.

WebiOS (native)Android (native)Cross-platform
React Router / Next App RouterNavigationStack + typed pathsNavigation Compose / Navigation 3RN: Expo Router (file-based!) or React Navigation · Flutter: go_router
<Link> / router.push()NavigationLink / path appendnavController.navigate()Expo <Link> / router.push()
Nested layoutsTab bars + nav stacksScaffold + nav graphExpo Router layouts (_layout files)
URL params / queryTyped navigation valuesTyped nav argumentsFile-based route params
Deep links (/product/42)Universal Links / URL schemesApp Links / intent filtersExpo deep-linking config

Gotcha: Native navigation is a literal stack you push and pop, plus platform chrome (iOS nav bars with swipe-back, Android's back button/gesture and system back stack). There's no uniform resource locator (URL) bar. Expo Router will feel uncanny to you: it's file-based routing (app/product/[id].tsx), typed params, and layouts — essentially the Next.js App Router model ported to mobile. If you know App Router, you already know Expo Router.

Async & concurrency

WebiOS (native)Android (native)Cross-platform
Promise / async/awaitSwift Concurrency: async/awaitCoroutines: suspend funRN: same JS model · Flutter: Future
Single event loopReal threads + actors for safetyReal threads + structured concurrencyRN: JS thread + native threads
Promise.allasync let / task groupsasync {} / awaitAllPromise.all (RN)
Observables / RxJSAsyncSequence, CombineFlow / StateFlowRxJS (RN) · Stream (Flutter)
— (UI is single-threaded)Must hop to @MainActor for UIMust switch to Dispatchers.MainRN marshals to JS/UI thread

Gotcha: This is where "no event loop" bites. There are real threads, so you must explicitly move work back to the main thread before touching UI. Swift 6.2's "approachable concurrency" (main-actor-by-default for UI code) and Kotlin's structured coroutines make this ergonomic, but the compiler will now catch data races that JavaScript's single-threaded model never let you have. Kotlin's Flow/StateFlow ≈ RxJS Observables; Swift's AsyncSequence and Combine play the same role.

Data fetching & networking

WebiOS (native)Android (native)Cross-platform
fetch / axiosURLSession (async/await)Retrofit / Ktor / OkHttpRN: fetch works · Flutter: http / dio
res.json() (untyped)Codable (typed decode)kotlinx.serialization (typed)RN: JSON.parse · Flutter: json_serializable
React Query cachingManual or third-partyRepository pattern + Room cacheReact Query (RN)
WebSocketURLSessionWebSocketTaskOkHttp/Ktor WebSocketsSame as web (RN)
GraphQL (Apollo/urql)Apollo iOSApollo KotlinApollo Client (RN)

Gotcha: JavaScript Object Notation (JSON) decoding is typed and checked on native — Codable and kotlinx.serialization turn network responses into strongly-typed models at the boundary, so you don't carry any through your app. In React Native, your existing fetch + React Query workflow ports over almost verbatim.

Local storage & persistence

WebiOS (native)Android (native)Cross-platform
localStorage (small K/V)UserDefaultsDataStore (SharedPreferences = legacy)RN: AsyncStorage / MMKV · Flutter: shared_preferences
Secure secretsKeychainEncryptedDataStore / KeystoreRN: expo-secure-store · Flutter: flutter_secure_storage
IndexedDB / SQL.jsSwiftData (Core Data = legacy)Room (SQLite ORM)RN: expo-sqlite/WatermelonDB · Flutter: Drift/Hive
Cache APIURLCache / file systemOkHttp cache / file systemFile-system libs
CookiesHTTPCookieStorageCookieManagerManaged by fetch layer

Gotcha: The web treats "storage" as one blurry bucket; native forces a clean split — key-value (UserDefaults/DataStore) for small preferences vs a real embedded SQLite database (SwiftData/Room) for structured data. SwiftData (iOS 17+) replaced Core Data for new projects; Room is Android's typed SQLite object-relational mapper (ORM). Secrets go in the platform's secure enclave (Keychain / Keystore) — never in plain key-value.

Build tooling & bundler

WebiOS (native)Android (native)Cross-platform
Vite / Webpack / esbuildXcode build system / xcodebuildGradle (Android Gradle Plugin)RN: Metro bundler · Flutter: flutter build
vite build → JS bundleCompile → machine code (.ipa)Compile → bytecode/native (.aab)RN: JS bundle + Hermes bytecode
HMR / dev serverSwiftUI Previews / live rebuildCompose Preview / Live EditMetro + Fast Refresh · Flutter hot reload
Tree-shaking / code-splitDead-code stripping (linker)R8 shrinking/minificationMetro splitting

Gotcha: You're compiling to a binary, not shipping source. Gradle (Android) is powerful but famously slow and configured in a Kotlin/Groovy domain-specific language (DSL) — budget time to learn it. Xcode's build system is more opaque and driven by the integrated development environment (IDE). React Native still ships a JS bundle, but the New Architecture (mandatory as of RN 0.83 / Expo SDK 55) precompiles it to Hermes bytecode for fast startup.

Package manager & registry

WebiOS (native)Android (native)Cross-platform
npm / pnpm / yarnSwift Package Manager (CocoaPods = declining)Gradle deps (Maven Central)RN: npm (+ native autolinking) · Flutter: pub (pub.dev)
package.jsonPackage.swift / project settingsbuild.gradle.ktspackage.json / pubspec.yaml
npm installSPM resolve / pod installGradle syncnpm install + pod install (iOS)
LockfilePackage.resolvedgradle.lockfile (opt-in)package-lock.json / pubspec.lock

Gotcha: Swift Package Manager (SPM) is now the primary iOS package manager; CocoaPods still appears in older projects but is winding down. In React Native you still npm install, but native modules require a build step (and, on iOS, pod install) — dependencies aren't pure JavaScript, so "it installed" doesn't always mean "it linked."

Testing

Your unit → component → end-to-end (e2e) pyramid maps cleanly.

WebiOS (native)Android (native)Cross-platform
Vitest / Jest (unit)Swift Testing / XCTestJUnit / kotlin-testJest / Vitest (RN) · flutter_test
Testing Library (component)SwiftUI ViewInspector / snapshotCompose UI testReact Native Testing Library
Playwright / Cypress (e2e)XCUITestEspresso / UI AutomatorMaestro / Detox · integration_test
MSW (network mocking)URLProtocol stubsMockWebServer / MockKMSW (RN)
vi.mockProtocol-based fakesMockK / MockitoJest mocks

Gotcha: "Swift Testing" is Apple's newer, expressive test framework (@Test, #expect) that sits alongside the older XCTest — you'll meet both. For cross-platform e2e, Maestro has largely become the pleasant modern choice (Detox is the older RN-specific option). Your instinct to keep e2e thin and fast applies doubly on mobile, where UI tests run on slow simulators/emulators.

Linting & formatting

WebiOS (native)Android (native)Cross-platform
ESLintSwiftLintdetektESLint (RN) · dart analyze
PrettierSwiftFormatktlintPrettier (RN) · dart format
eslint --fixSwiftFormat autofixktlint -Fsame

Gotcha: Nearly a one-to-one map — set these up on day one exactly as you would on the web, wired into pre-commit hooks and CI.

Dev tooling, debugging & hot reload

WebiOS (native)Android (native)Cross-platform
Chrome DevToolsXcode debugger / LLDBAndroid Studio debuggerRN DevTools / Flipper · Flutter DevTools
React DevTools (tree)SwiftUI view debuggerLayout InspectorReact DevTools (RN)
Performance/Network tabInstruments (profiler)Android Studio ProfilerNetwork inspector / Flipper
HMR / Fast RefreshSwiftUI PreviewsCompose Preview + Live EditFast Refresh · Flutter hot reload
LighthouseInstruments (Time Profiler, Leaks)Profiler + Baseline Profiles

Gotcha: The IDE is not optional on native — Xcode is mandatory for iOS (and macOS-only; you cannot build/ship iOS apps from Windows or Linux), and Android Studio is strongly expected for Android. SwiftUI Previews / Compose Preview render your component in isolation like a live Storybook, which is delightful. Flutter's hot reload and RN's Fast Refresh preserve state on save, matching your web hot-module-replacement (HMR) experience closely.

CI/CD & distribution

This is the biggest workflow shock — your continuous-integration/continuous-delivery (CI/CD) pipeline now ends at a store review, not a deploy.

WebiOS (native)Android (native)Cross-platform
GitHub ActionsXcode Cloud / FastlaneFastlane / GradleEAS Build (Expo) · Codemagic
Vercel / Netlify (push-to-deploy)TestFlight → App Store reviewPlay Console tracks → reviewEAS Submit
Preview deploymentsTestFlight buildsInternal/closed/open testing tracksEAS internal distribution
Instant rollbackNew build + reviewNew build + reviewEAS Update (OTA) for JS layer
Code signing / provisioning profilesApp signing / keystoreManaged by EAS

Gotchas:

  • No instant deploy. Both stores review your app (hours to a day+). You manage certificates, provisioning profiles (iOS) and signing keys/keystores (Android) — lose these and you can lock yourself out of updating your own app. Fastlane automates most of this pain and is worth learning early.
  • The one web-like escape hatch: React Native's EAS Update ships JavaScript/asset changes over-the-air, reaching users in minutes without a store review — perfect for hotfixes. It cannot change native code. This is the closest thing to "push to deploy" in mobile.

Whether a change reaches users in minutes or days depends entirely on what you changed:

Design systems & component libraries

WebiOS (native)Android (native)Cross-platform
MUI / Chakra / shadcnNative SwiftUI componentsMaterial 3 componentsRN Paper / Tamagui / NativeWind
Heroicons / LucideSF SymbolsMaterial SymbolsIcon libs (@expo/vector-icons)
Tailwind(build your own tokens)MaterialTheme tokensNativeWind (Tailwind)
StorybookXcode PreviewsCompose PreviewStorybook for RN

Gotcha: Apple and Google each ship a complete design language (Apple's HIG, Google's Material 3) baked into their frameworks — using native components gets you correct platform behaviour and accessibility for free. Cross-platform frameworks must either mimic these (Flutter ships both Material and Cupertino widget sets) or let you bring Tailwind-style utilities (NativeWind).


Part 3 — Where there is no web equivalent

These have no clean mapping. Budget real learning time.

  • Permissions. Camera, location, notifications, contacts — each must be requested at runtime, can be denied or later revoked in Settings, and your UI must handle every state gracefully. Nothing on the web works quite like this.
  • App lifecycle & background execution. Apps get suspended and killed by the OS. There's a formal lifecycle (foreground/background/terminated), and background work is tightly restricted and battery-policed. Long-running tasks need specific platform application programming interfaces (APIs).
  • Push notifications. The Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM, on Android) — a whole subsystem with tokens, server-side sending, and permission prompts. No web analogue beyond limited Web Push.
  • Native capabilities. Biometrics (Face ID / fingerprint), Bluetooth, near-field communication (NFC), camera/photo pipelines, secure enclaves, haptics, sensors — each is a native API surface.
  • Code signing & provisioning. Certificates, provisioning profiles, entitlements, keystores. There's no .env equivalent; this is cryptographic identity that gates whether your app can even run on a device or ship.
  • Two design languages. iOS users expect HIG conventions (swipe-back, bottom tabs, SF Symbols); Android expects Material (system back, floating action buttons (FABs), Material motion). "One design" often feels wrong on one platform.
  • Device fragmentation & memory limits. Especially Android — thousands of devices, screen sizes, OS versions, and low-RAM hardware. Your app can be killed for using too much memory. Testing on real low-end devices matters.
  • No hot-swapping native code in production. Web ships new JS on every load. Native ships a reviewed binary. (RN's OTA covers only the JS layer.)

Part 4 — Which path should a React developer take?

Your situationRecommended pathWhy
React dev, want to ship mobile fast, standard appExpo + React NativeReuse React, TS, React Query, your mental model. Expo Router ≈ Next.js App Router. OTA updates. Lowest ramp for you.
Need max native fidelity / heavy platform APIs / gamesNative (Swift + Kotlin)Day-one access to new OS features, best performance, no bridge. Cost: two codebases + two languages.
Want one UI codebase incl. web/desktop, OK learning DartFlutterSingle rendering engine everywhere, excellent tooling. Cost: Dart, non-native widgets, larger binaries.
Have (or want) shared business logic, native UIsKotlin Multiplatform (KMP)Share models/networking/logic in Kotlin; keep SwiftUI + Compose for UI. Production-proven in 2026, but Kotlin-centric.

The same four options, seen as a trade-off between native fidelity (how close it feels to a hand-built platform app) and how much code you share across iOS and Android:

For you specifically — a React/Next.js/TypeScript developer — the pragmatic on-ramp is Expo. It lets you carry ~90% of your existing stack (React, TypeScript, React Query, Zustand, your testing habits) and learn mobile concepts (navigation stacks, permissions, lifecycle, store deployment) without simultaneously learning two new languages. Once you've shipped, you'll understand the native layer well enough to decide whether any given feature justifies dropping into Swift/Kotlin.

A suggested learning sequence

  1. Ship a small Expo app with Expo Router, a couple of screens, a fetch + React Query data layer, and expo-secure-store. This teaches mobile concepts on familiar tools.
  2. Add one native capability (camera, notifications, biometrics) to feel the permissions + lifecycle model.
  3. Do a real store submission via EAS — TestFlight and a Play internal-testing track. This demystifies signing and review.
  4. Then, if curious, build the same tiny screen natively — a SwiftUI view and a Compose screen. The counter from Part 1 is your "hello world." You'll be surprised how familiar @State and remember feel.
  5. Only after that evaluate whether a native or KMP investment is worth it for your product.

The same on-ramp at a glance:


One-screen cheat sheet

ConcernWebiOSAndroidCross-platform
LanguageTSSwiftKotlinTS (RN) / Dart (Flutter)
UIReactSwiftUIComposeRN / Flutter
StylingCSS/SassmodifiersModifierStyleSheet/NativeWind
LayoutFlexboxVStack/HStackColumn/RowYoga flexbox
Local stateuseState@StaterememberuseState / setState
App stateZustand/Redux@ObservableViewModel+StateFlowZustand (RN) / Riverpod
Server stateReact Queryasync loadersrepo + FlowReact Query (RN)
RoutingReact Router/NextNavigationStackNavigation ComposeExpo Router / go_router
Asyncasync/awaitSwift ConcurrencyCoroutines/FlowJS async / Future
Networkingfetch/axiosURLSessionRetrofit/Ktorfetch / dio
K/V storagelocalStorageUserDefaultsDataStoreAsyncStorage/MMKV
DBIndexedDBSwiftDataRoomexpo-sqlite / Drift
BuildVite/WebpackXcodeGradleMetro / flutter build
PackagesnpmSPMGradle/Mavennpm / pub
Unit testVitest/JestSwift TestingJUnitJest / flutter_test
E2E testPlaywrightXCUITestEspressoMaestro / integration_test
Lint/formatESLint/PrettierSwiftLint/SwiftFormatdetekt/ktlintESLint / dart
Hot reloadHMRSwiftUI PreviewsCompose PreviewFast Refresh / hot reload
CI/CDGH ActionsXcode Cloud/FastlaneFastlane/GradleEAS Build
DeployVercel pushTestFlight→App StorePlay ConsoleEAS Submit + OTA

State-of-the-ecosystem note (mid-2026): iOS figures assume Swift 6.2, SwiftUI with @Observable/SwiftData, and NavigationStack. Android assumes Kotlin 2.x, Jetpack Compose with Navigation 3, ViewModel + StateFlow, and Room/DataStore. Cross-platform assumes React Native 0.83+/Expo SDK 55 with the New Architecture mandatory and Hermes as the default engine, plus Flutter (Dart) and Kotlin Multiplatform + Compose Multiplatform as alternatives. Framework defaults and deprecations move fast — always cross-check the official release notes for your exact minor version.