A mobile engineer at a Series B logistics startup got stuck in a loop last spring over a scroll handler. His FlatList of delivery stops jittered on a mid-range Android phone but ran smooth on his iPhone 15 in the simulator. He'd assumed it was a rendering problem and spent twenty minutes proposing memoization fixes. The actual answer, the one the interviewer was fishing for, had nothing to do with React at all: it was about how much data was crossing the bridge on every scroll frame and why that cost is different on Android's slower JSON serialization path than on iOS. He knew React. He didn't know React Native's runtime, and the gap showed within five minutes.
React Native interview questions increasingly split into two buckets that don't overlap as much as people expect. One bucket is ordinary React, hooks, props, component lifecycle, that any web developer already has. The other bucket is specific to running JavaScript inside a mobile runtime: the bridge and its JSI replacement, native modules, Metro, Hermes, and the reality that "it works on iOS" and "it works on Android" are two separate claims. Teams hiring for React Native roles in 2026 are also increasingly asking about the New Architecture (Fabric and TurboModules), since Meta made it the default starting with React Native 0.76 in late 2024 and the earlier bridge-based architecture is now the legacy path most teams are migrating off of (React Native documentation, New Architecture).
This page covers 50 React Native interview questions across eight areas: the bridge and New Architecture, Flexbox and styling, components and the render lifecycle, list performance, native modules and platform code, navigation and state, tooling and debugging, and shipping to the app stores. Code examples are real React Native JSX and Kotlin/Swift snippets where relevant, not pseudocode.
The bridge, JSI, and the New Architecture
Every loop touches this section somewhere, even for candidates who've shipped several apps. A candidate who can only describe RN as "React that renders native views" without touching how the JS and native sides actually talk to each other runs out of depth fast.
Easy questions
12React Native runs your JavaScript in a separate JS engine (historically JavaScriptCore, now Hermes by default) and uses that JS to describe a tree of native UI components, which then get rendered as actual UIKit views on iOS and actual Android View objects on Android. A Cordova-style app renders your UI inside a WebView, so buttons, scroll views, and text fields are all HTML and CSS being interpreted by a browser engine, not real platform widgets.
That distinction is why RN apps generally feel closer to native in scrolling, animations, and accessibility, and why RN still needs a JS-to-native communication layer for anything that touches the camera, storage, or native UI, since the JS thread itself has no direct access to platform APIs.
The original bridge was an asynchronous, batched message queue. Every call from JS to native, and every native event back to JS, got serialized to JSON, queued, and flushed in batches at the end of a run loop tick. That worked fine for occasional calls like a network request or a single native module method, but it fell apart under high-frequency traffic: a scroll position updating sixty times a second, a gesture tracking a finger drag, an animation driven from JS.
Every one of those events had to be JSON-stringified going one direction and parsed coming back, which is real CPU cost multiplied by frame rate. That's the specific mechanism behind janky scroll handlers and laggy custom animations on older RN apps, not "React being slow."
On the web, the CSS default for flex containers is flex-direction: row. In React Native, the default is flexDirection: "column". This trips up developers coming from web CSS constantly, they'll write a row of items expecting the default to stack horizontally and get a vertical stack instead, because RN assumes mobile layouts are column-first by default.
StyleSheet.create validates the style object shape at creation time and, on the old architecture, converted styles into numeric IDs that got sent across the bridge instead of the full object every render, which was a real performance win when serialization cost money. On the new architecture with JSI, that specific bridge-cost argument matters less, but StyleSheet.create still gives you upfront validation errors for invalid style properties and keeps styles defined once outside the render function instead of recreated as a new object literal on every render.
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
});
// vs inline: <View style={{ flex: 1, padding: 16 }}> creates a new object every renderFunctional components with hooks are the default for any new RN code, and have been for years. The one case where a class component is still genuinely useful is error boundaries, since React's error boundary API (componentDidCatch, getDerivedStateFromError) is only available on class components, there's no hook equivalent. Outside of that single case, there's no technical reason to write a new class component in 2026.
React uses the key to track which item in a list corresponds to which rendered instance across re-renders, so it can correctly reuse, update, or remove the right DOM/native node instead of re-mounting the entire list from scratch on every change. Skip it and React falls back to index-based identity, which breaks the moment the list is reordered, filtered, or has items inserted in the middle. Symptoms show up as text input state jumping to the wrong row, or animations playing on the wrong item, because React matched the wrong previous instance to the wrong new data.
ScrollView with a mapped array renders every single item up front, all at once, whether or not it's currently visible on screen. That's fine for ten items, and a genuine performance problem for a few hundred. FlatList only renders the items currently near the visible viewport and recycles the rest, which is the only approach that scales to long or unbounded lists without eating increasing amounts of memory and render time as the list grows.
Anytime the functionality genuinely isn't accessible from JS at all: a platform API with no existing RN or community wrapper (a specific Bluetooth protocol, a proprietary hardware SDK, a background processing API), or a case where performance genuinely requires native code, heavy image processing, or cryptography that would be too slow in JS. The wrong reason to write one is "it felt more familiar," most functionality teams reach for a native module for already has a well-maintained community package, and writing your own just to maintain means one more thing your team owns forever.
Stack navigator for a linear drill-down flow, list to detail to edit, where going back should return you to the previous screen in history. Tab navigator for a small, fixed set of top-level sections a user jumps between freely, home, search, profile, where there's no real "back" concept between tabs. Drawer navigator for a longer or less-frequently-used set of destinations, settings, help, account switching, that don't deserve permanent screen real estate as a tab bar item. Most real apps nest these, a tab navigator where each tab itself contains its own stack navigator.
The old framing, "Expo is easy but you can't add native code," is mostly outdated now that Expo's config plugin system and development builds let you add arbitrary native modules and still keep most of Expo's tooling, EAS Build for cloud builds, expo-updates for OTA, a huge library of pre-built native modules for camera, location, notifications, and so on. The real remaining tradeoff is control and debugging visibility: bare RN gives you direct access to the Xcode and Android Studio projects with no abstraction in between, which matters if you're doing deep, unusual native integration work, while Expo's config plugins generate that native project for you, which is faster for the common case and occasionally opaque when something in that generation step goes wrong.
Metro is React Native's JavaScript bundler, purpose-built for RN's specific constraints: fast incremental rebuilds for the tight edit-reload loop mobile development needs, resolving RN's platform-specific file extensions (.ios.js,.android.js) automatically, and producing a single bundle format the native runtime (JSC or Hermes) knows how to load, rather than the browser-oriented module and asset handling a general web bundler like webpack assumes.
A debug build runs unminified JS with dev warnings enabled, connects to Metro's dev server for live reload, and generally behaves more forgivingly than what actually ships. A release build runs the precompiled, minified bundle exactly as it'll run in production, no dev server, no fallback warnings, which is the only build type that will actually surface a real Hermes minification bug, a missing ProGuard keep rule, or a performance issue that dev mode's extra instrumentation was masking. Never submit an app to either store having only tested the debug build.
Medium questions
27JSI, the JavaScript Interface, is a lightweight C++ layer that lets JavaScript hold direct references to native C++ objects and call their methods synchronously, in the same thread, with no JSON serialization and no queue. Instead of JS sending a message and waiting for a batch to flush, JS can call a native method and get a return value immediately, the same way it calls any other JS function.
// Old bridge model: async, serialized, batched
NativeModules.MyModule.getValue((result) => {
console.log(result); // arrives later, after JSON round trip
});
// JSI model: synchronous, direct C++ binding, no serialization
const result = global.MyTurboModule.getValueSync();
console.log(result); // available immediately, same call stackThis is the change that made Reanimated 2 and 3 possible, worklets can run gesture and animation logic directly against native values without ever touching the old bridge's queue.
Fabric is the new rendering system, it replaces the old UIManager and uses JSI to construct and commit the native view tree more directly, with a C++ core shared across platforms instead of separate platform-specific bridging code for every view update. TurboModules is the new native module system, it uses JSI so native modules load lazily on first use instead of all initializing at app startup, and calls to them can be synchronous instead of always going through the async bridge.
Both sit on top of JSI as the underlying transport. Fabric is about views, TurboModules is about native functions and modules, and together they're what people mean by "the New Architecture," which became the default in React Native 0.76.
Fabric and TurboModules became the default for new projects starting with React Native 0.76 in late 2024. Apps built before that, and libraries that haven't been updated, may still run on the legacy bridge via a backward-compatibility layer, but Meta has been explicit that the legacy architecture is being phased out and third-party libraries are expected to ship New Architecture support going forward.
For an app that hasn't migrated, the practical risk isn't a broken build today, it's growing incompatibility with newer versions of popular libraries (Reanimated, gesture-handler, screens) that are dropping legacy-bridge support entirely in their latest majors.
Two approaches, and they solve slightly different problems. Platform.select lets you branch inline for small differences, a shadow style on iOS versus an elevation property on Android, say. File extensions, Component.ios.tsx and Component.android.tsx, are the right call when the divergence is big enough that maintaining one shared file with branches everywhere gets harder to read than just having two files.
const cardStyle = {
...Platform.select({
ios: { shadowColor: "#000", shadowOpacity: 0.1, shadowRadius: 4 },
android: { elevation: 4 },
}),
};Yoga is Meta's C++ implementation of Flexbox, and RN uses it as a shared, cross-platform layout engine rather than letting iOS Auto Layout and Android's layout system each compute layout independently with their own quirks. That gives every RN app the exact same layout behavior on both platforms, since the same Yoga computation runs underneath both, and it's why a Flexbox layout you write once genuinely behaves the same on iOS and Android instead of needing platform-specific layout tweaks for basic positioning.
Percentage-based dimensions resolve against the parent's own computed size, and if the parent itself doesn't have an explicit or flex-derived size (for example, a parent with no flex value and no explicit height, sitting inside a ScrollView's content container, which by default sizes to its content rather than the screen), the percentage has nothing concrete to resolve against and the layout collapses or behaves unpredictably. The fix is almost always giving the parent chain an explicit flex: 1 or a real height, tracing up from the percentage-sized child until you find the missing link.
The listener keeps firing and holding a reference to the component's closure even after the component unmounts, which is a memory leak, and in the worst case it calls setState on an unmounted component, which React will warn about and which can mask real bugs if the warning gets ignored long enough to become routine noise in the console.
useEffect(() => {
const subscription = AppState.addEventListener("change", handleAppStateChange);
return () => subscription.remove(); // required, or the listener outlives the component
}, []);They help when a component is genuinely expensive to render (a chart, a large list item with several nested views) and its props change infrequently relative to its parent's render frequency. They add overhead, real overhead, comparison cost on every render, when applied reflexively to cheap components that would've been fine re-rendering anyway. The classic RN-specific case where this actually matters: a renderItem function passed to FlatList that isn't memoized recreates on every parent render, which defeats FlatList's own internal optimizations for reusing item components.
A controlled TextInput ties its value prop to state and updates it on every onChangeText call, guaranteeing the displayed text always matches your state. On Android specifically, controlled inputs combined with autocomplete/autocorrect and certain IME (input method editor) keyboards can cause visible cursor jumping or dropped characters under fast typing, because the round trip through React state and back to the native input can lag behind the native keyboard's own composition state. The common workaround is using an uncontrolled ref-based approach for high-frequency typing (a search box with debounced filtering) and only syncing to state on blur or after a debounce, rather than controlling every keystroke.
The pattern is the same everywhere: anything you start in a useEffect (setInterval, setTimeout, event listener subscriptions, Animated.loop) needs a matching cleanup in the effect's return function, and that cleanup has to run on every unmount path, not just the happy one. The mistake I've seen most often in code review is a setTimeout inside an async function where the cleanup only clears a timer set synchronously, missing the case where the async work resolves after unmount and schedules a new timer that never gets cleared, because the closure captured a stale reference.
getItemLayout tells FlatList the exact height (or width, for horizontal lists), offset, and index of every item up front, without FlatList having to measure each item after it renders. Without it, FlatList has to render an item first, then measure it, before it knows where the next one starts, which adds real overhead, especially noticeable when jumping to an arbitrary scroll position with scrollToIndex, which can't work reliably at all without getItemLayout unless every item is a fixed, known height.
const ITEM_HEIGHT = 72;
<FlatList
data={stops}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>initialNumToRender sets how many items render on first mount, before any scrolling happens, lower it if your first screen is janky on load. maxToRenderPerBatch caps how many new items get rendered in each batch as the user scrolls, and windowSize controls how many screens' worth of content stay rendered above and below the visible viewport (measured in multiples of the visible screen height, not item count). Tune these down when each item is expensive to render and you're seeing scroll jank from over-rendering, tune windowSize up specifically if you're seeing blank flashes when scrolling fast, since that means content is being unmounted before it's needed again.
FlashList, built by Shopify, uses a true cell-recycling model closer to how native iOS UITableView and Android RecyclerView work beneath that: instead of unmounting and remounting components as they scroll off and back onto screen, it reuses the actual component instances and just swaps the data bound to them. FlatList's model is closer to virtualization with mount/unmount cycles. In practice FlashList needs an estimatedItemSize hint rather than an exact getItemLayout, and tends to hold up better on very long lists with variable-height items, which is the specific case where FlatList's performance degrades the most.
Flipper's Performance Monitor plugin (or its React Native DevTools successor) for React render timing and frame drops, the in-app Perf Monitor overlay for a quick live JS-thread and UI-thread FPS readout during development, Xcode Instruments' Time Profiler for iOS-specific native call stacks, and Android Studio's Profiler or Systrace for Android-specific main-thread work. The mistake I see most is a developer optimizing purely from React DevTools render counts without ever checking whether the actual bottleneck is native layout or image decode time, which none of React's own profiling tools can see.
The old API registered a module at app startup whether or not it was ever used, and every method call was async and serialized across the bridge. A TurboModule is defined with a typed spec (using Codegen to generate the native interface from a TypeScript definition), loads lazily on first access instead of at startup, and can expose synchronous methods since it's backed by JSI rather than the old bridge queue.
// NativeDeviceInfo.ts, the spec Codegen reads
import type { TurboModule } from "react-native/Libraries/TurboModule/RCTExport";
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
getBatteryLevel(): number; // synchronous, real return value
}
export default TurboModuleRegistry.getEnforcing<Spec>("DeviceInfo");Before autolinking, adding a native dependency meant manually editing Xcode project files and Android's settings.gradle and MainApplication.java to register the library, error-prone and easy to get subtly wrong, especially across a team where not everyone touches native code often. Autolinking, introduced with the React Native CLI, scans installed npm packages for a react-native.config.js or the standard package structure and wires up the native project files automatically at build time, so `npm install some-native-lib` plus a pod install (iOS) is usually the entire integration step now for a properly-configured library.
iOS requires a usage description string in Info.plist for every sensitive permission (NSCameraUsageDescription, NSLocationWhenInUseUsageDescription), and the system prompt shows that exact string to the user, missing one causes an instant crash the first time the permission is requested, not a graceful denial. Android's permission model is coarser at the manifest level but finer at runtime, permissions above a certain protection level have to be requested at runtime via a library like react-native-permissions, and Android 13+ added a separate runtime permission specifically for notifications that didn't exist on earlier Android versions. The practical answer in an interview: use a cross-platform permissions library rather than hand-rolling each platform's check, and always test the actual denial and "don't ask again" paths, not just the happy grant path.
The OS hands your app a URL, either a custom scheme (myapp://) or, on modern iOS and Android, a real https:// universal/app link verified against a file hosted on your domain. React Navigation's linking config maps URL path segments to screen names and params, so when the app receives that URL, either cold-starting or resuming from background, it parses the URL against your config and navigates directly to the matching screen with the parsed params already populated, without you writing manual URL-parsing logic yourself.
const linking = {
prefixes: ["myapp://", "https://myapp.com"],
config: {
screens: {
OrderDetail: "orders/:orderId",
},
},
};React Context works fine for state that changes infrequently, current user, theme, feature flags, since Context re-renders every consumer on any change, which becomes a real performance problem for state that updates often. For state that changes frequently and needs to be read from many disconnected components, a library like Zustand or Jotai gives you the ergonomics of a global store with selective subscriptions, so a component only re-renders when the specific slice it reads actually changes, which Context alone can't do without manually splitting into many separate contexts.
AsyncStorage is the baseline, simple async key-value storage, fine for small amounts of data like a theme preference or an auth token, but it's genuinely slow for anything beyond a handful of small reads and writes because every call round-trips through the bridge or JSI asynchronously. MMKV, built by Tencent and wrapped for RN, is a synchronous key-value store backed by memory-mapped files, dramatically faster for frequent small reads and writes since there's no async round trip at all. SQLite (via a library like op-sqlite or expo-sqlite) is the right call once you have actual relational data, an offline cache of orders with relationships to line items, something you'd genuinely want to query and filter rather than just fetch by key.
Offline-first means the app's primary source of truth for rendering is a local store (SQLite, MMKV, or a normalized cache from something like TanStack Query or Redux Persist), and network requests are treated as a background sync process that updates that local store, rather than the UI directly awaiting a network call before it can render anything. Writes made while offline get queued locally and replayed once connectivity returns, usually with an explicit conflict-resolution strategy for what happens if the same record changed on the server in the meantime. Simply caching the last successful response gets you a decent offline read experience but doesn't handle offline writes at all, which is the harder and more interview-relevant half of the problem.
React Native DevTools, built into recent RN versions and replacing the old remote-debugging-via-Chrome flow, connects directly to Hermes's own debugger protocol, which gives accurate breakpoints, a real call stack, and console output that actually reflects what's running on-device, instead of the old approach of running your JS a second time inside Chrome's V8 engine, which could behave subtly differently from how it ran on-device under Hermes. Flipper is still useful for native-side inspection, network request logging, and layout inspection alongside it.
Hermes was built specifically to optimize for RN's actual constraints, faster app startup through bytecode precompilation, lower memory usage, and a smaller engine footprint, all of which matter more on a budget Android phone than they do in a browser. JSC is still available as a fallback and some teams keep it for a specific compatibility need with an older native dependency that assumed JSC-specific behavior, but for a new app in 2026 there's essentially no reason to choose JSC over Hermes by default.
An over-the-air update (via expo-updates or the community CodePush) pushes a new JavaScript bundle directly to installed apps without going through app store review, since the JS bundle is just data your app downloads and executes, not a new native binary. Apple's App Store Review Guidelines explicitly restrict this: an OTA update can't materially change the app's primary purpose or add features that would themselves require review, it's meant for bug fixes and minor UI adjustments, not shipping an entirely new feature to bypass review. Any change that touches native code, adds a new permission, or changes core functionality genuinely requires a new build submitted through normal app store review, no way around that with an OTA update.
iOS requires a provisioning profile tying your app's bundle identifier to a specific set of entitlements and a distribution certificate issued by Apple, and iOS verifies that signature at install time to confirm the binary came from a trusted, registered developer account and hasn't been tampered with since signing. Android uses a keystore file containing a private key you generate yourself and keep, every release build gets signed with that key, and Android checks that an app update is signed with the same key as the currently installed version, which is why losing your original keystore is a genuine problem, you can't push updates to an already-installed app under the same package name without the original signing key (Google Play App Signing mitigates this somewhat by letting Google hold the actual distribution key).
Hermes bytecode itself is generally smaller than shipping raw minified JS for JSC, that's a real, close-to-free win from just using Hermes. Beyond that: enabling ProGuard/R8 on Android to strip unused code and resources, using vector icons instead of large PNG assets at every resolution, compressing and properly sizing images rather than shipping 4x resolution assets, and on iOS enabling App Thinning/bitcode-successor slicing so a user's device only downloads the resources for its specific screen resolution and architecture rather than a universal binary containing every variant.
Missing or vague permission usage-description strings (a camera permission request with a description that doesn't clearly explain why the app needs it), requesting a permission the app doesn't actually use anywhere in its flow, background modes declared in the manifest or Info.plist that the app doesn't genuinely need (background location is a frequent one reviewers scrutinize hard), and, on Android specifically, targeting an SDK version below Google Play's current minimum requirement, which Play Console will flag automatically before a human reviewer even looks at the app. Reading the actual rejection reason in App Store Connect or Play Console carefully, rather than guessing, saves more resubmission cycles than any other single habit.
Hard questions
11The old bridge ran JS and native on separate threads communicating through an async message queue by design, mainly for safety and to avoid blocking the UI thread on a slow native call. But that architectural choice meant there was no mechanism for JS to call a native function and block waiting for a return value in the same tick, every call had to round-trip through serialization and a batch flush, which is inherently asynchronous.
JSI fixes this by giving JS a direct C++ binding to native HostObjects, so a native method can be exposed as a real callable JS function backed by native code, executed in whatever thread makes sense (often the JS thread itself for cheap operations), with a real return value, no serialization step, no queue. The tradeoff: because JSI calls can be synchronous, a badly written synchronous native method can block the JS thread, which the old async-only bridge structurally couldn't do. That's a real footgun teams have to watch for when adopting TurboModules.
Hermes is Meta's JS engine built specifically for React Native, and instead of shipping raw JavaScript source that gets parsed and compiled on the device at every launch, Hermes precompiles your bundle into bytecode at build time. The device just loads and executes bytecode directly, skipping the parse-and-compile step that JSC has to do on every cold start.
The measurable win is time-to-interactive on app launch, especially on lower-end Android hardware where JS parsing used to eat a real chunk of the startup budget. Hermes also uses less memory at runtime than JSC in most RN workloads, which matters more on budget Android devices than on a flagship iPhone, which is part of why teams that only test on newer iPhones sometimes miss performance regressions their Android users actually feel.
Absolute positioning in RN is resolved relative to the nearest positioned ancestor, same concept as CSS, but the two platforms' underlying native layout systems historically had subtly different rounding and pixel-snapping behavior for sub-pixel values coming out of Yoga's flex calculations, especially on Android where the OS handles density-independent pixel rounding differently across device DPI buckets than iOS's point system does. In practice this shows up as a one or two pixel misalignment on some Android devices that's invisible on iOS, and it's usually device-DPI-dependent rather than reproducible on every Android phone, which makes it genuinely annoying to debug without testing on multiple physical Android devices at different pixel densities rather than trusting a single emulator.
renderItem gets a brand new function identity on every render of the parent component when it's written inline, and FlatList's internal cell recycling and shouldComponentUpdate-style checks compare function and prop identity to decide whether an item actually needs to re-render. A new function reference every time means every visible cell gets treated as if its render function changed, defeating the memoization FlatList relies on to only re-render cells whose actual data changed.
// Recreated every render, breaks FlatList's internal memoization
<FlatList data={items} renderItem={({ item }) => <Row item={item} />} />
// Stable reference via useCallback, FlatList can actually skip unchanged cells
const renderItem = useCallback(
({ item }: { item: Stop }) => <Row item={item} />,
[]
);
<FlatList data={items} renderItem={renderItem} />Two things are usually compounding. First, the default Image component decodes and lays out images on the main thread in a way that can block frames when many images finish loading in the same scroll burst. Second, if image dimensions aren't fixed ahead of time, the layout has to reflow once each image's actual size resolves, which cascades a re-layout through every item below it in the list.
The standard fix is fixing every image container to a known width and height before the image loads (so there's no reflow when it resolves), and using a caching image library, either the community's react-native-fast-image historically or, on the New Architecture, expo-image, which decodes off the main thread and caches to disk so images that scroll back into view don't redownload or re-decode.
Historically RN split work across three threads: the JS thread runs your application logic, the shadow thread (also called the layout thread) computes Yoga layout, and the UI thread (called the main thread on iOS, the native thread on Android) actually draws views and handles gestures and touches. Before Reanimated 2, any animation driven by a gesture had to send every touch update from the UI thread, across the bridge, to the JS thread for calculation, then send the result back across the bridge to the UI thread to actually update the view, a full round trip per frame.
Reanimated's worklets are small JS functions marked to run directly on the UI thread via JSI, with no bridge round trip at all, so a drag gesture's transform update happens in the same thread that's drawing the frame, which is why gesture-driven Reanimated animations stay smooth even when the JS thread itself is busy doing something unrelated, like a network response parsing or a heavy list re-render.
On the old bridge, every value crossing the boundary had to be JSON-serializable, plain objects, arrays, strings, numbers, booleans. You couldn't pass a function reference, a class instance, or anything with circular references, because JSON.stringify has no representation for those. Callbacks worked around this by registering a callback ID on one side and invoking it by ID from the other, not by literally passing the function across.
JSI removes the JSON-serialization requirement entirely for New Architecture code, since JS can hold a real reference to a native HostObject and native code can hold a real reference to a JS function value, no stringify step involved. That's what makes it possible to pass an actual callback function directly into a native method call under TurboModules in a way that wasn't structurally possible under the old bridge.
First check whether it's actually a Hermes-versus-JSC or minification issue, release builds run precompiled Hermes bytecode with different optimization behavior and stack traces that no longer map cleanly to source without sourcemaps uploaded to your crash reporter (Sentry, Bugsnag, or Crashlytics), so step one is confirming the crash symbolicates correctly at all before chasing the wrong line number. Second, release builds strip dev-only warnings and enable stricter native optimizations (ProGuard/R8 on Android can remove or rename code paths dev builds never touch), so a real class of these bugs is code relying on a class or method name that got renamed or stripped by the Android release-build minifier, which needs an explicit ProGuard keep rule once you find it. I've chased this exact symptom back to a missing keep rule for a reflection-based native library more than once.
iOS suspends an app's background execution after a short grace period, and in-flight network requests, timers, and websocket connections don't just pause, some genuinely get killed by the OS while the app is backgrounded, then the JS thread resumes as if nothing happened and waits on a promise that's never going to resolve. The fix is subscribing to AppState changes and explicitly re-establishing anything time-sensitive (refetching data, reconnecting a websocket, restarting a polling interval) on the transition back to "active," rather than assuming in-flight work survived the background period.
useEffect(() => {
const sub = AppState.addEventListener("change", (nextState) => {
if (nextState === "active") {
refetchData(); // don't trust whatever was in flight before backgrounding
}
});
return () => sub.remove();
}, []);Three common causes. First, dev-only code paths, __DEV__ checks or console.log-heavy debug branches, sometimes accidentally gate real logic rather than just diagnostics, so the release path genuinely runs different code. Second, Hermes bytecode optimizations can expose timing-sensitive bugs, a race condition that dev mode's extra overhead happened to mask by slowing things down just enough to avoid it. Third, and most common in my experience, Android's release-build minifier (ProGuard/R8) stripping or renaming a class or method that a native library locates via reflection at runtime, which silently breaks only in the minified build and needs an explicit keep rule once identified.
Apple's guidelines (specifically the sections covering software updates and app completeness) prohibit using a mechanism like an OTA JS bundle push, or a remote feature flag, to unlock functionality that wasn't present and reviewable in the submitted binary, the concern is apps using dynamic code loading to bypass review for functionality Apple never actually got to evaluate. Reviewers have specifically flagged apps where an OTA update introduced a materially new feature shortly after approval. The safe pattern is treating OTA updates strictly as bug fixes and minor content or copy adjustments, and submitting an actual new build through review for anything that changes what the app fundamentally does.
How to prepare for a React Native interview in 2026
Skip another slide of "here's the component lifecycle." Build one small thing that forces you into the mobile-specific parts: a screen with a long FlatList of real remote data with images, profiled with Flipper until you can explain exactly where the frame time goes. Add a gesture-driven animation with Reanimated and explain out loud why it stays smooth even when the JS thread is busy. Write one small TurboModule, even a trivial one, wrapping a native API that has no existing RN package, just so you've actually seen the JSI boundary from both sides instead of only reading about it.
Across React Native mock interviews run through LastRoundAI, the FlatList performance question and the bridge-versus-JSI question trip up more otherwise-strong candidates than any generics or hooks question does, mostly because most day-to-day RN work never forces you to think about the runtime underneath your components. That's the exact gap a good mock interview is meant to expose before a real one does.
One more thing worth knowing going into 2026: the New Architecture question isn't going away as a "someday" topic. Meta made Fabric and TurboModules the default in 0.76, and an increasing share of the ecosystem's popular libraries, Reanimated, react-native-screens, react-native-gesture-handler, have dropped legacy-bridge support entirely in their current majors. An interviewer who asks "have you migrated to the New Architecture" isn't asking a trivia question anymore, they're asking whether your production experience is current.
Get the reps in before the real thing
LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, including systems-style questions about the bridge, JSI, and list performance that are hard to rehearse just by reading. The free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $29/mo (or $19/mo on the 3-month plan) if a handful of sessions isn't enough runway before a loop.
Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough mobile and full-stack roles that actually test React Native depth instead of treating it as generic React. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com. That's the only inbox we check.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
Do I need hands-on React Native experience to pass?
It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.
Is React Native still worth learning in 2026?
For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.
Should I memorise React Native syntax for the interview?
Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.
What is the most common mistake in React Native interviews?
Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.
