Blog55 min read

160+ React Native Interview Questions and Answers (2026)

160+ React Native interview questions and answers for 2026

These are the React Native interview questions I’d study before a mobile loop in 2026: 160 of them, grouped by topic and ordered from basic to senior. Every question has an answer, and the ones where people usually slip have a note on what the interviewer is actually checking.

React Native interviews changed a lot in the last two years. The New Architecture (JSI, Fabric, and TurboModules) became the default in React Native 0.76, Expo became the recommended way to start a project, and the old bridge is gone. Interviewers use those topics to tell whether your knowledge is current, so this list covers them in depth. The answers follow the official React Native documentation.

What to Expect

  • 11 sections, from fundamentals to native modules, security, and shipping apps to the stores.
  • Direct answers first, then the detail that makes an answer strong: code, trade-offs, and pitfalls.
  • React Native coding interview questions and production scenarios for experienced and senior developers.
  • Links to FrontendLead practice problems wherever a React or JavaScript concept carries over.

React Native interviews also test plain React and JavaScript. If those are rusty, pair this list with the React interview questions and JavaScript interview questions guides.

Jump to a section

React Native Fundamentals

Diagram: React Native renders one React codebase to native iOS and Android views

Find in-depth explanations and practice here: React Interview Questions and Answers ->

Fundamentals questions open almost every React Native interview. They’re quick to answer, but a vague answer here sets a junior tone for the rest of the round.

1. What is React Native?

React Native is a framework for building iOS and Android apps with React and JavaScript (or TypeScript). You write components like <View> and <Text>, and React Native renders them as real native views: a UIView on iOS and an android.view.View on Android. That’s the key difference from hybrid tools like Cordova or Capacitor, which render HTML inside a WebView.

2. How is React Native different from React on the web?

The React part is the same: components, props, state, hooks, and the same reconciler. What changes is the renderer and the platform:

  • There’s no DOM, so no <div> or <span>. You use <View>, <Text>, and other core components.
  • There’s no CSS. Styles are JavaScript objects, layout is Flexbox only, and styles don’t cascade (except text styles inside nested <Text>).
  • Navigation, storage, gestures, and animations come from libraries instead of the browser.
  • You ship through the App Store and Google Play, with code signing and review.

3. How does React Native compare with Flutter?

React Native Flutter
Language JavaScript / TypeScript Dart
Rendering Real native platform views Draws its own widgets with its own rendering engine
Look and feel Matches each platform by default Pixel-identical on both platforms
Code reuse with web High: React skills, libraries, and logic carry over Low, unless you also use Flutter for web
Hiring Large JavaScript and React talent pool Smaller Dart pool

A good answer picks based on the team: a company with React web engineers usually gets more out of React Native.

4. When would you choose fully native (Swift or Kotlin) over React Native?

When the app is mostly platform-specific work: heavy 3D or games, advanced camera or audio processing, deep OS integrations (widgets, watch apps, car integrations) that dominate the product, or a team that’s already all native. React Native wins when you want one team shipping the same product to both platforms quickly. Many large apps mix both: native shells and performance-critical screens, with React Native for feature screens.

5. What are the core components?

The building blocks that map to native views:

  • View: the layout container, like a div.
  • Text: all text must be inside it.
  • Image: local and remote images.
  • TextInput: keyboard input.
  • ScrollView, FlatList, SectionList: scrolling content and lists.
  • Pressable: touch handling.
  • Modal, Switch, ActivityIndicator, StatusBar.

6. What is Expo, and should you use it?

Expo is a framework and toolset on top of React Native: a large set of maintained native modules (camera, notifications, secure storage), Expo Router for file-based navigation, config plugins for native setup, and EAS for cloud builds, store submission, and over-the-air updates. The React Native docs now recommend starting new apps with a framework like Expo. The old reason to avoid it, that you couldn’t add custom native code, no longer applies: development builds and config plugins support any native module.

7. What’s the difference between Expo Go and a development build?

Expo Go is a prebuilt app from the stores that runs your JavaScript, but it only includes the native modules Expo ships with. A development build is your own debug version of the app, compiled with whatever native modules you add. Expo Go is great for learning and quick prototypes; real projects use development builds as soon as they need a native library that Expo Go doesn’t include.

8. What is Metro?

Metro is React Native’s JavaScript bundler. It transforms your code with Babel, resolves modules (including platform extensions like .ios.tsx), serves the bundle to the app during development with Fast Refresh, and produces the production bundle that ships inside the app. It’s the mobile equivalent of webpack or Vite.

9. What is Hermes?

Hermes is the JavaScript engine built for React Native, and it’s the default. Its big advantage is ahead-of-time compilation: your JavaScript is compiled to bytecode at build time, so the app doesn’t parse and compile JavaScript on launch. That means faster startup, lower memory use, and a smaller download on Android. It also supports debugging through React Native DevTools.

10. Does React Native use a virtual DOM?

Not a DOM, because there’s no DOM on mobile. React still keeps its tree of elements and reconciles changes the same way it does on the web. With the New Architecture, the Fabric renderer turns the result into a shadow tree in C++, computes layout with Yoga, and then applies the changes to native views. Interviewers ask this to catch people repeating “it uses a virtual DOM” without understanding the rendering pipeline.

The React reconciliation page explains the diffing part, which is the same on both platforms.

11. How do you write platform-specific code?

Three options, from smallest to largest difference:

import { Platform } from 'react-native';

// 1. Inline checks
const padding = Platform.OS === 'ios' ? 20 : 16;

// 2. Platform.select
const styles = StyleSheet.create({
  card: Platform.select({
    ios: { shadowOpacity: 0.2, shadowRadius: 6 },
    android: { elevation: 4 },
  }),
});

// 3. Platform-specific files: Metro picks the right one
// DatePicker.ios.tsx and DatePicker.android.tsx
import DatePicker from './DatePicker';

12. What is Fast Refresh?

Fast Refresh reloads edited modules instantly while keeping component state. If you edit a component file, React re-renders it with the new code and preserves its state; if you edit a file that isn’t a component (or the edit has a syntax error), it falls back to a full reload of the JavaScript. It’s on by default in development.

13. What happens when a React Native app starts?

  1. The native app launches and shows the splash screen.
  2. It starts the JavaScript engine (Hermes) and loads the bundle: bytecode embedded in the app in production, or served by Metro in development.
  3. The bundle runs and registers the root component (AppRegistry.registerComponent, which Expo does for you).
  4. React renders the tree, Fabric creates native views, and the app hides the splash screen.

Startup optimization means shortening steps 2 to 4: Hermes bytecode, lazy-loaded modules, and less work in the first render.

14. Can you share code between React Native and a React web app?

Yes, at two levels. Business logic, hooks, API clients, validation, and state stores share easily in a monorepo because they’re plain TypeScript. UI can be shared with React Native Web, which renders React Native components to the DOM; Expo supports web out of the box. In practice, teams share logic everywhere and share UI only where the design is the same on both.

15. What are React Native’s main limitations?

  • You still need native knowledge for builds, signing, permissions, and debugging native crashes.
  • New OS features arrive in native SDKs first, so you wait for a library or write a native module.
  • Performance-sensitive work (complex animations, huge lists) needs care to keep the JavaScript thread free.
  • Upgrades between React Native versions can be work, especially with many native dependencies.

A senior answer adds how you’d mitigate each one, for example by using Expo to reduce native maintenance.

Architecture, Threads, and the New Architecture

Diagram: the React Native New Architecture, with JavaScript calling native code through JSI, Fabric rendering, TurboModules, and Codegen

Find in-depth explanations and practice here: The JavaScript Event Loop ->

This is the section that separates current knowledge from outdated tutorials. If you only remember one thing: the bridge is gone, and JSI, Fabric, TurboModules, and Codegen replaced it.

16. Which threads does a React Native app use?

  • Main (UI) thread: the native thread that draws views and handles touches. If it’s blocked, the app freezes.
  • JavaScript thread: runs your React code, event handlers, and business logic. If it’s busy, taps and JS-driven animations lag, but native scrolling keeps working.
  • Background threads: layout calculation and native module work can run off the main thread.

Most performance answers come back to one rule: keep the JavaScript thread free and never block the UI thread.

17. What was the bridge, and why was it replaced?

In the old architecture, JavaScript and native code talked through an asynchronous bridge: every call was serialized to JSON, queued, and sent across in batches. That caused three problems: serialization overhead on large payloads, no synchronous calls (so you couldn’t measure a view and use the result in the same frame), and congestion when lots of messages crossed at once, such as during scroll events. The New Architecture removed the bridge, and since React Native 0.82 there’s no option to turn the old architecture back on.

18. What is JSI?

The JavaScript Interface is a C++ API that lets JavaScript hold references to C++ objects and call their methods directly. No JSON serialization and no queue: calls can be synchronous or asynchronous. JSI is the foundation the rest of the New Architecture is built on, and it’s also engine-agnostic, which is how Hermes plugs in.

19. What is the New Architecture?

Four pieces that replaced the bridge:

Piece What it does
JSI Direct JavaScript-to-C++ calls, no serialization
Fabric The new renderer, with a C++ core shared by iOS and Android
TurboModules Native modules that load lazily and are called through JSI
Codegen Generates type-safe native glue code from TypeScript or Flow specs

It became the default in React Native 0.76. The architecture docs go deeper.

20. What is Fabric?

Fabric is the rendering system. It keeps the shadow tree in C++ (shared across platforms instead of separate Java and Objective-C implementations), can measure and lay out views synchronously, and supports React’s concurrent features. Practical wins: useLayoutEffect can read a view’s size and update before the user sees a frame, so tooltips and popovers no longer flicker into position.

21. What are TurboModules?

TurboModules are the new native module system. Two differences from legacy native modules: they load lazily, the first time JavaScript uses them, instead of all at startup (faster launch), and they’re called through JSI with a typed interface, so calls can be synchronous when that makes sense, such as reading a value from fast storage.

22. What is Codegen?

Codegen reads a typed spec of your native module or component, written in TypeScript or Flow, and generates the C++, Objective-C++, and Java/Kotlin boilerplate that connects it to JavaScript. Type mismatches between JavaScript and native code show up at build time instead of as runtime crashes.

// NativeDeviceInfo.ts: a TurboModule spec
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getBatteryLevel(): number;            // synchronous
  getDeviceName(): Promise<string>;     // asynchronous
}

export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');

23. What is bridgeless mode?

Bridgeless mode is the New Architecture running without initializing the legacy bridge at all. Before it, even New Architecture apps started the bridge for things like timers and error handling. Since React Native 0.74 it’s on by default with the New Architecture, which removes the last of the bridge’s startup cost.

24. What is Yoga?

Yoga is the cross-platform layout engine, written in C++, that implements a subset of Flexbox. React Native passes it your style props, Yoga computes the position and size of every view, and the result is applied to native views. That’s why layout in React Native behaves the same on iOS and Android, and why only Flexbox (plus absolute positioning) is available.

25. What happens when the JavaScript thread is blocked?

Anything that needs JavaScript stops responding: button presses queue up, JS-driven animations freeze, and state updates don’t render. Native scrolling and native-driven animations keep running because they live on the UI thread. Common causes are heavy computation in render, parsing large JSON, synchronous loops over big arrays, and too many re-renders. Fixes: memoize, paginate, move work to a native module or a worklet, or split work into chunks.

26. How do you tell whether a performance problem is on the JS thread or the UI thread?

Open the Perf Monitor from the Dev Menu. It shows two frame rates: JS and UI. If the JS frame rate drops while the UI stays at 60, your JavaScript is doing too much (re-renders, heavy logic). If the UI frame rate drops, the native side is struggling: too many views, expensive shadows or transparency, or huge images being decoded. Always test in a release build, because development mode is much slower.

27. Which React features does the New Architecture unlock?

Concurrent React: startTransition and useTransition for interruptible updates, useDeferredValue, Suspense for data fetching, and automatic batching everywhere. Plus synchronous layout reads in useLayoutEffect. On the old architecture, most of these either didn’t work or worked only partially.

28. How do you migrate an app to the New Architecture?

  1. Upgrade React Native (and Expo SDK) to a recent version.
  2. Audit native dependencies: check that each library supports the New Architecture (the React Native Directory shows this) and upgrade or replace the ones that don’t.
  3. Move your own native modules to TurboModules and native components to Fabric components with Codegen specs.
  4. Test heavily on both platforms, especially custom native views, layout timing, and anything that relied on bridge timing.

29. What is the interop layer?

A compatibility layer that lets many legacy native modules and components run on the New Architecture without being rewritten. It made the migration gradual: apps could switch on the New Architecture while some dependencies were still on the old APIs. It doesn’t cover everything, so libraries that do unusual things with the bridge still need updates.

30. Why do synchronous native calls matter?

Some things are awkward when every native call is asynchronous: reading a stored setting before the first render, measuring a view to position a tooltip in the same frame, or checking a feature flag at startup. With JSI, libraries like MMKV read storage synchronously, so the first render already has the data and you skip a loading flash. Use synchronous calls only for fast operations, because a slow synchronous call blocks the JavaScript thread.

31. What is view flattening?

An optimization in the renderer: views that exist only for layout and don’t draw anything (no background, border, or touch handling) are removed from the native view hierarchy. Fewer native views means less memory and faster drawing, so wrapping components in an extra View for layout is cheaper than it looks. Fabric does this on both platforms.

32. How are JavaScript errors and native crashes different?

A JavaScript error shows a red error screen in development, and in production an uncaught error can close the app. You can catch rendering errors with error boundaries and report them. A native crash, such as a null pointer in Java or a bad memory access in Objective-C, kills the process immediately, and JavaScript can’t catch it. You need native crash reporting (Sentry, Firebase Crashlytics) with symbol files uploaded to read those stack traces.

33. What is LogBox?

LogBox is the in-app tool that shows warnings and errors during development: warnings as a yellow notification at the bottom of the screen and errors as a full-screen red overlay with a symbolicated stack trace and the component stack. You can silence specific noisy warnings with LogBox.ignoreLogs([...]), but fix them instead whenever you can.

34. How do you check that an app is running on Hermes?

const isHermes = () => typeof HermesInternal === 'object' && HermesInternal !== null;
console.log(isHermes() ? 'Hermes' : 'another engine');

React Native DevTools also shows the engine. Hermes is the default for both iOS and Android, so you’d only see something else in an older or custom setup.

35. How does a state update reach the screen?

  1. Render: React calls your components and produces the new element tree (JavaScript thread).
  2. Commit: Fabric builds the new shadow tree in C++ and Yoga computes layout.
  3. Mount: the changes are applied to native views on the UI thread.

Knowing the three phases lets you reason about where a slowdown lives: slow render functions, expensive layout, or too many native view changes.

Components, Styling, and Layout

Diagram: React Native Flexbox defaults, a column layout where a flex: 1 view fills the space between a header and a footer

Find in-depth explanations and practice here: React Coding Questions ->

Web developers usually trip on styling first: Flexbox defaults are different, nothing cascades, and every platform handles shadows, keyboards, and notches its own way.

36. How does styling work in React Native?

Styles are JavaScript objects with camelCased properties, passed through the style prop. There are no selectors, no cascade, and no units; numbers are density-independent pixels. You can pass an array, and later entries override earlier ones, which is how you apply conditional styles.

const styles = StyleSheet.create({
  button: { paddingVertical: 12, borderRadius: 8, backgroundColor: '#2563eb' },
  disabled: { opacity: 0.5 },
});

<Pressable style={[styles.button, disabled && styles.disabled]} />

37. Why use StyleSheet.create instead of inline style objects?

Mostly for organization and stable references: styles are defined once outside the component, so they’re not recreated every render and don’t break memoized children that receive them. In development, it also validates property names. The performance difference against inline objects is small today, so inline styles are fine for one-off dynamic values like a computed width.

38. How is Flexbox different in React Native?

Property Web default React Native default
flexDirection row column
flexShrink 1 0
alignContent normal (stretch) flex-start
flex Shorthand for grow, shrink, and basis A single number
display Many values Only flex or none

Every View is a flex container by default, so there’s no display: flex to add.

39. What does flex: 1 mean in React Native?

A positive flex number makes the view grow to fill the available space along the parent’s main axis, shared in proportion with siblings. Two children with flex: 1 and flex: 2 split the space one third and two thirds. flex: 1 on a screen’s root view is how you make it fill the screen. A common bug is a parent without a size or flex, so the child has nothing to fill and collapses to zero height.

40. What units does React Native use?

Plain numbers are density-independent pixels (points on iOS, dp on Android), so width: 100 looks about the same physical size on every screen. Percentage strings like '50%' work for dimensions, margins, and positions. When you need real pixels, for example for a hairline border or image resolution, use PixelRatio.get() or StyleSheet.hairlineWidth.

41. How do you build layouts that work on phones and tablets?

  • Use Flexbox and percentages instead of fixed widths.
  • Read the window size with useWindowDimensions(), which updates on rotation and split-screen, and switch layouts at breakpoints (for example, two columns above 768 points).
  • Test landscape, large text settings, and small devices.
function ProductGrid({ products }) {
  const { width } = useWindowDimensions();
  const columns = width >= 768 ? 3 : 2;
  return <FlatList key={columns} numColumns={columns} data={products} renderItem={renderProduct} />;
}

The key forces a fresh list when the column count changes, because FlatList doesn’t support changing numColumns on the fly.

42. Why must all text be inside a Text component?

Native platforms render text with dedicated text views, and a plain View can’t draw strings. Rendering a raw string inside a View throws an error. Text is also the only place where styles are inherited: a nested <Text> inherits the font, color, and size of its parent <Text>, which is how you bold one word in a sentence.

43. How do you handle notches and safe areas?

Use react-native-safe-area-context: wrap the app in SafeAreaProvider, then use the SafeAreaView from that library or the useSafeAreaInsets() hook to pad content away from the notch, status bar, and home indicator. The SafeAreaView built into React Native is iOS-only and deprecated, so use the library. Navigation libraries already handle insets for headers and tab bars.

44. How do you stop the keyboard from covering inputs?

  • KeyboardAvoidingView adjusts padding or height when the keyboard opens; use behavior="padding" on iOS and test Android separately, where the window often resizes on its own.
  • Put forms inside a ScrollView with keyboardShouldPersistTaps="handled", so tapping a button doesn’t just dismiss the keyboard.
  • For complex forms and chat screens, react-native-keyboard-controller gives consistent behavior on both platforms and animates with the keyboard.

45. Should you use Pressable or TouchableOpacity?

Pressable. It’s the newer, more flexible API: its style and children can be functions of the pressed state, it supports onLongPress, hitSlop, and android_ripple, and it’s what the docs recommend for new code. TouchableOpacity and friends still work, and you’ll see them in older codebases.

<Pressable
  onPress={save}
  style={({ pressed }) => [styles.button, pressed && { opacity: 0.7 }]}
  accessibilityRole="button"
>
  <Text>Save</Text>
</Pressable>

46. How do you make a small icon easier to tap?

Use hitSlop to extend the touchable area without changing the layout, for example hitSlop={12} or hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}. Platform guidelines recommend touch targets of about 44 by 44 points on iOS and 48 by 48 dp on Android, which also matters for accessibility.

47. What do you need to know about images?

  • Remote images need an explicit size (width and height, or aspectRatio), because React Native can’t know the size before the download.
  • resizeMode (cover, contain, stretch) controls how the image fits.
  • Serve images close to their display size; decoding a 4000-pixel photo for a thumbnail wastes memory.
  • For disk caching, placeholders, and transitions, use a library like expo-image.

48. How do shadows differ between iOS and Android?

Historically, iOS used shadowColor, shadowOffset, shadowOpacity, and shadowRadius, while Android only supported elevation, which gives a Material-style shadow with little control. On the New Architecture, React Native supports the CSS-like boxShadow style on both platforms, which makes cross-platform shadows much easier. Shadows are expensive to draw, so avoid them on every row of a long list.

49. When should you use ScrollView and when FlatList?

ScrollView renders all of its children at once, which is fine for a screen of content with a known, small number of items. FlatList renders items lazily and unmounts the ones far off screen, so use it for long or unbounded lists, like feeds, search results, and chat. Mapping 500 items inside a ScrollView is a classic performance bug.

50. How do you support dark mode?

Read the system theme with useColorScheme(), which returns 'light' or 'dark' and updates live. Keep colors in a theme object, provide it through context, and pick the palette from the scheme (plus a user override if the app has one). Also set the status bar style and the native splash and navigation bar colors, which aren’t controlled by your React tree.

51. How do you make a React Native app accessible?

  • Give interactive elements a role and a label: accessibilityRole="button" and accessibilityLabel="Delete message" (or the role and aria-label props).
  • Expose state with accessibilityState, like { checked: true } or { disabled: true }.
  • Group related elements with accessible so screen readers read them as one item.
  • Support large font sizes and test with VoiceOver on iOS and TalkBack on Android.

52. How do you use custom fonts?

With Expo, load them with expo-font (the useFonts hook or the config plugin, which embeds fonts at build time). In bare React Native, add the font files as native assets and link them. The fontFamily value must match the font’s name, which can differ between iOS (the PostScript name) and Android (the file name). Keep the splash screen visible until the fonts load to avoid a flash of the wrong font.

53. How does absolute positioning work?

position: 'absolute' takes the view out of the flex flow and positions it with top, right, bottom, and left relative to its parent (every view is positioned relative by default). Use zIndex to control stacking among siblings; on Android, elevation also affects stacking order, which surprises people when a shadowed card covers a dropdown.

54. How would you build a themed component library?

Define design tokens (colors, spacing, radii, typography) in one place, expose the active theme through context, and build primitives like Box, Text, and Button with typed variant props instead of raw styles. Memoize style creation per theme so components don’t rebuild styles every render. Libraries like Restyle, Tamagui, and Unistyles package this pattern; a senior answer explains the trade-off between a library and a small in-house system.

55. Can you use Tailwind in React Native?

Yes, with NativeWind, which compiles Tailwind classes to React Native styles: <View className="flex-1 items-center p-4">. Teams like it for sharing a design language with a Tailwind web app. The trade-offs are an extra build step and another layer to debug, and not every web utility makes sense on native.

Lists and Performance

Diagram: FlatList virtualization renders only a window of rows around the viewport and unmounts the rest

Find in-depth explanations and practice here: Frontend Performance Optimizations ->

Performance questions dominate mid-level React Native interviews, and most of them are about lists. The pattern interviewers want: measure in a release build first, find which thread is struggling, then apply the cheapest fix.

56. How does FlatList work?

FlatList is built on VirtualizedList. Instead of rendering every item, it renders a window of items around the visible area and replaces items far outside it with empty space of the same size. As you scroll, it renders new items in batches and unmounts old ones. That keeps memory flat for long lists, at the cost of render work while scrolling, which is why fast scrolling can show blank areas.

57. Why does keyExtractor matter?

Keys tell React which item is which between renders. With stable, unique keys (like an ID from the server), inserting an item at the top only renders the new row. With index keys, every row below shifts its key, so React re-renders them and can mix up local state, such as a half-typed input in the wrong row. FlatList uses item.key or item.id by default; pass keyExtractor otherwise.

58. What does getItemLayout do?

It tells FlatList each item’s size and position up front, so it doesn’t have to measure items after rendering them. That makes scrollToIndex reliable and scrolling smoother. It only works when items have a fixed (or calculable) height.

const ROW_HEIGHT = 72;

<FlatList
  data={contacts}
  renderItem={renderContact}
  getItemLayout={(data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index })}
/>

59. What do initialNumToRender, maxToRenderPerBatch, and windowSize control?

  • initialNumToRender: how many items render on the first pass. Make it just enough to fill the screen, so the first render is fast.
  • maxToRenderPerBatch: how many items render per batch while scrolling. Higher fills blank areas faster but blocks the JavaScript thread longer.
  • windowSize: how many screens’ worth of items stay mounted (default 21: 10 above, 10 below, plus the visible one). Smaller saves memory but shows more blank areas on fast scrolls.

Tune them after measuring; the defaults are reasonable for most lists.

60. What does removeClippedSubviews do?

It detaches off-screen item views from the native view hierarchy, which reduces the work the UI thread does while scrolling. It can help very long lists with heavy rows, especially on Android, but it has had bugs with content disappearing in some layouts, so turn it on only after measuring and test it.

61. Why should renderItem be stable and list items memoized?

When the parent re-renders, an inline renderItem arrow function is a new function every time, and unmemoized rows re-render even when their data didn’t change. With hundreds of mounted rows, that’s a lot of wasted work on the JavaScript thread.

const MessageRow = memo(function MessageRow({ message, onPress }) {
  return (
    <Pressable onPress={() => onPress(message.id)}>
      <Text>{message.text}</Text>
    </Pressable>
  );
});

function Inbox({ messages, openMessage }) {
  const renderItem = useCallback(
    ({ item }) => <MessageRow message={item} onPress={openMessage} />,
    [openMessage]
  );
  return <FlatList data={messages} renderItem={renderItem} />;
}

62. What is FlashList, and how is it different from FlatList?

FlashList, from Shopify, has the same API shape as FlatList but recycles item views: when a row scrolls off screen, its component instance is reused for a row coming on screen, with new props, instead of being unmounted and a new one mounted. Less mounting means fewer blank areas and lower CPU use, especially on low-end Android devices. The catch: because components are reused, local state in a row can leak into another row unless you reset it when the item changes.

63. Why do blank areas appear when scrolling fast?

The user scrolls faster than the JavaScript thread can render new batches, so the list shows empty space until items catch up. Fixes, in order: make rows cheaper (memoize, fewer views, smaller images), keep the JavaScript thread free during scroll, provide getItemLayout, increase windowSize or maxToRenderPerBatch carefully, or switch to FlashList.

64. How do you implement infinite scroll?

Use onEndReached with onEndReachedThreshold (a fraction of the visible length, for example 0.5 means “half a screen from the end”), and guard against duplicate calls, because onEndReached can fire several times.

const loadMore = useCallback(() => {
  if (isLoading || !nextCursor) return; // already loading, or no more pages
  fetchPage(nextCursor);
}, [isLoading, nextCursor]);

<FlatList
  data={posts}
  renderItem={renderPost}
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
  ListFooterComponent={isLoading ? <ActivityIndicator /> : null}
/>

Practice the same pattern on the web with the infinite scrolling news feed problem.

65. How do you add pull to refresh?

Pass refreshing and onRefresh to FlatList (or a RefreshControl to a ScrollView). Set refreshing to true while reloading the first page, replace the data, and set it back to false. Reset the pagination cursor too, otherwise the next onEndReached loads a stale page.

66. When do you use SectionList?

For grouped data with headers, like contacts by letter or transactions by day. It takes sections (each with a title and data), renders renderSectionHeader, and supports sticky headers with stickySectionHeadersEnabled (on by default on iOS). It’s virtualized like FlatList, so the same optimization advice applies.

67. How do you avoid unnecessary re-renders?

  • Keep state as close as possible to where it’s used, so updates re-render a small subtree.
  • Split contexts by how often they change, or use a store with selectors, so a fast-changing value doesn’t re-render every consumer.
  • Memoize expensive children with memo and pass them stable props (useCallback, useMemo).
  • Don’t store derived data in state; compute it during render.

More on the context part in avoiding context re-render pitfalls.

68. Does React Compiler change memoization advice?

Yes. React Compiler automatically memoizes components and values at build time, and React Native and Expo support it. With it enabled, most manual useMemo, useCallback, and memo calls become unnecessary. You still need to understand why re-renders happen, because the compiler skips code that breaks the rules of React, and list performance still depends on how expensive each row is.

69. How do you find what’s re-rendering?

Use the Profiler in React Native DevTools: record an interaction, then see which components rendered, how long each took, and why (props, state, context, or parent). Turn on “highlight updates” to see re-renders on screen as you use the app. In older setups, why-did-you-render logs avoidable re-renders.

70. How do you improve app startup time?

  1. Measure cold start in a release build on a mid-range Android device.
  2. Make sure Hermes is enabled, so the app ships bytecode.
  3. Defer work: load non-critical modules lazily, delay analytics and SDK setup until after the first screen, and avoid large synchronous work at import time.
  4. Make the first screen cheap: fewer components, cached data instead of waiting on the network, smaller images.
  5. Audit native SDKs that initialize at launch; TurboModules already load lazily.

71. What are inline requires?

A Metro transform option that delays executing a module until the code that uses it actually runs, instead of executing every import when the bundle loads. Screens and libraries that aren’t needed at launch don’t cost anything until they’re used, which speeds up startup in large apps. It’s the inlineRequires option in the transformer section of the Metro config. The trade-off: code that relies on import side effects running at startup may need an explicit import.

72. How do you reduce app size?

  • Ship an Android App Bundle so Google Play delivers only the code and resources each device needs.
  • Enable R8 shrinking and resource shrinking on Android.
  • Remove unused native dependencies, fonts, and large images; compress the images you keep.
  • Download large, rarely used assets at runtime instead of bundling them.

73. How do you profile a release build?

Development builds are much slower, so profile release or profiling builds. Use the Perf Monitor for JS and UI frame rates, the React Native DevTools Profiler for React renders, Xcode Instruments (Time Profiler, Allocations) on iOS, and the Android Studio profiler or Perfetto on Android for native CPU, memory, and frame timing.

74. Why shouldn’t you judge performance in development mode?

Development builds run extra checks (prop type warnings, strict mode double-invoking, LogBox), don’t minify the bundle, and load JavaScript from Metro instead of precompiled bytecode. An app that stutters in development can be smooth in release, and the opposite problem (a real issue hidden by noise) happens too. Always confirm with a release build on a real, mid-range device.

75. How do you run heavy computation without freezing the UI?

  • Do less: paginate, filter on the server, and memoize.
  • Split the work into chunks and yield between them (for example, process 500 items, then schedule the next chunk with setTimeout).
  • Use startTransition so React can interrupt a heavy re-render to handle input.
  • Move the work off the JavaScript thread: a native module, or a worklet-based library that runs JavaScript on another thread.

76. Does console.log affect performance?

Yes. In release builds, console calls still run and serialize their arguments, and logging large objects inside a scroll handler or a render can cost noticeable time. Remove them from production with babel-plugin-transform-remove-console, or route logs through a logger you can turn off.

77. How do you find and fix memory leaks?

The usual causes are subscriptions and timers that outlive their component: event listeners, setInterval, WebSocket handlers, and native event emitters without cleanup. Every subscription in an effect needs a cleanup function. To find leaks, take heap snapshots in React Native DevTools before and after navigating back and forth, and check native memory with Xcode’s memory graph or the Android Studio memory profiler.

useEffect(() => {
  const sub = AppState.addEventListener('change', handleAppState);
  return () => sub.remove(); // without this, every mount adds another listener
}, []);

The same causes apply in browsers; see handling memory leaks.

78. How do you optimize image-heavy screens?

Request images at the size you display them (most image CDNs resize by URL), use modern formats like WebP, cache them on disk, and prefetch the images for the next screen. Show a placeholder or blurhash while loading, and give every image a fixed size so the layout doesn’t jump. Decoded image memory is width times height times 4 bytes, so oversized images are the fastest way to run out of memory on Android.

79. How can concurrent features help in React Native?

With the New Architecture, useTransition and useDeferredValue work in React Native. A classic use is search-as-you-type over a large list: keep the input responsive by updating the text immediately and marking the expensive filtered list as a deferred or transition update, so React can interrupt it when the user types again.

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterProducts(products, deferredQuery), [products, deferredQuery]);

80. How do you measure performance in production?

Track a few metrics from real devices: cold and warm start time, time to interactive for key screens, slow and frozen frames, and JavaScript error and crash rates. Sentry, Firebase Performance Monitoring, and Datadog all provide these for React Native. A senior answer also mentions segmenting by device class, because the median iPhone hides how the app feels on a cheap Android phone.

State Management and Data

Diagram: React Native state layers, local state, global client state, server cache, and persistent storage

Find in-depth explanations and practice here: React Context and the Provider Pattern ->

State management in React Native is mostly React state management, plus three mobile concerns: persisting data between launches, working offline, and reacting to the app going to the background.

81. How is state management different in React Native than on the web?

The tools are the same: useState, context, Redux Toolkit, Zustand, TanStack Query. The differences are around the platform. Apps are killed and restarted by the OS, so important state must persist. Connectivity comes and goes, so apps need offline handling. And apps move between foreground and background, so data should refresh when the user returns. Interviewers want to hear those three concerns, not a list of libraries.

82. What’s the difference between server state and client state?

Server state is data the server owns (the feed, the profile, orders): it’s async, can go stale, and needs caching, refetching, and deduplication. Client state is data the app owns (a selected tab, a draft message, theme). Mixing them in one Redux store is a common source of complexity. A modern setup uses TanStack Query (or RTK Query) for server state and a small store or context for client state.

83. When would you use Context, Redux Toolkit, or Zustand?

Tool Good for Watch out for
Context Values that rarely change: theme, current user, locale Every consumer re-renders when the value changes
Zustand Small to medium client state with minimal boilerplate Fewer conventions for large teams
Redux Toolkit Large apps and teams that want strict patterns, middleware, and devtools More code and concepts

Both Zustand and Redux use selectors, so components only re-render when the slice they read changes.

84. How do you persist state across app restarts?

Use your store’s persistence middleware with a storage engine: persist in Zustand, redux-persist for Redux, or the persister plugins for TanStack Query. Persist only what you need (not loading flags or errors), version the persisted shape, and write a migration when it changes, or an app update will load old data into a new shape.

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const useSettings = create(
  persist(
    (set) => ({
      theme: 'system',
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'settings', version: 1, storage: createJSONStorage(() => AsyncStorage) }
  )
);

85. What is AsyncStorage, and what are its limits?

AsyncStorage is a simple, asynchronous, unencrypted key-value store for strings. It’s fine for small settings and cached data. Its limits: every read is asynchronous (so you can’t read it before the first render), values must be serialized to strings, it gets slow with large data, and it’s not encrypted, so tokens and personal data don’t belong there. That last one is the most common mistake interviewers check for.

86. How does MMKV compare with AsyncStorage?

MMKV is a key-value store originally from WeChat, exposed to React Native through JSI. It’s synchronous and much faster than AsyncStorage, supports multiple instances, and can encrypt its data. Because reads are synchronous, you can hydrate state before the first render and avoid a loading flash. It’s a common choice for persisted stores and caches. It still isn’t a replacement for the Keychain or Keystore for secrets, because its encryption key has to live somewhere.

87. How do you store larger structured data?

Use a database: SQLite (for example expo-sqlite, or op-sqlite for speed), or a sync-focused layer on top of it like WatermelonDB. A database fits when you need queries, relations, thousands of records, or partial updates, such as an offline-first notes app or a message history. Key-value storage forces you to load and rewrite whole blobs, which gets slow fast.

88. How would you design an offline-first app?

  1. Make the local database the source of truth for the UI, so screens render instantly from local data.
  2. Write changes locally first, and add them to an outbox queue.
  3. Sync the queue to the server when online, with retries and backoff.
  4. Pull server changes since the last sync and merge them.
  5. Decide on conflict resolution up front: last write wins, field-level merge, or asking the user.

Then mention the edge cases: the app is killed mid-sync, the same change is sent twice (make writes idempotent with client-generated IDs), and the user logs out with unsynced data.

89. How do you detect connectivity?

Use @react-native-community/netinfo. Its isConnected says the device is on a network, and isInternetReachable says it can actually reach the internet; they differ on captive Wi-Fi portals. Don’t block requests just because NetInfo says offline. Try the request, handle the failure, and use connectivity events to retry queued work.

90. How do you react to the app moving to the background or foreground?

Listen to AppState, which reports active, background, and on iOS inactive. Typical uses: refetch data when the app becomes active, pause video and timers in the background, and hide sensitive screens in the app switcher. With TanStack Query, connect AppState to its focus manager so queries refetch when the user returns.

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

useEffect(() => {
  const sub = AppState.addEventListener('change', (status) => {
    focusManager.setFocused(status === 'active');
  });
  return () => sub.remove();
}, []);

91. How do you refresh data when a screen comes back into focus?

Screens in a stack stay mounted when you navigate away, so useEffect with an empty dependency array doesn’t run again when the user comes back. Use useFocusEffect from React Navigation, which runs when the screen gains focus and cleans up when it loses focus. Wrap the callback in useCallback, or it runs on every render.

useFocusEffect(
  useCallback(() => {
    refetchCart();
  }, [refetchCart])
);

92. How do you handle forms?

For anything beyond a couple of fields, use React Hook Form with a schema validator like Zod. React Hook Form keeps re-renders low by not storing every keystroke in React state, which matters because each re-render of a big form costs JavaScript thread time. Use Controller to connect TextInput, set the right keyboardType, autoComplete, and textContentType props, and move focus to the next field with returnKeyType="next" and refs.

93. How do you implement optimistic updates?

Update the UI immediately, send the request, and roll back on failure. With TanStack Query, you do it in onMutate: cancel in-flight queries, snapshot the cache, apply the change, and return the snapshot so onError can restore it. On mobile, optimistic updates matter even more than on the web, because slow and flaky networks are the norm.

94. How do you share data between screens?

Pass IDs, not objects, in navigation params: navigation.navigate('Product', { id }). The destination reads the full object from a shared cache (TanStack Query or a store). Passing whole objects makes deep links impossible (a URL can’t carry them), serializes large data into navigation state, and shows stale data when the object changes elsewhere.

95. How do you avoid a flash of default state while persisted state loads?

With async storage, the first render happens before the data loads, so the app briefly shows defaults, like a light theme flashing before dark or a login screen before the saved session. Fixes: keep the splash screen up until hydration finishes (expo-splash-screen‘s preventAutoHideAsync and hideAsync), or use synchronous storage like MMKV so the first render already has the data.

Diagram: nested React Native navigators, a root stack containing tabs, each tab with its own stack, opened by a deep link

Find in-depth explanations and practice here: Authentication Code Component ->

Navigation questions test whether you’ve built a real app: nested navigators, auth flows, and deep links that work when the app is cold-started from a notification.

96. What are the main navigation options in React Native?

  • React Navigation: the most widely used library, with stack, native stack, tabs, and drawer navigators configured in JavaScript.
  • Expo Router: file-based routing built on top of React Navigation. Files in the app/ folder become routes, and every screen gets a URL automatically.
  • React Native Navigation (by Wix): navigation implemented fully natively, used by some large apps.

New Expo projects default to Expo Router.

97. What’s the difference between the stack and native stack navigators?

The JavaScript stack animates screens with JavaScript-driven transitions, so it’s very customizable but can drop frames when the JavaScript thread is busy. The native stack uses the platform’s own navigation primitives through react-native-screens, which gives native transitions, gestures, and large titles on iOS, plus better performance. Use the native stack unless you need a transition it can’t do.

98. How do you combine stacks, tabs, and drawers?

By nesting navigators. A common structure is a root stack containing the tab navigator plus full-screen modals, with each tab holding its own stack so it keeps its history. Things to mention: each navigator has its own header and back behavior, navigating to a screen in another navigator uses nested params (navigate('Account', { screen: 'Settings' })), and deep nesting makes deep link configs and types harder to maintain.

99. How does Expo Router work?

Your file structure defines your routes: app/index.tsx is /, app/product/[id].tsx is /product/123, and _layout.tsx files define the navigator (stack, tabs) for a folder. Groups in parentheses, like (tabs), organize files without adding a URL segment. Because every screen has a URL, deep links and web support work without a separate linking config, and routes can be type-checked.

100. How do you type navigation params in TypeScript?

In React Navigation, declare a param list per navigator and use it in screen props, so navigate calls with missing or wrong params fail to compile:

type RootStackParamList = {
  Home: undefined;
  Product: { id: string };
};

type ProductProps = NativeStackScreenProps<RootStackParamList, 'Product'>;

function ProductScreen({ route }: ProductProps) {
  const { id } = route.params; // typed as string
}

Expo Router generates typed routes from the file system, so router.push('/product/123') is checked too.

101. How do you implement an authentication flow?

Render different screens based on auth state instead of navigating imperatively after login. When the user is signed out, only the auth screens exist; when they sign in, the app screens replace them. That way, the back button can’t return to the login screen, and a logged-out user can’t reach a protected screen through a deep link. In Expo Router, the same idea uses a protected group or a redirect in the layout.

<Stack.Navigator>
  {user ? (
    <Stack.Screen name="Home" component={HomeScreen} />
  ) : (
    <Stack.Screen name="SignIn" component={SignInScreen} />
  )}
</Stack.Navigator>

102. What is deep linking, and how do you set it up?

Deep linking opens a specific screen from a URL, like myapp://orders/42 from a notification or email. You register a URL scheme in the native config (Expo does it from app.json), then map URL paths to screens with React Navigation’s linking config, or let Expo Router map them from files. Test both cases: the app already running, and the app cold-started by the link.

Custom schemes (myapp://) can be claimed by any app and don’t work if the app isn’t installed. Universal links (iOS) and App Links (Android) use normal HTTPS URLs on your domain instead: the OS verifies that you own the domain (through an apple-app-site-association file and an assetlinks.json file on your server) and opens the app directly, falling back to the website when the app isn’t installed. Use them for anything shared publicly, like product links.

Save the intended destination, show the login screen, and send the user to the saved destination after they sign in. If you skip this, the user either lands on a protected screen without a session or loses the link entirely after logging in. Expo Router and React Navigation’s auth patterns make it straightforward: redirect to sign-in with the original path as a param.

105. How do you navigate from outside a component?

For things like a push notification handler or an API interceptor that sees a 401, use a navigation ref. Create it with createNavigationContainerRef(), pass it to NavigationContainer, and call navigationRef.navigate() after checking navigationRef.isReady(). In Expo Router, the router object can be imported and used anywhere.

106. How do you stop the user from leaving a screen with unsaved changes?

Use React Navigation’s usePreventRemove hook (or the beforeRemove event): when there are unsaved changes, it blocks the back gesture, header back button, and the Android back button, and you show a confirmation dialog. For other Android back button behavior, like closing a custom bottom sheet first, use BackHandler.

107. How do you keep navigation fast in a large app?

  • Use the native stack and react-native-screens, so inactive screens are native screens the OS can manage.
  • Tabs render lazily by default; keep it that way.
  • Consider freezeOnBlur, which stops re-rendering screens that aren’t visible.
  • Avoid heavy work in screen components during the transition; defer it until the animation finishes.

Animations and Gestures

Diagram: React Native animations running on the UI thread with Reanimated worklets while the JavaScript thread stays free

Find in-depth explanations and practice here: Animate an Element to the Right ->

Every animation question comes back to one idea: animations that run on the JavaScript thread stutter when JavaScript is busy, so smooth animations run on the UI thread.

108. What are your options for animations in React Native?

  • Animated: the built-in API, fine for simple transitions, especially with the native driver.
  • LayoutAnimation: animates the next layout change automatically.
  • Reanimated: the standard for complex, gesture-driven animations, running on the UI thread.
  • Lottie: plays designer-made vector animations exported from After Effects.

109. What does useNativeDriver do, and what are its limits?

With useNativeDriver: true, the Animated API sends the whole animation to the native side once, and it runs on the UI thread without touching JavaScript per frame. So it stays smooth even when the JavaScript thread is busy. The limit: it only supports non-layout properties, like transform and opacity. Animating width, height, top, or colors through the native driver isn’t supported.

const opacity = useRef(new Animated.Value(0)).current;

useEffect(() => {
  Animated.timing(opacity, { toValue: 1, duration: 250, useNativeDriver: true }).start();
}, []);

return <Animated.View style={{ opacity }}>{children}</Animated.View>;

110. What is Reanimated, and what are worklets?

Reanimated is an animation library that runs animation logic on the UI thread. A worklet is a small JavaScript function that the library runs in a separate JavaScript runtime on the UI thread, so your animation code (including gesture handling and interpolation) executes every frame without waiting on the main JavaScript thread. Unlike the native driver, it can animate any style property, including layout.

111. How do shared values and useAnimatedStyle work?

A shared value (useSharedValue) holds animation state that both the JavaScript thread and the UI thread can read and write. useAnimatedStyle is a worklet that maps shared values to styles and re-runs on the UI thread whenever they change. Changing a shared value doesn’t re-render the component, which is exactly why it’s fast.

const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));

<Pressable onPressIn={() => (scale.value = withSpring(0.95))} onPressOut={() => (scale.value = withSpring(1))}>
  <Animated.View style={[styles.card, animatedStyle]} />
</Pressable>

112. Why use React Native Gesture Handler instead of PanResponder?

PanResponder handles touches on the JavaScript thread, so drags lag when JavaScript is busy, and coordinating it with native scroll views is painful. Gesture Handler recognizes gestures natively, works with platform scroll views, lets you compose gestures (simultaneous, exclusive, or racing), and with Reanimated runs gesture callbacks as worklets on the UI thread.

113. How would you build a card that you can drag and that springs back?

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

function DraggableCard() {
  const offsetX = useSharedValue(0);

  const pan = Gesture.Pan()
    .onChange((event) => {
      offsetX.value += event.changeX; // runs on the UI thread
    })
    .onEnd(() => {
      offsetX.value = withSpring(0);
    });

  const style = useAnimatedStyle(() => ({ transform: [{ translateX: offsetX.value }] }));

  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={[styles.card, style]} />
    </GestureDetector>
  );
}

For swipe-to-dismiss, check the translation or velocity in onEnd: past a threshold, animate off screen and call a JavaScript callback with runOnJS to remove the item; otherwise spring back.

114. What is LayoutAnimation?

LayoutAnimation.configureNext() tells React Native to animate the next layout change: call it right before a state update, and views that move, appear, or resize animate to their new positions. It’s the cheapest way to animate an accordion opening or an item being removed from a short list. It applies to the whole next layout pass, so it’s hard to control in busy screens.

115. How do you animate items entering and leaving a list?

Reanimated’s layout animations: pass entering, exiting, and layout props to an Animated.View, for example entering={FadeIn} and exiting={FadeOut}, and the remaining items animate into place with layout={LinearTransition}. It’s a clean answer to “animate removing a row” without measuring positions yourself.

116. How do you keep animations smooth at 60 or 120 frames per second?

  • Run them on the UI thread (native driver or Reanimated).
  • Prefer animating transform and opacity, which avoid layout recalculation.
  • Don’t trigger React re-renders on every frame; shared values exist to avoid that.
  • Start heavy work, like data fetching for the next screen, after the transition, not during it.
  • Test on a low-end Android device in a release build.

117. How do you respect users who prefer reduced motion?

Check the system setting and tone down animations: AccessibilityInfo.isReduceMotionEnabled() with its change listener, or Reanimated’s useReducedMotion() hook. Replace large movements and parallax with simple fades or instant changes. It’s an accessibility requirement, and mentioning it unprompted is a good senior signal.

Native Modules and Platform APIs

Diagram: a typed JavaScript spec goes through Codegen to generate iOS and Android native module code

Find in-depth explanations and practice here: Host Objects vs Native Objects ->

You don’t need to be an iOS or Android expert for most React Native roles, but senior interviews expect you to know when native code is needed, how it connects to JavaScript, and how permissions, push notifications, and background work behave on each platform.

118. When do you need a native module?

When JavaScript can’t do the job: using a platform API that no library exposes (a new OS feature, a vendor SDK for payments or analytics), doing heavy work faster in native code (image processing, encryption), or reusing existing native code from a company’s native apps. Before writing one, check the React Native Directory and Expo’s modules; most common needs are already covered.

119. How do you write a native module today?

Two main routes:

  • TurboModule with Codegen: write a typed spec in TypeScript, run Codegen, and implement the generated interface in Objective-C++ or Swift on iOS and Kotlin or Java on Android.
  • Expo Modules API: write the module in Swift and Kotlin with a small declarative API; it works in any React Native app, not only Expo-managed ones.

Both run on the New Architecture. The Expo route is usually less boilerplate; the TurboModule route gives full control, including C++ modules shared across platforms.

120. What is the Expo Modules API?

A way to write native modules and views in Swift and Kotlin with a declarative syntax: you define functions, async functions, events, and views, and Expo generates the JavaScript bindings. It handles type conversion between JavaScript and native types, and the same structure works on both platforms, which makes native modules much more approachable for JavaScript-first teams.

// ios/BatteryModule.swift
public class BatteryModule: Module {
  public func definition() -> ModuleDefinition {
    Name("Battery")
    Function("getLevel") { () -> Float in
      UIDevice.current.isBatteryMonitoringEnabled = true
      return UIDevice.current.batteryLevel
    }
  }
}

121. How do you build a native UI component?

Write the native view on each platform, describe its props in a typed spec, and let Codegen generate the Fabric component glue (or define a view with the Expo Modules API). You’d do this to wrap a native-only view like a map, video player, or chart library that’s much faster natively. Keep the prop API small, and pass events back to JavaScript as callbacks, like onRegionChange.

122. What are config plugins and prebuild?

Expo’s prebuild generates the ios and android folders from your app config instead of you committing and hand-editing them (Expo calls this Continuous Native Generation). Config plugins are functions that modify the generated native projects, for example adding a permission string to Info.plist, a Gradle dependency, or an entitlement. The win: upgrading React Native means regenerating native projects instead of merging diffs by hand.

123. How does native code send events to JavaScript?

Through event emitters. The native module emits named events with a payload, and JavaScript subscribes with a listener that returns a subscription to remove in cleanup. Typed event emitters can be declared in TurboModule specs, and the Expo Modules API has an Events definition. Typical uses: download progress, Bluetooth device updates, and location changes.

124. How do you handle permissions?

  1. Declare them natively: usage description strings in Info.plist on iOS (the app crashes without them when you request) and entries in AndroidManifest.xml.
  2. Request at runtime, in context: ask for the camera when the user taps “Scan”, not on first launch, and explain why first.
  3. Handle every result: granted, denied, and permanently denied (“blocked”). When blocked, explain and offer Linking.openSettings().

Expo modules expose a consistent requestPermissionsAsync() for this; bare apps often use react-native-permissions.

125. How do push notifications work in React Native?

The app asks for permission and gets a device token from Apple Push Notification service (APNs) on iOS or Firebase Cloud Messaging (FCM) on Android, then sends that token to your server. Your server sends notifications through APNs and FCM (directly or through a service like Expo’s push service). In the app, you handle three states: foreground (decide whether to show it), background, and app killed (opened from a tap, where you read the initial notification and deep link to the right screen). Libraries: expo-notifications or Notifee with Firebase Messaging.

126. How do you run work in the background?

Carefully, because both platforms restrict it to save battery. iOS gives short, OS-scheduled windows for background refresh and won’t run your task on a fixed schedule. Android allows more through WorkManager and foreground services (with a visible notification for long work). In React Native, use libraries such as expo-background-task for periodic work, and native solutions for things like continuous location or audio playback. Never promise “runs every 15 minutes exactly” on iOS.

127. How do you add React Native to an existing native app?

This is called a brownfield integration. You add React Native as a dependency of the native project, create a React Native view or screen hosted inside a native view controller or activity, and pass initial props from native code. Teams usually start with one self-contained feature screen, share navigation and auth through native modules, and expand from there. Expo also supports integrating into existing native apps.

Networking and Security

Diagram: where React Native apps store data, AsyncStorage for settings and the Keychain or Keystore for tokens

Find in-depth explanations and practice here: API Retry ->

Security questions come up in most React Native interviews and dominate in fintech and health. The single most common wrong answer: storing tokens in AsyncStorage.

128. How do you make network requests, and what’s missing by default?

React Native includes fetch, XMLHttpRequest, and WebSocket, so fetch, axios, and TanStack Query all work. What’s missing: fetch has no timeout, so a request on a bad mobile connection can hang for a long time. Add one with AbortController, and retry idempotent requests with backoff.

async function fetchWithTimeout(url, ms = 10000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

Practice the retry part with the API retry coding question.

129. How do you refresh an expired access token?

Catch the 401, refresh the token, and retry the original request. The detail interviewers look for: when several requests fail at once, only one refresh should run, and the others should wait for it. Otherwise you send parallel refresh requests, and with rotating refresh tokens, all but one fail and log the user out.

let refreshPromise = null;

function refreshTokens() {
  if (!refreshPromise) {
    refreshPromise = fetch('https://api.example.com/auth/refresh', { method: 'POST' })
      .then((res) => {
        if (!res.ok) throw new Error('refresh failed');
        return res.json();
      })
      .finally(() => {
        refreshPromise = null; // the next expiry starts a new refresh
      });
  }
  return refreshPromise; // concurrent callers share the same promise
}

130. Where should you store auth tokens?

In the platform’s secure storage: the Keychain on iOS and the Android Keystore (through encrypted storage) on Android. Use expo-secure-store or react-native-keychain. Don’t use AsyncStorage, which is unencrypted and readable on a rooted or jailbroken device or from an unencrypted backup. Keep the access token short-lived, and treat the refresh token as the sensitive one.

131. Can you hide an API key inside a React Native app?

No. Everything shipped in the app (the JavaScript bundle, Hermes bytecode, native config files) can be extracted by anyone who downloads it. Environment variables like EXPO_PUBLIC_* are inlined into the bundle at build time, so they’re public by definition. Secret keys belong on your server: the app calls your backend, and the backend calls the third-party API. The same rule applies on the web; see securing AI features on the frontend.

132. What is SSL pinning, and what are the trade-offs?

SSL pinning makes the app accept only specific certificates or public keys for your API, instead of any certificate the device trusts. It blocks man-in-the-middle attacks with a user-installed or rogue certificate. The trade-offs: when the certificate rotates, old app versions can stop working entirely unless you pin backup keys and plan rotations, and it complicates debugging with proxies. Teams that handle money or health data usually pin public keys with a backup; others often skip it.

133. How do you implement OAuth login securely?

Use the Authorization Code flow with PKCE, opened in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), and return to the app through a redirect URL. Libraries: expo-auth-session or react-native-app-auth. Don’t use an embedded WebView for login: the app could read the user’s password, and Google blocks sign-in from embedded WebViews.

134. How do you protect sensitive screens?

  • Hide content in the app switcher: show a blur or overlay when AppState changes to inactive or background.
  • Block screenshots and screen recording on screens like card details (FLAG_SECURE on Android; expo-screen-capture covers both platforms as far as each allows).
  • Require biometric re-authentication (expo-local-authentication) before showing very sensitive data.
  • Clear sensitive state on logout, including caches and persisted stores.

135. Does Hermes bytecode protect your code?

Not really. Bytecode is harder to read than minified JavaScript, but decompilers exist. Treat anything in the app as public. Obfuscation, root and jailbreak detection, and tamper checks raise the cost of attacks but can all be bypassed. Real protection lives on the server: validate every request, check authorization server-side, and use platform attestation (App Attest on iOS, Play Integrity on Android) when you need to know the request comes from a genuine app.

136. How do you handle real-time updates with WebSockets?

React Native supports WebSocket natively. The mobile-specific parts: reconnect with exponential backoff and jitter when the connection drops (networks switch between Wi-Fi and cellular constantly), close or pause the socket when the app goes to the background, resubscribe and fetch missed events after reconnecting, and use push notifications for updates while the app isn’t running, because the socket won’t stay open in the background.

137. What security issues come with WebViews?

  • Restrict navigation with originWhitelist and onShouldStartLoadWithRequest, so the WebView can’t be steered to an attacker’s page.
  • Treat messages from onMessage as untrusted input: validate their shape and check the source URL.
  • Never inject tokens or secrets into pages you don’t fully control.
  • Keep file access and mixed content disabled unless you need them.

Testing, Debugging, and Releasing

Diagram: the React Native release pipeline, from tests to build, store review, staged rollout, and over-the-air updates

Find in-depth explanations and practice here: Build a Testing Framework ->

Shipping is where mobile differs most from the web: there’s a store review between you and your users, old versions stay installed for months, and a crash can’t be fixed with a redeploy. Senior React Native interview questions lean hard on this section.

138. How do you test a React Native app?

  • Unit tests with Jest for logic: reducers, hooks, formatters, API clients.
  • Component tests with React Native Testing Library: render a screen, interact like a user, and assert on what’s shown.
  • End-to-end tests with Detox or Maestro on a real build in a simulator or device, for the critical flows: sign-in, checkout, onboarding.

Most tests should be unit and component tests; keep end-to-end tests to the flows that would cost the most if they broke.

139. How do you write a component test with React Native Testing Library?

Query elements the way a user finds them (by role, label, or text), interact, and assert on the result. Avoid testing implementation details like state values or component names.

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

test('shows an error when the email is empty', () => {
  render(<SignInForm onSubmit={jest.fn()} />);
  fireEvent.press(screen.getByRole('button', { name: 'Sign in' }));
  expect(screen.getByText('Email is required')).toBeOnTheScreen();
});

140. How do you mock native modules in Jest?

Jest runs in Node, where native modules don’t exist, so you mock them. Use the react-native or jest-expo preset, which mocks the core modules, then use the mocks that libraries ship (many include a jest/ setup file) and jest.mock() for your own native modules. Keep the mocks thin, and cover the real native behavior in end-to-end tests.

141. How do Detox and Maestro compare?

Detox Maestro
Tests written in JavaScript or TypeScript YAML flows
Approach Gray-box: syncs with the app’s internal state, which reduces flakiness Black-box: drives the UI like a user
Setup More native configuration Very quick to start
Best for Teams that want tests in code next to the app Fast coverage of flows, including by QA engineers

142. How do you debug a React Native app?

React Native DevTools is the default debugger: open it from the Dev Menu or by pressing j in the Metro terminal. It’s built on Chrome DevTools, with a console, breakpoints, source maps, memory snapshots, and the React Components and Profiler panels. For native issues, use Xcode and Android Studio (Logcat). For network traffic, proxies like Proxyman or Charles work for both platforms.

143. What happened to Flipper and the old remote debugger?

The old “Debug JS Remotely” mode ran your JavaScript in Chrome instead of on the device, so timing and behavior differed from production, and it doesn’t work with JSI-based modules. Flipper was removed from the default template in React Native 0.74. React Native DevTools replaced both: it connects directly to Hermes running on the device, so you debug the code that actually ships.

144. How do you debug a native crash?

  1. Get the native stack trace: Xcode’s console or crash logs on iOS, adb logcat on Android, or your crash reporter for production.
  2. Make sure it’s symbolicated: upload dSYM files (iOS) and ProGuard/R8 mapping files (Android) to Sentry or Crashlytics, or the trace is just memory addresses.
  3. Find the library or native module in the trace, check its issues and version, and reproduce in a release build.

145. How do you build and ship an app?

Create a release build signed for each store: with Expo, eas build builds in the cloud and eas submit uploads to the stores; bare projects often use Fastlane in CI. Ship to internal testers first (TestFlight on iOS, internal or closed testing tracks on Google Play), then submit for review. Apple reviews every build, usually within a day or two; Google’s review is often faster.

146. How does code signing work on iOS and Android?

On iOS, a distribution certificate proves who built the app, and a provisioning profile ties the app’s bundle ID, the certificate, and its capabilities (push, App Groups) together. On Android, you sign uploads with an upload key from a keystore, and with Play App Signing, Google holds the final app signing key. Losing an Android upload key is recoverable through Google; that’s one reason to use Play App Signing. EAS can manage credentials for both.

147. What are over-the-air updates, and what can they change?

Over-the-air (OTA) updates replace the JavaScript bundle and assets without a store release: the app downloads the new bundle and uses it on the next launch. They can’t change native code, so adding a native module or changing permissions needs a new store build. Each update targets a runtime version so it’s only delivered to builds with compatible native code. EAS Update is the main service today; Microsoft’s CodePush was retired with App Center in 2025. Store rules allow OTA bug fixes and improvements, but not changing what the app fundamentally does to avoid review.

148. How do you roll out a release safely?

  • Use staged rollouts (phased release on iOS, percentage rollout on Google Play) and watch crash-free rates before going to 100%.
  • Put risky features behind remote feature flags, so you can turn them off without a release.
  • Keep an OTA rollback ready for JavaScript bugs.
  • Track adoption, and use a minimum supported version check to force updates when an old version has a critical bug.

149. How do you manage development, staging, and production builds?

Build separate app variants with different bundle identifiers (for example com.app.dev), names, and icons, so all three can be installed side by side. Keep environment config (API URLs, keys for public services) per variant: EAS build profiles with an app.config.ts that reads the environment, or iOS schemes and Android product flavors in bare apps. Point OTA update channels at the matching builds.

150. How do you upgrade React Native versions?

One version at a time where possible. With Expo, upgrade the SDK with npx expo install expo@latest and npx expo install --fix, and regenerate native projects with prebuild. In bare apps, use the React Native Upgrade Helper to see the native file diff between versions. Check every native dependency’s compatibility first, read the release notes for breaking changes, and test on both platforms before shipping.

React Native Coding Interview Questions and Scenarios

Diagram: a React Native coding interview workflow, clarify, build, handle edge cases, and test on device

Find in-depth explanations and practice here: JavaScript Coding Questions ->

React Native coding rounds usually ask for a small feature with real-world edge cases: debouncing, retries, offline handling, and app lifecycle. Senior rounds add debugging scenarios, where the interviewer describes a production problem and watches how you investigate.

151. Build a search screen that queries the API as the user types.

Debounce the input so you don’t send a request per keystroke, and make sure an old, slow response can’t overwrite a newer one. TanStack Query handles the second part: each query key is cached separately, and it passes an AbortSignal so outdated requests can be cancelled.

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

function SearchScreen() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query);
  const { data = [], isFetching } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: ({ signal }) => searchProducts(debouncedQuery, { signal }),
    enabled: debouncedQuery.length > 1,
  });

  return (
    <>
      <TextInput value={query} onChangeText={setQuery} placeholder="Search" autoCorrect={false} />
      {isFetching && <ActivityIndicator />}
      <FlatList data={data} keyExtractor={(item) => item.id} renderItem={renderProduct} keyboardShouldPersistTaps="handled" />
    </>
  );
}

Practice the hook on its own with useDebouncedValue, and the underlying function with Debounce.

152. Implement an outbox that queues actions while offline and sends them when the connection returns.

Send items in order, keep an item in the queue until the server confirms it, and never run two flushes at once. Each item gets a client-generated ID, which the server can use to ignore duplicates if a send succeeds but the response is lost.

function createOutbox(send) {
  const queue = [];
  let flushing = false;

  async function flush() {
    if (flushing) return;
    flushing = true;
    try {
      while (queue.length > 0) {
        await send(queue[0]); // throws while offline, so the item stays queued
        queue.shift();
      }
    } catch {
      // keep the remaining items; flush again when the connection returns
    } finally {
      flushing = false;
    }
  }

  return {
    add(action) {
      queue.push({ id: `${Date.now()}-${Math.random()}`, ...action });
      flush();
    },
    flush,
    size: () => queue.length,
  };
}

let online = false;
const sent = [];
const outbox = createOutbox(async (item) => {
  if (!online) throw new Error('offline');
  sent.push(item.type);
});

outbox.add({ type: 'like' });
outbox.add({ type: 'comment' });

setTimeout(async () => {
  console.log(outbox.size()); // 2
  online = true;
  await outbox.flush();
  console.log(sent); // [ 'like', 'comment' ]
}, 0);

In a real app, persist the queue (MMKV or SQLite) so it survives the app being killed, and call flush() from a NetInfo listener and when the app returns to the foreground.

153. Write a retry helper with exponential backoff.

Retry a failing async function a limited number of times, doubling the wait each time and adding random jitter, so thousands of phones don’t retry at the same moment after an outage.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function retryWithBackoff(fn, { retries = 3, baseMs = 500 } = {}) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt >= retries) throw error;
      await sleep(baseMs * 2 ** attempt + Math.random() * baseMs);
    }
  }
}

let calls = 0;
const flaky = async () => {
  calls++;
  if (calls < 3) throw new Error('network error');
  return 'ok';
};

retryWithBackoff(flaky, { baseMs: 10 }).then((result) => console.log(result, calls)); // ok 3

Only retry requests that are safe to repeat (GETs, or writes with an idempotency key), and don’t retry 4xx errors like a 400 or 403. Practice it with API Retry.

154. Show a “scroll to top” button after the user scrolls down a long list.

Listen to onScroll, but only update state when the user crosses the threshold, not on every scroll event. React skips the re-render when the new state equals the old one.

function Feed({ posts }) {
  const listRef = useRef(null);
  const [showTop, setShowTop] = useState(false);

  const onScroll = useCallback((event) => {
    setShowTop(event.nativeEvent.contentOffset.y > 600);
  }, []);

  return (
    <>
      <FlatList ref={listRef} data={posts} renderItem={renderPost} onScroll={onScroll} scrollEventThrottle={16} />
      {showTop && (
        <Pressable style={styles.fab} onPress={() => listRef.current?.scrollToOffset({ offset: 0, animated: true })}>
          <Text>Top</Text>
        </Pressable>
      )}
    </>
  );
}

If the button should fade in with the scroll position, use Reanimated’s useAnimatedScrollHandler so the animation runs on the UI thread with no re-renders at all. For the general idea of limiting how often handlers run, see Throttle.

155. Build a countdown timer that stays correct when the app is backgrounded.

JavaScript timers pause when the OS suspends the app, so a timer that subtracts one second per tick drifts. Store the end time and compute the remaining time from the clock on each tick and whenever the app returns to the foreground.

function useCountdown(endsAt) {
  const [remaining, setRemaining] = useState(() => Math.max(0, endsAt - Date.now()));

  useEffect(() => {
    const tick = () => setRemaining(Math.max(0, endsAt - Date.now()));
    const id = setInterval(tick, 1000);
    const sub = AppState.addEventListener('change', (status) => {
      if (status === 'active') tick();
    });
    return () => {
      clearInterval(id);
      sub.remove();
    };
  }, [endsAt]);

  return remaining;
}

If the user must be alerted when it ends while the app is closed, schedule a local notification for endsAt, because your JavaScript won’t be running.

156. Scenario: a list scrolls smoothly on iPhone but is janky on Android. How do you investigate?

  1. Reproduce in a release build on a mid-range Android device, not an emulator or debug build.
  2. Open the Perf Monitor: is the JS frame rate or the UI frame rate dropping?
  3. If JS: profile renders. Usual causes are unmemoized rows, an inline renderItem, or state updates during scroll.
  4. If UI: look at row complexity, shadows and elevation on every row, and oversized images being decoded.
  5. Fix the biggest cause, measure again, and consider FlashList if rows are still expensive.

What the interviewer is checking: that you measure and narrow down instead of listing every optimization you know.

157. Scenario: the app takes five seconds to start. What do you do?

Measure first: cold start time in a release build, broken down into native startup, JavaScript bundle load, and time to the first meaningful screen. Then work through the usual causes: Hermes disabled, large libraries and SDKs initialized at import time, synchronous storage reads of big blobs, network requests blocking the first screen, and a heavy first render. Fix with lazy loading, deferring SDK initialization, rendering cached data first, and showing the shell of the screen before data arrives. Track the metric in production so it doesn’t regress.

158. Scenario: a crash happens only in production, on some Android devices.

  1. Pull the symbolicated stack trace and device breakdown from Sentry or Crashlytics: OS version, manufacturer, app version, and memory.
  2. Decide if it’s a JavaScript error or a native crash, and whether it started with a specific release.
  3. Common culprits: out-of-memory from large images, a native library bug on one OS version, R8 stripping a class a library needs through reflection, and manufacturer-specific behavior.
  4. Reproduce on a matching device or emulator image with a release build, fix, and roll out to a small percentage first.

159. Scenario: users see stale data after coming back to the app.

The screens stayed mounted in the background, and nothing told them to refetch. Fix it at the data layer: connect AppState to TanStack Query’s focus manager (or your own refetch logic) so active queries refresh when the app becomes active, use useFocusEffect for screens that need fresh data when navigated back to, and set sensible stale times. For data that must be live, like an order status, add push notifications or a socket that reconnects on foreground.

160. How would you structure a large React Native codebase?

Organize by feature (features/checkout, features/profile), each with its screens, components, hooks, and API calls, plus shared folders for the design system, navigation, and core services like the API client, storage, and analytics. Keep server state in TanStack Query, client state in a small store, and native code behind typed modules. Add the guardrails that keep a big team fast: TypeScript strict mode, lint rules for import boundaries, CI running tests and type checks on every PR, end-to-end tests on critical flows, and OTA plus feature flags for safe releases.

If the interviewer turns this into a mobile system design question, like “design a chat app”, use the same structure as a frontend design round. The chat application design and the frontend system design handbook walk through it.

How to Use This List

Don’t read 160 questions the night before. Pick the sections that match the role, answer each question out loud before reading the answer, and build at least one small app feature end to end on a real device. React Native developer interview questions reward people who have shipped, so tie your answers to things you’ve actually built.

Level Focus on Then practice
Junior Fundamentals, components and styling, state and data A list screen with pull to refresh, a form with validation
Mid-level Lists and performance, navigation, animations, testing Debounced search, infinite scroll, a draggable card
Senior / staff Architecture and the New Architecture, native modules, security, releasing, scenarios An offline outbox, a release plan with OTA and staged rollouts, a mobile system design

React Native interview questions for experienced developers lean on the scenarios in the last section, so practice explaining how you’d investigate a problem, not just how you’d fix it. Senior React Native interview questions also tend to include a design round; prepare it like a frontend one.

Where to go next on FrontendLead

The React Native interview questions and answers above cover what interviewers ask in 2026. The fastest way to make them stick is to build: take one feature from each section and ship it in a small app.