Mobile and React Native interview questions
React Native architecture and Hermes, the JS and UI threads, lists and animations, navigation, offline sync, push, lifecycle and process death, deep links, store releases and OTA updates, secure token storage, crash symbolication, and iOS versus Android differences.
Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.
Nothing is due right now. Every question in this set is scheduled for a later review.
What is the React Native New Architecture, and what did it replace?
Reveal model answer
The New Architecture replaces the old asynchronous bridge, which serialized every call between JavaScript and native as JSON batches. Its foundation is JSI, a C++ interface that lets JavaScript hold references to native objects and call them directly, without serialization. On top of it sit Fabric, the new renderer that supports synchronous layout and concurrent React features such as transitions and Suspense, and TurboModules, native modules that are typed and loaded lazily. It became the default in React Native 0.76, and from 0.82 it is the only architecture: the opt-out flags are ignored, and 0.81 (Expo SDK 54) is the last release where you can still run the legacy one.
WHAT A STRONG ANSWER COVERS
- The old bridge was asynchronous, batched, and serialized every message.
- JSI gives direct, synchronous calls into C++ host objects.
- Fabric is the renderer, with synchronous layout (for example `useLayoutEffect` measuring without a visible jump) and concurrent React.
- TurboModules are lazily loaded, codegen-typed native modules.
- Version facts: default since 0.76; the only option from 0.82.
- Migration risk lives in third-party native libraries, and an interop layer helps them along.
RED FLAGS
- Saying JSI makes everything faster automatically; it removes serialization cost, but a slow render is still slow.
- Believing you can stay on the legacy architecture indefinitely by flipping a flag.
- Confusing the New Architecture with Hermes; they are separate pieces.
LIKELY FOLLOW-UPS
- How would you plan the upgrade for an app with a dozen native dependencies?
- What does synchronous layout let you do that `onLayout` could not?
How did your answer compare?
SOURCES
What is Hermes, and how does it change how you build and debug a React Native app?
Reveal model answer
Hermes is the JavaScript engine React Native uses by default, and each React Native release ships a matching build of it. In release builds your bundle is compiled to Hermes bytecode (`.hbc`) at build time, so the device skips parsing and compiling JavaScript at startup. That gives faster startup, lower memory use, and a smaller app than JavaScriptCore. The practical consequences: measure performance only in release builds, crash stack traces point into bytecode and need the matching source map to be readable, and a few engine differences (for example in `Intl` or newer syntax) can show up only on device.
WHAT A STRONG ANSWER COVERS
- Default engine, bundled with and versioned alongside React Native.
- Compiles to bytecode at build time, which helps startup.
- Profile and benchmark in release builds, not dev mode.
- Release stack traces need Hermes source maps to symbolicate.
- You can check for it at runtime with `global.HermesInternal`.
RED FLAGS
- Judging performance from a debug build running against Metro.
- Assuming Hermes is a JIT that speeds up hot loops; its advantage is ahead-of-time bytecode and startup.
LIKELY FOLLOW-UPS
- A crash only happens in the release build. Where do you start?
How did your answer compare?
SOURCES
A fade-in animation built with `Animated.timing(opacity, { toValue: 1, duration: 300 })` stutters whenever the screen is also parsing a large API response. Why, and how do you fix it?
Reveal model answer
Without `useNativeDriver: true`, `Animated` computes every frame on the JavaScript thread, so anything else that keeps that thread busy (JSON parsing, a big re-render, `console.log` in production) misses the 16.67 ms frame budget at 60 fps and drops frames. The UI (main) thread handles native rendering, native stack transitions, and `ScrollView` scrolling, which is why those stay smooth even while JS is blocked. Fix it by moving the animation off the JS thread: `useNativeDriver: true` for transform and opacity, or Reanimated worklets that run on the UI thread for gesture-driven work. Then shrink the JS work itself: move parsing after the transition, paginate, memoize, and strip console calls from release builds.
WHAT A STRONG ANSWER COVERS
- There are two frame rates to watch: the JS thread and the UI thread (the Perf Monitor shows both).
- 16.67 ms per frame at 60 fps, and less on 120 Hz devices.
- `useNativeDriver` or Reanimated worklets run the animation off the JS thread.
- The native driver supports only non-layout props such as transform and opacity.
- Reduce the JS work too; moving the animation only hides the symptom.
RED FLAGS
- Blaming React Native in general without naming the thread involved.
- Animating `width` and `height` instead of `transform: [{ scale }]`.
- Profiling in dev mode, where JS is much slower.
LIKELY FOLLOW-UPS
- How would you build a swipe-to-dismiss gesture that tracks the finger at 120 Hz?
- When would `LayoutAnimation` be enough?
How did your answer compare?
SOURCES
A feed renders `<ScrollView>{posts.map((p) => <Post key={p.id} post={p} />)}</ScrollView>` with 3,000 posts and the screen takes seconds to open. What's wrong, and what do you change?
Reveal model answer
`ScrollView` renders every child up front, so all 3,000 rows are mounted and laid out before the first frame, which costs time and memory. Use a virtualized list: `FlatList` renders only a window around the viewport, and a recycling list such as FlashList or Legend List reuses row views, which helps on low-end Android. Then make each row cheap: a stable `keyExtractor`, a memoized row component and a `renderItem` that doesn't change on every render, `getItemLayout` when rows have a fixed height, images sized to their display size, and tuned `windowSize` and `initialNumToRender`. Paginate from the server, and measure in a release build before and after.
WHAT A STRONG ANSWER COVERS
- `ScrollView` is not virtualized; `FlatList` and its relatives are.
- Recycling lists reduce mount and unmount churn.
- Stable keys, memoized rows, and no inline objects that break memoization.
- `getItemLayout` skips measurement for fixed-height rows.
- Oversized images and pagination often matter more than which list library you pick.
RED FLAGS
- Using the array index as the key in a list that can reorder or have items inserted.
- Swapping list libraries before measuring where the time goes.
- Setting `removeClippedSubviews` or a huge `windowSize` as a cure-all.
LIKELY FOLLOW-UPS
- Rows have variable heights and images. What gets harder?
- How do you keep the scroll position when new posts arrive at the top?
How did your answer compare?
SOURCES
Design offline support for a field-inspection app where users edit records without signal and sync later.
Reveal model answer
Make a local database (SQLite, WatermelonDB, Realm, or similar) the source of truth for the UI, and treat the network as a sync channel. Record each user change as a durable intent in an outbox, with a client-generated ID and an idempotency key, then replay the queue in order when the device is online, so retries never create duplicates. Every write carries the version it was based on, so the server can detect conflicts and resolve them by a defined policy (per-field merge, last-writer-wins only where it's harmless, or surfacing the conflict to the user) instead of letting arrival order decide. Pull changes with a cursor (an updated-since value or change-feed token), show sync state in the UI, and don't trust connectivity signals: NetInfo is a hint, and only a request that succeeds proves you're online. Background sync is opportunistic on both platforms, so sync when the app is in the foreground and treat background work as a bonus.
WHAT A STRONG ANSWER COVERS
- Local-first reads; the network only syncs.
- A durable outbox of intents with idempotency keys.
- Version or base-revision on every write, plus an explicit conflict policy.
- Delta pulls with a cursor; tombstones for deletes.
- Visible sync state and a retry policy with backoff.
- OS limits on background execution mean sync can't depend on it.
RED FLAGS
- Caching GET responses and calling that offline support.
- Last-writer-wins by client clock for everything.
- Retrying POSTs without idempotency keys.
- Assuming background fetch runs on a schedule you control.
LIKELY FOLLOW-UPS
- Two inspectors edit the same record offline. Walk through what each sees.
- How do you migrate the local schema when an app update ships?
How did your answer compare?
SOURCES
Walk through how push notifications reach a React Native app on iOS and Android, and what can go wrong.
Reveal model answer
The app asks for permission, then registers with the platform service (APNs on iOS, FCM on Android, often through FCM or Expo on both) and receives a device token, which it sends to your backend linked to the user. Your server sends messages through the provider, which delivers them to the device; a notification message is displayed by the OS, while a data-only message wakes your code, though background delivery is throttled and not guaranteed. Things that go wrong: tokens rotate or are invalidated on reinstall, so refresh them on every launch and prune the ones the provider rejects. Since Android 13, notifications need the `POST_NOTIFICATIONS` runtime permission and are off by default for new installs. iOS shows its prompt only once. A tap that cold-starts the app must be routed after navigation is ready. Never put sensitive data in the payload, since it can appear on the lock screen.
WHAT A STRONG ANSWER COVERS
- Permission, device token, backend registration, then a send through APNs or FCM.
- Notification messages and data messages behave differently in the background.
- Token refresh, plus cleaning up invalid tokens on the server.
- Android 13+ runtime permission; ask in context rather than at first launch.
- Handle a cold-start tap by routing through the deep-link setup.
- Delivery is best effort, so don't use push as a reliable data channel.
RED FLAGS
- Asking for notification permission on the first screen with no context.
- Storing one token per user and overwriting it when they log in on a second device.
- Relying on silent pushes to keep data in sync.
LIKELY FOLLOW-UPS
- How would you deduplicate a notification shown both by push and by an in-app WebSocket event?
How did your answer compare?
SOURCES
A user fills half of a long form, switches to their camera app, and comes back to an empty form on the home screen. What happened, and how do you prevent it?
Reveal model answer
While the app was in the background, the OS killed its process to reclaim memory. Both platforms do this (Android says so explicitly), and when the user returns, the app cold-starts with all in-memory state gone: JS state, the navigation stack, and the half-filled form. The fix is to treat backgrounding as a point where you may lose everything: persist the draft (debounced as the user types, and again when `AppState` changes to `background`) to local storage, persist or rebuild the navigation state, and restore both on launch. Keep the saved state small, an ID plus draft fields, and reload everything else. Test it on purpose by backgrounding the app and killing its process with `adb shell am kill <package>`, then reopening it from Recents, because a normal test session rarely triggers it. Android's "Don't keep activities" option is not a substitute: it destroys the activity but keeps the process, so the JS runtime and its state can survive.
WHAT A STRONG ANSWER COVERS
- Backgrounded apps can be killed without any further callback.
- In-memory state, React state included, does not survive it.
- Save drafts continuously and on the background transition, then restore on launch.
- Keep saved state minimal and rebuild the rest from persistent storage.
- Reproduce it deliberately; it rarely shows up in normal testing.
RED FLAGS
- Expecting an unmount or `beforeunload`-style event to fire before the kill.
- Saying "the user swiped the app away" when the OS killed it, which is a different case.
- Persisting the entire Redux store, tokens included, to unencrypted storage as the fix.
LIKELY FOLLOW-UPS
- What's the difference between a configuration change and process death on Android?
- How does this interact with an OAuth redirect that opens the browser?
How did your answer compare?
SOURCES
What's the difference between a custom URL scheme and universal links or Android App Links, and how do you handle deep links safely?
Reveal model answer
A custom scheme (`myapp://order/42`) is easy to set up, but any app can register the same scheme, so it can be hijacked, and it does nothing if the app isn't installed. Universal links (iOS) and App Links (Android) use your HTTPS domain and are verified through files you host (`apple-app-site-association` and `assetlinks.json`), so only your app can claim them, and they fall back to the website when the app is missing. Handle both cold start (an initial URL) and warm start (a URL event) through one routing config, such as React Navigation's `linking`, and wait until auth and navigation are ready before routing. Treat every link as untrusted input: validate parameters, re-check authorization on the server, and never put tokens or secrets in a link.
WHAT A STRONG ANSWER COVERS
- Custom schemes aren't unique and can be hijacked; verified links are tied to a domain.
- Verification files hosted on the domain, plus entitlements and intent filters in the app.
- Cold-start and warm-start paths go through the same routing.
- Deep links are untrusted input; no sensitive data in them.
- Deferred deep links (after install) need a third-party or custom solution.
RED FLAGS
- Passing an auth token or password-reset secret through a custom scheme.
- Routing before the auth state has loaded, which drops the link or shows the wrong screen.
LIKELY FOLLOW-UPS
- Links open the website instead of the app on some Android devices. How do you debug it?
How did your answer compare?
SOURCES
How do you ship a fix to a React Native app quickly, and what can and can't an over-the-air update change?
Reveal model answer
Store releases go through review and a staged rollout (phased release on iOS, staged rollout on Play), and users update on their own schedule, so many old versions stay in use for a long time. Over-the-air tools such as EAS Update can replace the JavaScript bundle and assets for builds with a compatible runtime version, but they can't change native code, native dependencies, permissions, or the SDK version, all of which need a new binary. Store policy also limits OTA: Apple's App Review Guideline 2.5.2 forbids downloaded code that introduces or changes features, and the Developer Program License Agreement (section 3.3.1(B)) allows downloaded interpreted code such as a JS bundle only if it doesn't change the app's primary purpose, bypass OS security, or create a storefront. So OTA is for fixes and small changes, not new features that skip review. A sound release setup has runtime-version pinning, staged OTA rollouts with automatic rollback on crash spikes, remote feature flags as a kill switch, and a server-enforced minimum-version check for breaking changes.
WHAT A STRONG ANSWER COVERS
- Store review and staged rollouts; old versions live on.
- OTA covers JS and assets only; anything native needs a binary.
- Runtime version matching prevents JS that expects missing native code.
- Store policy: no feature changes that bypass review.
- Feature flags, staged OTA, rollback, and a minimum-version gate.
- The backend must stay compatible with the oldest supported app version.
RED FLAGS
- Saying OTA lets you ship anything at any time without review.
- Pushing an OTA update that calls a native module the installed binary doesn't have.
- Breaking an API that old app versions still call.
LIKELY FOLLOW-UPS
- A release crashes on launch for 5% of users. What are your first three moves?
- How do you decide which minimum version to enforce?
How did your answer compare?
SOURCES
Code review: after login the app runs `AsyncStorage.setItem("refreshToken", token)` and reads `API_SECRET` from `react-native-config`. What's wrong?
Reveal model answer
AsyncStorage is an unencrypted key-value store, so on a rooted or jailbroken device, or through some backups, the refresh token is readable in plain text. Tokens belong in the platform's secure storage: the iOS Keychain and Android Keystore-backed encryption, through `expo-secure-store` or `react-native-keychain`. Anything in the JS bundle or build config, including environment values, can be extracted by anyone who unpacks the app, so `API_SECRET` isn't secret; move calls that need it behind your backend. Also keep access tokens short-lived, use OAuth with PKCE for login, clear secure storage on logout, and remember that iOS Keychain items can survive an uninstall (Apple doesn't guarantee it either way), so check for a first launch after reinstall.
WHAT A STRONG ANSWER COVERS
- AsyncStorage is not encrypted.
- Keychain and Keystore through a maintained library.
- The app bundle and its config are public; real secrets live on the server.
- Short-lived access tokens, refresh rotation, and PKCE.
- Keychain persistence across reinstall; size limits on stored values.
RED FLAGS
- Obfuscating the secret and calling it secure.
- Saying SSL pinning solves token storage.
- Storing the whole persisted Redux state, tokens included, in AsyncStorage.
LIKELY FOLLOW-UPS
- When would you require biometric authentication to read the token, and what breaks when biometrics change?
How did your answer compare?
SOURCES
Your crash reporter shows release stack traces like `at a (index.android.bundle:1:48213)`. How do you make them useful?
Reveal model answer
Release builds are minified and, with Hermes, compiled to bytecode, so traces need symbolication: upload the JavaScript source map (for Hermes, the composed map that includes the bytecode step) for every build, tagged with the exact release and build number the crash reporter will match. Native crashes need their own symbols: dSYM files for iOS, and the R8/ProGuard mapping file plus native debug symbols for Android. Automate the uploads in CI as part of the build, because a missing or mismatched map can't be fixed after the build is gone, and make sure OTA updates upload their own maps under their own update identifier. Then triage by release, device, and OS, and watch ANRs and app hangs alongside crashes.
WHAT A STRONG ANSWER COVERS
- JS source maps per build, matched by release and build.
- Hermes needs the composed source map.
- dSYMs for iOS; R8 mapping and native symbols for Android.
- Uploads automated in CI, including for OTA updates.
- JS errors, native crashes, and ANRs are separate signals.
RED FLAGS
- Uploading source maps by hand, sometimes.
- Reusing one release name across several builds, so maps don't match.
- Assuming every crash is a JS exception.
LIKELY FOLLOW-UPS
- The trace is readable but you can't reproduce the crash. What context would you add?
How did your answer compare?
SOURCES
- Sentry: Source maps for React Native (opens in new tab)Checked
- Sentry: Uploading source maps for Hermes (opens in new tab)Checked
- Apple Developer: Building your app to include debugging information (opens in new tab)Checked
- Play Console Help: Deobfuscate or symbolicate crash stack traces (opens in new tab)Checked
What platform differences between iOS and Android do you have to design for in a cross-platform app?
Reveal model answer
Navigation and input differ: Android has a system back gesture or button that must close modals and pop screens, while iOS relies on swipe-back and on-screen controls. Permissions differ in timing and wording (iOS usage-description strings and one-shot prompts; Android runtime permissions with "don't ask again" and newer ones such as notifications since Android 13). Background execution, push behavior, and process killing differ by OS and even by Android manufacturer. UI details differ too: shadows versus `elevation`, fonts, keyboard avoidance, safe areas and notches, and date pickers. Release processes differ (App Store review and TestFlight versus Play tracks and staged rollouts). Handle this with `Platform.select` or `.ios.tsx` and `.android.tsx` files for real differences, and test on real low-end Android hardware, not just a recent iPhone.
WHAT A STRONG ANSWER COVERS
- Back handling on Android and swipe-back on iOS.
- Different permission models and prompt rules.
- Background limits and push delivery vary, and Android OEMs add their own.
- Styling and keyboard differences (shadow versus elevation, safe areas).
- Different release and review pipelines.
- Platform-specific files or `Platform.select` only where behavior really differs.
RED FLAGS
- Testing only on an iOS simulator.
- Making the Android app imitate iOS conventions, or the other way around.
- Ignoring hardware back, which leaves modals that can't be closed.
LIKELY FOLLOW-UPS
- Which performance problems show up on Android first, and why?
How did your answer compare?
SOURCES