DEV Community

Prabhakar
Prabhakar

Posted on

React Native Developer Interview Questions & Answers : Real Scenario-Based Questions

A deep, easy-to-understand interview preparation guide for React Native developers — explained the way a senior engineer would explain it to you over coffee.

Why This Guide Is Different

Most interview blogs throw definitions at you. "What is a component? A reusable piece of UI." That answer gets you rejected.

Real interviews are not vocabulary tests. The interviewer describes a situation — an app that drops frames, a list that re-renders too often, a screen that freezes — and watches how you think. They want to see if you understand why something works, not just what it is called.

So this guide is written around real-life scenarios. Every question is the kind of thing actually asked in interviews at product companies, startups, and service companies. Every answer starts simple, then goes deep enough to impress a senior interviewer.

Read it slowly. Speak the answers out loud. By the end, you will not just remember answers — you will understand React Native.

Let's begin.

Table of Contents

  1. React Native & JS Fundamentals + App Lifecycle (Q1–Q12)
  2. Components, Navigation & Routing (Q13–Q22)
  3. UI, Styling, Lists & Layout (Q23–Q35)
  4. Async, Threading, the JS Engine & the Bridge/JSI (Q36–Q50)
  5. Networking & APIs (Q51–Q58)
  6. Data Persistence — Storage, SQLite, Secure Storage (Q59–Q68)
  7. State Management & Architecture (Q69–Q80)
  8. Native Modules & Dependency Patterns (Q81–Q85)
  9. Performance, Memory & Rendering (Q86–Q95)
  10. Security (Q96–Q100)
  11. Testing (Q101–Q105)
  12. JavaScript / TypeScript Deep-Dive Scenarios (Q106–Q112)
  13. Advanced React Native Deep Dive — Most-Asked in 2026 (Q113–Q138)

Section 1: React Native & JS Fundamentals + App Lifecycle

Q1. A user is filling a long form, switches apps to copy something, comes back and the data is gone. What happened, and how do you fix it?

When the user leaves your app, the OS can move it to the background and, if memory is tight, kill the process entirely. If your form data lived only in component state (a useState value in memory), it dies with the process.

The fix depends on the data:

  • For transient UI state, listen to AppState changes and save a draft when the app goes to background.
  • For anything the user would hate to lose, persist as they type to local storage (AsyncStorage/MMKV for small values, SQLite for real data), not just on exit.
  • Restore the draft when the screen mounts.

Senior-level point: Distinguish a re-render (cheap, frequent — state survives) from unmount (component left the tree — its state is gone unless lifted/persisted) from process death (OS killed the app — only persisted data survives). Most "lost data" bugs come from confusing these. Test process death by killing the app from the background.

Q2. Walk me through the React Native app lifecycle (AppState), and tell me which state the app is in during an incoming phone call.

React Native exposes app-level state via AppState, with three values:

  • active — the app is running in the foreground and receiving input.
  • inactive — a transitional state (iOS): the app is in the foreground but not receiving events, e.g., during an incoming call overlay or while in the app switcher.
  • background — the app is not visible, running in the background. A good place to save state and pause work.

(On Android, inactive is effectively not used the same way; you mostly see active and background.)

During an incoming call: the app goes to inactive (call UI interrupts it on iOS); if the user leaves your app for the call, it moves to background.

Key rule: save state and pause work (timers, video, location) on background/inactive; resume on active. You subscribe with AppState.addEventListener('change', handler) and clean up the subscription on unmount.

Q3. What is the difference between props and state? Give a real reason to use each.

  • props — data passed into a component from its parent. They're read-only from the child's perspective — the child displays or uses them but doesn't change them. Use props to configure a component: a Button receiving its title and onPress.
  • state — data owned and managed by the component that can change over time, triggering a re-render. Use state for things that change due to interaction or data: a toggle's on/off, form input, loading status.

Real reason: A PriceTag that just shows a passed-in price → props only. A LikeButton that flips filled/unfilled on tap → state for the liked flag.

Senior nuance: "data flows down" — parents pass props to children; children communicate up via callbacks. Lifting state to the right level (and not duplicating it) is the core skill. Overusing local state for shared data leads to prop-drilling and bugs.

Q4. What is useEffect, and where would you set up and tear down a subscription?

useEffect runs side effects in function components — things outside pure rendering: subscriptions, timers, network calls, event listeners. It runs after render, and its dependency array controls when it re-runs.

  • useEffect(fn, []) — runs once after the first render (mount). Set up a subscription here.
  • useEffect(fn, [dep]) — re-runs whenever dep changes.
  • Cleanup: return a function from the effect — React calls it on unmount and before re-running the effect. Unsubscribe / clear timers here.
useEffect(() => {
  const sub = AppState.addEventListener('change', onChange);
  return () => sub.remove();   // cleanup on unmount
}, []);
Enter fullscreen mode Exit fullscreen mode

The pairing rule interviewers love: whatever you set up in an effect, tear down in its cleanup. Forgetting cleanup leaks subscriptions/timers and causes "state update on an unmounted component" bugs.

Q5. Explain the useEffect dependency array and the most common bug people create with it.

The dependency array tells React when to re-run the effect — it re-runs whenever any listed value changes. Three cases: [] (run once), [a, b] (run when a or b change), and no array (run after every render).

The most common bug — stale closures / missing deps: if your effect uses a value (a prop, state, or function) but you leave it out of the deps, the effect captures an old (stale) value and never updates — e.g., a setInterval that always logs the initial count. Conversely, putting an unstable value (a function/object recreated every render) in deps causes the effect to re-run constantly (an infinite loop of re-subscribes).

Fixes: include all used values, and stabilize functions/objects with useCallback/useMemo so they don't change identity every render. The ESLint react-hooks/exhaustive-deps rule catches missing deps. Explaining stale closures is a strong senior signal.

Q6. What is the Virtual DOM and reconciliation, and how does it apply to React Native?

React keeps a lightweight in-memory tree describing your UI (the "virtual" tree). When state/props change, React builds a new tree and diffs it against the old one (reconciliation) to compute the minimal set of changes needed, then applies only those.

In React Native there's no browser DOM — instead, React reconciles the component tree and sends the minimal updates to the native side, which updates the actual native views (UIView/ViewGroup). So the same diffing idea applies, but the output is native views, not HTML.

Why it matters: this is why keys in lists matter (they help the diff match items correctly) and why unnecessary re-renders are wasteful work — React still has to reconcile them. Understanding reconciliation underpins most performance answers.

Q7. What is the difference between a controlled and an uncontrolled component (e.g., a TextInput)?

  • Controlled — the component's value is driven by state. The TextInput's value comes from state, and onChangeText updates that state. React is the single source of truth:
<TextInput value={text} onChangeText={setText} />
Enter fullscreen mode Exit fullscreen mode
  • Uncontrolled — the component manages its own internal value; you read it only when needed (e.g., via a ref). React isn't driving each keystroke.

Trade-off: controlled inputs give you full control (validation, formatting, syncing) but re-render on every keystroke; uncontrolled inputs are lighter but harder to control. Scenario: a form needing live validation → controlled. A simple input where you only need the final value → uncontrolled (ref) can avoid per-keystroke re-renders. Knowing this trade-off shows real form-handling experience.

Q8. The app shows a blank/splash screen for too long before the UI appears. Why, and how do you reduce it?

That delay is startup time — the native app launches, the JS engine (Hermes) loads and parses your JS bundle, and React renders the first screen. Doing heavy synchronous work on startup stretches it.

Causes and fixes:

  • A large JS bundle → enable Hermes (precompiles JS to bytecode for faster startup), enable the RAM bundle/inline requires, and lazy-load heavy screens/modules.
  • Heavy synchronous work in the first component's render or top-level module code → defer it (run after first paint, use InteractionManager).
  • Too many libraries initialized eagerly → lazy-init.
  • Use a proper native splash screen (react-native-bootsplash) so the wait looks intentional.

Senior point: "Measure with Flipper / the profiler before optimizing," and mention Hermes + inline requires as the biggest startup wins. That signals current, practical knowledge.

Q9. How do you pass data between two screens the right way in React Native?

It depends on direction and scope:

  • Forward (A → B): pass data through navigation params: navigation.navigate('Detail', { id: productId }), read via route.params. Pass identifiers or small data, not huge objects.
  • Backward (B → A): use a callback passed in params, or better, update shared state (Context/Redux/Zustand) or trigger a refetch (React Query) that A is observing.
  • Shared/app-wide data: use global state management so multiple screens read the same source of truth instead of threading data through navigation.

Why not a global variable / module-level mutable: it creates hidden coupling, is hard to test, and doesn't trigger re-renders. Navigation params (explicit) or scoped state (shared) are the clean options.

Q10. What actually happens when you call setState/a state setter from useState?

Calling a state setter (e.g., setCount(1)) does two things: it schedules an update with the new value and marks the component to re-render. On the next render, useState returns the new value, and React reconciles the new output against the old to update the native views.

Key details interviewers probe:

  • State updates are asynchronous/batched — calling setCount(count + 1) twice in a row may not add 2, because both read the same count. Use the functional updater setCount(c => c + 1) to update based on the latest value.
  • The setter replaces the value (it doesn't merge like old class setState did) — for objects, spread the old state yourself.

Knowing batching and the functional updater is a very common, practical question.

Q11. Why does useState stop being enough as an app grows? Set up the need for state management.

useState is perfect for local, ephemeral state inside one component. But as an app grows, you hit walls:

  • Sharing state across distant components forces you to lift state up and pass it down through many layers (prop drilling) — tedious and fragile.
  • Server data (cached API responses, refetching, caching, invalidation) is awkward to manage with useState/useEffect by hand.
  • Complex/related state scattered across components becomes hard to reason about and test.

So you need ways to share state (Context, Redux, Zustand), and a dedicated tool for server state (React Query). Framing the problem before naming solutions is the senior approach — and it sets up the distinction between client state and server state, which is a major 2026 theme.

Q12. The OS killed your app in the background and the user lost their place. How do you restore it gracefully?

The OS reclaims memory by killing background apps silently. To bring the user back, combine layers:

  • Persist critical data (drafts, selections, in-progress work) to local storage as it changes — only persisted data survives process death.
  • Save navigation state — React Navigation supports state persistence (save the nav state to storage, restore it on launch) so the user lands on the right screen.
  • On relaunch, rehydrate persisted state (e.g., redux-persist, or read from storage) and restore navigation, instead of starting fresh.

Senior point: Test by enabling "Don't keep activities" on Android or killing the app from the OS while backgrounded. Distinguish user-killed (intentional fresh start) from system-killed (should restore). Most restoration bugs are invisible until you force-test this.


Section 2: Components, Navigation & Routing

Q13. What is React Navigation and how does the stack navigator's navigation stack work?

React Navigation is the de facto navigation library for RN. Its stack navigator manages a stack of screens — navigation.push/navigate adds a screen on top; goBack/the back gesture pops it.

Example: Home → Product List → Product Detail. The stack (bottom to top) is [Home, List, Detail]. Back pops Detail → you see List. Pop again → Home.

Tricky part: sometimes you don't want a screen left on the stack. After login, you don't want Back to return to login — so you use navigation.replace('Home') (swap the current screen) or navigation.reset(...) (rebuild the stack) so the user lands on Home with no way back to login.

Senior note: navigate is smart — if the screen already exists in the stack, it may go to it rather than push a duplicate; push always adds a new instance. Knowing navigate vs push vs replace vs reset is a common, practical distinction.

Q14. What's the difference between navigate, push, replace, and reset?

  • navigate(name) — go to a screen; if it already exists in the current stack, React Navigation may jump to it instead of adding a duplicate.
  • push(name) — always adds a new instance on top, even if the same screen is already there (useful for drill-down chains like Profile → Profile → Profile).
  • replace(name) — swaps the current screen with a new one (no Back to the old one) — e.g., splash/login → home.
  • reset(state) — rebuilds the entire navigation stack from scratch — e.g., after logout, reset to a single Login screen with no history.

Choosing the right one is about what Back should do afterward — a very practical, commonly-asked distinction.

Q15. Stack vs Tab vs Drawer navigator — and how do you nest them?

  • Stack navigator — linear drill-down (Home → Detail) with a header and back behavior.
  • Tab navigator — parallel sections (Home, Search, Profile) with a bottom tab bar; each tab usually holds its own stack.
  • Drawer navigator — a side menu that slides out to switch sections.

Nesting: the common pattern is a tab navigator at the root, where each tab contains a stack navigator — so each tab maintains its own drill-down history. You can also nest a stack inside a drawer, etc.

Senior caution: deep nesting can complicate passing params and navigation actions across navigators (you sometimes need to target a specific nested route). Understanding how tabs hold independent stacks — and the param-passing nuance when nesting — is what interviewers probe.

Q16. A user taps a button rapidly and two copies of the next screen get pushed. Why, and how do you prevent it?

The taps fire before the first navigation completes, so you push the same screen twice — a real, common bug.

Fixes:

  • Disable the button after the first tap (a flag or loading state) and re-enable when appropriate.
  • Use React Navigation's helper to prevent duplicate navigations — e.g., check navigation.isFocused() or debounce the handler.
  • Prefer navigate (which can dedupe to an existing screen) over push where a duplicate isn't wanted.

Anticipating double-taps, race conditions, and edge cases — rather than only the happy path — is exactly what senior interviewers look for.

Q17. How does deep linking work in React Navigation, and how would you route a notification tap to a specific screen?

A deep link opens your app at a specific screen via a URL (myapp://product/42 custom scheme, or a universal/app link https://myapp.com/product/42). React Navigation has a linking config that maps URL paths to screens (with params), and it parses incoming URLs to navigate and build the correct stack so Back behaves naturally.

Notification tap: the notification payload carries a destination (an id or a deep-link URL). When tapped, you read the payload and navigate (e.g., navigation.navigate('Detail', { id })) — or pass the URL through the linking config. You also handle the cold-start case (app was killed): read the initial URL/notification on launch and navigate after the navigator is ready.

Senior point: the hard part is the back stack (don't strand the user on the deep-linked screen) and the cold-start vs warm distinction. Naming both shows real experience.

Q18. What are keys in lists, and what real bug does a wrong/missing key cause?

A key is a stable identifier React uses to match list items across re-renders during reconciliation. With correct keys, React knows "this is the same item, just moved/updated" vs "this is a new item."

The classic bug: using the array index as the key while items are inserted, deleted, or reordered. React then matches by position, so state and UI attach to the wrong item — a checkbox stays checked on the wrong row, a TextInput's text jumps to a different item, or an item appears not to delete. Using a stable unique id (keyExtractor={item => item.id} in FlatList, or key={item.id}) ties identity to the item, so reordering moves state with the correct item.

Senior point: index-as-key is fine only for static lists that never reorder/change. Explaining when it breaks (dynamic lists) is the depth interviewers want.

Q19. What is the difference between useMemo, useCallback, and React.memo?

All three prevent unnecessary work/re-renders:

  • useMemo(fn, deps) — memoizes a computed value; recomputes only when deps change. Use for expensive calculations so they don't run every render.
  • useCallback(fn, deps) — memoizes a function so its identity stays stable across renders (unless deps change). Use when passing callbacks to memoized children or to effect deps, so they don't trigger re-renders/re-runs.
  • React.memo(Component) — memoizes a component; it skips re-rendering when its props haven't changed (shallow compare).

How they work together: React.memo only helps if the props you pass are stable — which is why you pair it with useCallback/useMemo for function/object props. Misusing them (memoizing everything) adds overhead without benefit, so apply where profiling shows real re-render cost. That nuance is the senior signal.

Q20. How do you communicate from a child component back to a parent cleanly?

Use a callback passed down via props — the parent gives the child a function to call on an event:

<ChildButton onPress={() => parentDoesSomething()} />
Enter fullscreen mode Exit fullscreen mode

The child stays unaware of who its parent is (reusable), and the parent reacts when the callback fires. This is the React "data down, events up" pattern.

For deeper trees or app-wide communication, use shared state (Context/Redux/Zustand) so the child dispatches an action/updates state the parent (and others) observe — better than threading callbacks through many layers. Choosing callback (local, simple) vs shared state (cross-cutting) is the thoughtful answer.

Q21. What's the difference between a functional component with hooks and a class component, and why did the ecosystem move to hooks?

  • Class components use lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount) and this.state/this.setState.
  • Functional components are plain functions that use hooks (useState, useEffect, etc.) for state and lifecycle.

Why the ecosystem moved to hooks:

  • Reuse logic — custom hooks let you extract and share stateful logic across components, which classes couldn't do cleanly (the old HOC/render-prop patterns were clunky).
  • Less boilerplate and no confusing this binding.
  • Related logic togetheruseEffect colocates setup and cleanup, instead of splitting across didMount/willUnmount.

In 2026, functional components with hooks are the standard; classes are legacy. Knowing custom hooks as the reuse mechanism is the strong point.

Q22. What is a custom hook, and give a real example of when you'd write one.

A custom hook is a JavaScript function whose name starts with use and that calls other hooks to encapsulate and reuse stateful logic across components.

Real example: a useDebounce for a search box, or a useNetworkStatus to track connectivity:

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}
Enter fullscreen mode Exit fullscreen mode

Now any component can const q = useDebounce(query) without duplicating the logic. Custom hooks are React's answer to logic reuse — they keep components clean and make logic testable in isolation. Showing you extract reusable logic into hooks is a strong architecture signal.


Section 3: UI, Styling, Lists & Layout

Q23. Your FlatList janks and stutters with images. Walk me through how you'd fix it.

Jank means frames take too long — usually too much work on the JS thread, re-rendering too many items, or heavy images.

Systematic fixes:

  • Are you using FlatList (not ScrollView with .map)? ScrollView renders all children up front; FlatList virtualizes — only rendering visible items plus a buffer. For long lists this is the #1 fix.
  • Memoize the row — wrap the item component in React.memo and stabilize renderItem/callbacks with useCallback, so rows don't re-render when unrelated state changes.
  • keyExtractor — provide stable unique keys so items aren't recreated.
  • getItemLayout — if rows are fixed height, this lets FlatList skip measurement and jump directly, hugely improving scroll performance.
  • Tune windowSize, maxToRenderPerBatch, initialNumToRender, removeClippedSubviews to control how much is rendered/retained.
  • Images — use react-native-fast-image (or proper caching) and correctly sized images, not full-resolution into small cells.

Senior point: mention the 16ms budget (60fps) and that you'd profile to see if it's JS-thread or UI-thread bound. Also mention FlashList (Shopify) as a higher-performance drop-in for heavy lists. That signals real list-perf experience.

Q24. How does styling work in React Native, and how is it different from CSS on the web?

React Native uses JavaScript style objects (typically via StyleSheet.create), not CSS files. It supports a subset of CSS-like properties with key differences:

  • No cascade/inheritance (except some text styles) — styles are explicit per component.
  • camelCase properties (backgroundColor, not background-color).
  • Flexbox by default for layout (via the Yoga engine), with flexDirection defaulting to column (web defaults to row).
  • Units are density-independent pixels (unitless numbers), not px/em/rem.

Why StyleSheet.create: it can validate styles and (historically) optimize by referencing styles by id. Senior note: styles compile down to native layout via Yoga, which is why Flexbox is the core layout model. Knowing "Flexbox via Yoga, column default, no cascade" is the clean answer.

Q25. Explain Flexbox in React Native — flexDirection, justifyContent, alignItems.

React Native lays out with Flexbox (via Yoga). The key axes:

  • flexDirection — the main axis direction. Default is column (top to bottom); row is left to right.
  • justifyContent — alignment along the main axis (flex-start, center, space-between, etc.).
  • alignItems — alignment along the cross axis (perpendicular to main).
  • flex: 1 — makes a component expand to fill available space along the main axis (split space among siblings by their flex values).

Scenario: a row with a label on the left and an icon on the right → flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'. Two columns sharing width equally → both flex: 1. Knowing that column is the default (a frequent gotcha for web developers) and how flex: 1 distributes space is essential.

Q26. You get content overflowing or cut off on smaller screens. How do you build responsive UI?

Hardcoded sizes break across devices. Responsive tools:

  • Flexbox (flex, flexGrow, flexWrap) so content adapts instead of fixed sizes.
  • Dimensions / the useWindowDimensions hook to read screen size (prefer the hook — it updates on rotation).
  • Percentage sizing and maxWidth/minWidth.
  • SafeAreaView / react-native-safe-area-context to avoid notches and system bars.
  • PixelRatio for density-aware sizing.
  • For text scaling, respect the user's font scale and use numberOfLines/adjustsFontSizeToFit.

Senior point: prefer useWindowDimensions over a one-time Dimensions.get (which won't update on rotation/foldables), and design with flex + breakpoints rather than absolute pixels. That shows mature responsive thinking.

Q27. What's the difference between ScrollView and FlatList, and why does it matter for performance?

  • ScrollView renders all its children immediately, even off-screen. Fine for a small, known set of items (a settings page).
  • FlatList (and SectionList) virtualizes — it renders only the items currently visible (plus a buffer) and recycles as you scroll, via data + renderItem.

Why it matters: for a list of thousands, ScrollView mounts thousands of views up front — slow, memory-heavy, possibly freezing the app. FlatList keeps memory low and scrolling smooth by rendering just what's needed. Rule: any long or data-driven list → FlatList/SectionList (or FlashList). A short, fixed set → ScrollView is fine. This is one of the most common performance questions because the mistake is so frequent.

Q28. What is FlashList and why might you choose it over FlatList?

FlashList (by Shopify) is a high-performance list component that's a near drop-in replacement for FlatList. It improves performance through better view recycling — it reuses item components more aggressively (closer to native RecyclerView/UITableView behavior) instead of mounting/unmounting as much.

Why choose it: for large, complex, or image-heavy lists where FlatList still drops frames, FlashList typically delivers smoother scrolling and lower memory use. You provide an estimatedItemSize to help it recycle efficiently.

Senior nuance: FlashList recycles item views, so your item components must handle prop changes correctly (not rely on mount-time-only setup that won't re-run on recycle). Knowing FlashList exists, why it's faster (recycling), and the recycling caveat is a strong 2026 list-performance answer.

Q29. Why does a list flash the wrong image in the wrong row while scrolling, and how do you fix it?

This is a recycling/async-load race: as you scroll, a row's component is reused (or its image loads asynchronously). If image A's load completes after the row was reassigned to item B, you briefly see A's image on B's row — the wrong image.

Fixes:

  • Use an image library (react-native-fast-image) that handles caching and cancels/replaces stale loads correctly.
  • Ensure each row is keyed by a stable id so React/Flash recycling maps correctly.
  • Reset/override image state on prop change (don't rely on stale state from the previous item).
  • Avoid index-based keys for dynamic lists.

This "wrong image flashes in wrong row" scenario is a classic list bug; explaining the async-load-vs-recycle race is the senior-level answer.

Q30. What is the difference between Pressable, TouchableOpacity, and Button?

  • Button — a basic, minimally-styleable native button. Quick, but limited customization.
  • TouchableOpacity — a wrapper that dims (reduces opacity) on press; widely used for custom-styled tappable elements.
  • Pressable — the modern, flexible API (recommended): it gives you press state (pressed) to style any way you want, supports onPressIn/onPressOut/onLongPress, and handles hit slop and different press feedback.

Recommendation: prefer Pressable for new code — it's more capable and future-proof than the Touchable family. Knowing Pressable is the current standard (with its pressed state for custom feedback) signals you're up to date.

Q31. How does layout actually get computed in React Native (the role of Yoga and the shadow tree)?

When you write Flexbox styles, RN computes layout using Yoga — a cross-platform layout engine. RN maintains a shadow tree (a lightweight representation of your view hierarchy with layout info) on a background thread; Yoga calculates each node's size and position there, then the results are applied to the actual native views on the main/UI thread.

Why it matters: layout computation happens off the JS thread (on the shadow/layout thread), which is good — but very deep or complex hierarchies still cost time, and frequent layout changes can cause work. Understanding that Yoga computes Flexbox in a shadow tree, then native views are positioned explains how RN bridges JS-described UI to real native views — a strong fundamentals answer that goes beyond "it just uses Flexbox."

Q32. What is the difference between flex: 1, flexGrow, flexShrink, and flexBasis?

  • flexBasis — the initial size of an item along the main axis before growing/shrinking (like a starting width/height).
  • flexGrow — how much the item grows to fill extra space (relative to siblings' grow values).
  • flexShrink — how much the item shrinks when there's not enough space.
  • flex: 1 — a shorthand roughly meaning flexGrow: 1, flexShrink: 1, flexBasis: 0% — "take an equal share of available space."

Scenario: two cards that should share the row equally → both flex: 1. A sidebar with a fixed base width that shouldn't shrink → flexBasis: 200, flexShrink: 0. Knowing the individual properties (not just flex: 1) shows you understand the layout model deeply, which interviewers probe for senior roles.

Q33. A designer wants a custom animated component. What are your options and which runs smoothly?

Options, roughly from simplest to most powerful:

  • Animated API (built-in) — declarative animations (Animated.timing/spring). Crucially, use useNativeDriver: true so the animation runs on the native/UI thread (smooth even if JS is busy). Limitation: native driver supports only non-layout properties (transform, opacity).
  • LayoutAnimation — animate layout changes automatically; simple but less controllable.
  • Reanimated (2/3) — the powerful choice: animations and gesture logic run on the UI thread via worklets, so they stay smooth even under heavy JS load, and it supports complex, interruptible, gesture-driven animations. Pairs with react-native-gesture-handler.

Senior point: the key to smooth RN animations is running them off the JS thread — either useNativeDriver (Animated) or Reanimated worklets. Explaining why (JS-thread jank vs UI-thread animation) is exactly what's tested.

Q34. Why does useNativeDriver: true make animations smoother?

Without the native driver, each animation frame is computed in JavaScript and sent across to the native side — so if the JS thread is busy (rendering, processing data), the animation stutters. With useNativeDriver: true, the animation's definition is sent to the native side once, and then it runs entirely on the UI/native thread, independent of the JS thread. So even if JS is blocked, the animation keeps running at 60/120fps.

The catch: the native driver only supports non-layout properties — transform (translate/scale/rotate) and opacity — not things like width/height/top that require layout. For those you use Reanimated or rethink the animation. Explaining the JS-thread-vs-UI-thread reason, and the non-layout limitation, is the complete senior answer.

Q35. What is react-native-gesture-handler and why use it over the built-in touch system?

The built-in touch/gesture system (PanResponder) runs gesture logic on the JS thread, so under load gestures feel laggy and can conflict with native scrolling. react-native-gesture-handler moves gesture recognition to the native/UI thread, giving:

  • Smooth, responsive gestures even when JS is busy.
  • Better interaction with native components (scroll views, swipeable rows) and proper gesture composition (simultaneous, exclusive).
  • It pairs with Reanimated so gesture-driven animations run entirely on the UI thread (e.g., a draggable, snapping bottom sheet that stays smooth).

Why it matters: for anything beyond a simple tap — swipes, drags, pinch, bottom sheets — gesture-handler + Reanimated is the standard for native-quality interactions. Knowing the JS-thread vs native-thread reason is the senior point.


Section 4: Async, Threading, the JS Engine & the Bridge/JSI

Q36. React Native runs JavaScript on one thread. Why does heavy JS work freeze the UI, and what threads exist?

React Native runs your JavaScript on a single JS thread. The key threads are:

  • JS thread — runs your React code, business logic, and (in the old architecture) sends UI updates across the bridge.
  • UI/main (native) thread — handles native rendering, touches, and native animations.
  • Shadow/layout thread — runs Yoga layout calculations.

Why heavy JS freezes the UI: if you run a long synchronous task on the JS thread (parsing a huge array, a heavy loop), it blocks the JS thread — so it can't process the next React update or respond to events, and the app feels frozen, dropping frames. Animations using the native driver may keep going (they're on the UI thread), but anything JS-driven stalls.

Fix: break up heavy work, move it off the critical path (InteractionManager, batching), offload to native, or use background processing. JavaScript being single-threaded is the root of most RN performance discussions.

Q37. What is the Bridge in the old React Native architecture, and what were its limitations?

In the old (pre-New-Architecture) RN, the JS thread and native side communicated over the Bridge — an asynchronous, batched, serialized (JSON) message channel. JS would serialize commands (e.g., "update these views," "call this native method"), send them across, and native would deserialize and act, and vice versa.

Limitations:

  • Asynchronous only — you couldn't call native synchronously; everything was message-passing.
  • Serialization overhead — every message was converted to JSON and back, costly for large/frequent data (big lists, fast gestures).
  • Bottleneck — heavy traffic over the bridge caused lag, and the async nature made some interactions feel laggy.

These limitations are exactly why the New Architecture (JSI/Fabric/TurboModules) was created — a perfect setup for the next question.

Q38. What is JSI and the New Architecture, and how does it fix the Bridge's problems?

JSI (JavaScript Interface) is a lightweight C++ layer that lets JavaScript hold direct references to native (C++) objects and call them synchronously, without serialization over a bridge. It's the foundation of the New Architecture, which has three parts:

  • JSI — direct, synchronous JS↔native communication (no JSON bridge).
  • TurboModules — native modules loaded lazily and called directly via JSI (faster startup, sync calls when needed).
  • Fabric — the new rendering system that lets the UI be created/updated more efficiently and supports concurrent React features.

How it fixes the Bridge: no serialization bottleneck, synchronous calls are possible, lazy module loading, and better interop — yielding faster startup, smoother interactions, and lower overhead. In 2026, the New Architecture (with Bridgeless mode) is the default direction. Explaining "JSI removes the serialized async bridge, enabling direct sync access" is a key senior-level answer.

Q39. What is Hermes, and why does it improve performance?

Hermes is a JavaScript engine built by Meta specifically for React Native (now the default). Its big win is ahead-of-time compilation: instead of shipping raw JS that the engine parses and compiles at runtime, Hermes precompiles your JS to bytecode at build time.

Benefits:

  • Faster startup — no expensive parse/compile of JS on launch; it loads bytecode.
  • Lower memory usage and smaller app footprint on device.
  • Better performance on lower-end devices (very relevant for markets like India).

It also has good debugging support and works tightly with the New Architecture. Mentioning "Hermes precompiles to bytecode → faster startup + less memory" is the expected, current answer.

Q40. Explain the JavaScript event loop, microtasks, and how Promises fit in.

JavaScript is single-threaded with an event loop. It runs your synchronous code, and async work is queued:

  • The call stack runs the current synchronous code.
  • The microtask queue (Promise callbacks, queueMicrotask) is drained after the current task and before the next macrotask — high priority.
  • The macrotask/callback queue (timers like setTimeout, I/O, events) runs one task per loop iteration.

Promises resolve via the microtask queue, so .then/await continuations run before the next setTimeout.

Why it matters in RN: because it's single-threaded, flooding microtasks or running long synchronous work blocks everything — including UI updates. Understanding that await yields to the event loop (it doesn't block the thread) but a sync loop does block it explains a lot of RN performance behavior. This is a frequent JS-fundamentals question.

Q41. The interviewer says: "I parse a huge JSON array and the app freezes during it. Fix it." What do you do?

The freeze is because parsing/processing a large array synchronously blocks the JS thread, so React can't update and the UI stalls. async/await alone won't help if the work itself is a synchronous CPU loop — it still runs on the one JS thread.

Fixes:

  • Break the work into chunks and yield between them (e.g., process N items, then setTimeout(0)/InteractionManager.runAfterInteractions to let the UI breathe), so you don't block in one long burst.
  • Defer heavy work until after animations/interactions with InteractionManager.
  • Offload to native via a TurboModule/native module (do the heavy parsing in C++/native, return the result via JSI) for truly heavy work.
  • Paginate/virtualize so you never need to process the whole array at once.
  • Avoid unnecessary re-parsing; cache results.

This "heavy sync work freezing the JS thread" scenario is one of the most-asked RN async questions because single-threaded JS is so commonly misunderstood.

Q42. What is InteractionManager and when do you use it?

InteractionManager lets you schedule work to run after animations/interactions have finished, so you don't jank an in-progress transition. You wrap deferrable work in InteractionManager.runAfterInteractions(() => { ... }).

Real use: when navigating to a new screen, you might want to delay an expensive data load or computation until the navigation animation completes — running it during the animation would drop frames. So you defer it with runAfterInteractions, keeping the transition buttery.

Senior nuance: it's a coarse tool (it waits for interactions to end, not a true scheduler), and for chunked work you still combine it with batching. Knowing it exists and why (don't compete with animations) is the practical point interviewers look for.

Q43. What's the difference between setTimeout, Promise.then, and how do they order?

Given the event loop:

  • Promise.then/await continuations go to the microtask queue, drained immediately after the current synchronous code, before any timers.
  • setTimeout(fn, 0) goes to the macrotask queue, running after all current microtasks.

So in:

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
Enter fullscreen mode Exit fullscreen mode

The order is A, D, C, B — sync first (A, D), then microtask (C), then the timer (B). This classic ordering question tests whether you truly understand microtasks vs macrotasks, which directly affects timing bugs in RN.

Q44. The user navigates away while a fetch is in flight. How do you avoid "setState on an unmounted component"?

After an await, the component may have unmounted. Updating its state then triggers warnings/bugs. Defenses:

  • Track mounted status with a ref or an AbortController, and bail out:
useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(e => { if (e.name !== 'AbortError') handle(e); });
  return () => controller.abort();   // cancel on unmount
}, [url]);
Enter fullscreen mode Exit fullscreen mode
  • AbortController actually cancels the fetch and the effect cleanup runs on unmount.
  • With React Query, this is handled for you — queries are cancelled/ignored on unmount automatically.

Senior point: prefer AbortController (real cancellation) over a mounted-flag hack, and note that React Query removes this whole class of bug. That's the current best-practice answer.

Q45. What is the difference between server state and client state, and why does it matter?

  • Client state — UI/app state your app owns: a modal's open/closed, a form's input, the selected tab. It's synchronous and lives only in the app.
  • Server state — data that lives on a server, fetched async, cached on the client, can become stale, and may change outside your app (other users, other devices). Examples: a product list, a user profile from an API.

Why it matters: people often jam server state into Redux/useState and then hand-write loading flags, caching, refetching, and invalidation — error-prone and verbose. Server state has unique needs (caching, background refetch, staleness, dedup) that a dedicated tool like React Query (TanStack Query) handles. Separating the two — client state in Context/Zustand/Redux, server state in React Query — is a major modern best practice and a strong senior answer.

Q46. What is React Query (TanStack Query) and what problem does it solve?

React Query manages server state — fetching, caching, synchronizing, and updating remote data — so you don't hand-write it. You define a query with a key and a fetch function; it gives you data, isLoading, isError, and handles:

  • Caching and deduplication (same query isn't fetched twice).
  • Background refetching and staleness control (keep data fresh).
  • Automatic retries, pagination/infinite queries, and cache invalidation after mutations.
  • Cancellation on unmount.
const { data, isLoading, error } = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
});
Enter fullscreen mode Exit fullscreen mode

Why it's everywhere in 2026: it removes a huge amount of boilerplate and bugs around remote data, letting Redux/Zustand focus on pure client state. Explaining "React Query owns server state; client-state libraries own UI state" is the modern architecture answer.

Q47. How do you handle errors in async code (Promises and async/await)?

  • async/await: use try/catch/finally:
try { const r = await api.fetch(); }
catch (e) { handle(e); }
finally { setLoading(false); }
Enter fullscreen mode Exit fullscreen mode
  • Promises: use .catch() (and .finally()); never leave a promise without error handling.

Senior points: distinguish recoverable errors (show a retry) from fatal ones; map low-level failures (network down, timeout, non-2xx, parse error) to friendly UI states; use a typed result/discriminated union (with TypeScript) so the UI handles every case; add an Error Boundary for render-time errors; and report unexpected errors (Sentry/Crashlytics). Don't swallow errors silently. Mentioning Error Boundaries (for render errors) plus try/catch (for async) shows you understand React's two error surfaces.

Q48. What is an Error Boundary and what can't it catch?

An Error Boundary is a React component (currently must be a class with componentDidCatch/getDerivedStateFromError, or you use a library) that catches JavaScript errors in its child component tree during rendering and shows a fallback UI instead of crashing the whole app.

What it catches: errors thrown during rendering, in lifecycle methods, and in constructors of the components below it.

What it does NOT catch:

  • Errors in event handlers (use try/catch there).
  • Errors in asynchronous code (setTimeout, promises, await) — those aren't part of render.
  • Errors in the error boundary itself.

Why it matters: without boundaries, one render error can crash the entire RN app (red screen / blank). Wrapping screens in boundaries contains failures. Knowing its limits (no event handlers, no async) is the senior-level detail.

Q49. What is a race condition in async RN code, and give a real example?

A race condition occurs when the outcome depends on the unpredictable timing of async operations. Even on a single JS thread, interleaved awaits create races.

Real example — search: the user types "ab" then "abc". Two requests fire. If the "ab" response arrives after "abc" (slower network), you display stale "ab" results over "abc" — wrong.

Fixes: debounce input; cancel outdated requests (AbortController); or tag requests and ignore responses that aren't the latest (track a sequence id). React Query handles much of this automatically (it cancels and uses the latest). Recognizing that single-threaded ≠ race-free (because awaits interleave) is a sharp, senior-level insight.

Q50. How would you run truly heavy computation without blocking the JS thread in React Native?

Since the JS thread is single and blocking it freezes the UI, options for heavy work:

  • Chunk + yield — split the work and yield to the event loop between chunks (so the UI can render), via setTimeout/InteractionManager. Simple but still on the JS thread.
  • Offload to native — implement the heavy work in a native module / TurboModule (C++/Kotlin/Swift) and call it via JSI; native runs on its own threads, freeing the JS thread. Best for genuinely heavy CPU work (image processing, crypto, parsing).
  • react-native-worklets / Reanimated worklets or libraries that run JS on a separate thread/runtime for specific tasks.
  • Background tasks (e.g., react-native-background-fetch) for work that should continue outside the foreground.

Senior point: RN has no built-in web-worker-style threads for arbitrary JS by default, so the real answer for heavy CPU is usually native modules via JSI (or worklet runtimes). Knowing that distinguishes someone who understands RN's threading model deeply.


Section 5: Networking & APIs

Q51. Walk me through what happens from the moment a user taps "Load" to data appearing on screen.

  1. The UI calls a handler (or a React Query hook triggers).
  2. You set a loading state (or React Query reports isLoading) and call your data layer.
  3. The data layer calls the network (fetch/axios), which runs the request asynchronously.
  4. The JSON response comes back; you parse it (and ideally validate it, e.g., with Zod) into typed objects.
  5. The result may be cached (React Query cache or a local store) and returned.
  6. State updates to success(data) (or error).
  7. The UI re-renders to show the data.

Describing this clean flow — UI → data layer/React Query → fetch → parse/validate → cache → render — shows you understand structured data flow, not just "call fetch in a component." Mentioning runtime validation (Zod) of untrusted API data is a senior-level bonus.

Q52. fetch vs axios — when do you choose which?

  • fetch — built into the JS runtime, no dependency. Good for simple requests, but you handle more manually: it doesn't reject on HTTP error statuses (a 404/500 still resolves — you must check response.ok), no built-in timeout, manual JSON parsing, and no interceptors.
  • axios — a popular library offering interceptors (auto-attach tokens, logging, refresh), automatic JSON parsing, rejects on error statuses, request timeout, cancellation, and request/response transformation.

Choose: fetch for minimal needs; axios when you want interceptors, centralized auth/refresh, timeouts, and cleaner error handling — i.e., most production apps. Mentioning fetch's "doesn't reject on 404/500" gotcha is a sharp, commonly-tested detail.

Q53. A token expires mid-session and APIs start returning 401. How do you refresh it without breaking the user's flow?

Use an axios response interceptor: when a request fails with 401, the interceptor:

  1. Calls the refresh-token endpoint to get a new token.
  2. Saves it (to secure storage).
  3. Retries the original failed request with the new token, transparently.

The detail interviewers probe: handle concurrent 401s. If several requests fail at once, you must not fire multiple refreshes. Use a flag/lock with a queue so only one refresh runs; the other failed requests wait for it and then retry with the new token. With axios you typically queue pending requests during refresh, then replay them. Mentioning this race condition and the single-refresh queue is a strong senior signal.

Q54. How do you model network errors so the UI shows something sensible?

Represent outcomes explicitly. With TypeScript, use a discriminated union (or a Result type):

type Result<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: AppError };
Enter fullscreen mode Exit fullscreen mode

In the data layer, catch low-level failures (network down, timeout, non-2xx, parse/validation error) and map them to a friendly AppError. The UI renders by case: spinner for loading, content for success, a retry message for error.

Why a union: TypeScript forces you to handle every case (exhaustive switch), so you can't forget the error state — which is exactly the state that gets forgotten and causes blank screens. (React Query gives you isLoading/isError/data directly, encoding the same idea.)

Q55. The user is on a flaky connection. How do you make networking resilient?

  • Timeouts — set request timeouts so calls don't hang forever (axios timeout, or AbortController with fetch).
  • Retries with backoff — retry transient failures with increasing delays, capped (React Query has built-in retry).
  • Caching — serve cached data when offline (React Query cache, or a local store).
  • Offline-first — read from local storage first (instant UI), then refresh from network and update the store.
  • Connectivity awareness — use @react-native-community/netinfo to detect connectivity and show "offline"/"retrying" states instead of an infinite spinner.

Senior point: the best UX is offline-first with local storage (or React Query's cache + persistence) as the source of truth — the network just keeps it fresh. That framing stands out.

Q56. What are interceptors and give two real uses.

An interceptor (in axios) runs on every request/response, letting you read or modify both. Two common uses:

  1. Auth interceptor — automatically attach the Authorization: Bearer <token> header to every request (and refresh on 401 — Q53), so you don't repeat it everywhere.
  2. Logging interceptor — log requests/responses during development for debugging.

Other uses: adding common headers, retry logic, transforming/normalizing responses, and global error handling. Interceptors centralize cross-cutting networking concerns in one place instead of scattering them across every call — that's the value to articulate.

Q57. How do you handle JSON parsing safely, and what is Zod's role?

response.json() (or axios) gives you data typed as any/your assumed type — but the server could send something unexpected (missing fields, wrong types, nulls), and TypeScript can't verify runtime data. Blindly trusting it causes crashes deep in your UI.

Zod (or similar) validates API responses at runtime against a schema and infers the TypeScript type from it:

const User = z.object({ id: z.string(), name: z.string() });
const user = User.parse(json);   // throws if shape is wrong
Enter fullscreen mode Exit fullscreen mode

Why it matters: you catch bad data at the boundary with a clear error, instead of a mysterious crash later, and you get type safety that actually reflects runtime reality. Mentioning runtime validation (Zod) bridging the TS-can't-check-runtime gap is a strong, current senior answer.

Q58. What is pagination/infinite scroll and how do you implement it in React Native?

Pagination loads data in pages (e.g., 20 at a time) instead of all at once — essential for big feeds (less data, less memory, faster first render).

Implementation: with FlatList, use onEndReached (and onEndReachedThreshold) to fetch the next page as the user nears the bottom, append results, and show a footer spinner; handle the "no more pages" end state and dedup. React Query's useInfiniteQuery handles the paging logic, caching, and "fetch next page" cleanly:

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ /* ... */ });
Enter fullscreen mode Exit fullscreen mode

Pairing FlatList onEndReached with useInfiniteQuery is the modern, robust approach, and doing pagination well (smooth, no duplicates, low memory) is a very common practical task.


Section 6: Data Persistence — Storage, SQLite, Secure Storage

Q59. A user toggles dark mode. After relaunch, the app forgot it. Where should you store this and why?

A simple preference like this belongs in a key-value store — AsyncStorage (classic) or MMKV (modern, much faster).

await AsyncStorage.setItem('isDarkMode', 'true');
Enter fullscreen mode Exit fullscreen mode

Why not a variable: variables/state die with the process. Why a key-value store: it's the right-sized tool for tiny settings (theme, flags, last tab). You read it at startup and apply the theme.

What NOT to put there: large data (big lists, images) and secrets (tokens, passwords — those need secure storage). Senior note: AsyncStorage is asynchronous and (on Android) has size limits; MMKV is synchronous and far faster (built on JSI), so many teams now prefer MMKV for preferences. Mentioning MMKV as the faster modern choice signals you're current.

Q60. AsyncStorage vs MMKV vs SQLite/WatermelonDB vs Keychain vs Files — how do you choose?

  • AsyncStorage — simple async key-value for small settings. Easy, but slower and not for big/secure data.
  • MMKV — fast, synchronous key-value (JSI-based), great for preferences and small data; can be encrypted.
  • SQLite (op-sqlite / react-native-quick-sqlite) / WatermelonDB / Realm — structured, queryable databases for complex, related, large datasets (offline apps, lots of records). WatermelonDB/Realm add reactive, scalable data layers.
  • Keychain / EncryptedStorage (react-native-keychain) — encrypted storage for secrets: tokens, passwords (iOS Keychain / Android Keystore).
  • Files (react-native-fs) — large blobs: images, downloaded PDFs/videos. Store the file; keep its path in your DB.

Rule: Settings → MMKV/AsyncStorage. Secrets → Keychain. Structured/queryable → SQLite/WatermelonDB/Realm. Big binaries → Files.

Q61. Why is MMKV faster than AsyncStorage?

AsyncStorage is asynchronous and, on the old architecture, communicates over the bridge (serialized), and on Android historically used a SQLite-backed store — all of which add overhead, so reads/writes are slower and you must await them.

MMKV is built on JSI (direct, synchronous native access — no bridge serialization) and uses an efficient memory-mapped storage mechanism (originally from Tencent). So MMKV reads/writes are synchronous and dramatically faster, with optional encryption built in.

Practical impact: for frequently-accessed values (feature flags, auth token presence, preferences read at startup), MMKV's speed and synchronous API simplify code and improve startup. Explaining "JSI + synchronous + memory-mapped" vs "async + bridge" is the precise, current answer.

Q62. How do you keep the UI in sync with a local database automatically?

Use a database with reactive/observable queries — WatermelonDB and Realm both let you observe a query so the UI re-renders automatically when the data changes (insert/update/delete). You connect components to observables (WatermelonDB's withObservables, Realm's hooks) and they update live.

Why powerful: the database becomes the single source of truth. You never manually tell the UI "data changed, reload" — it observes the store and reacts, which is the foundation of offline-first apps. With plain SQLite you'd manually re-query and update state after writes (more boilerplate), which is exactly what reactive databases remove. Knowing the reactive-DB approach (live UI from observables) is the senior-level point.

Q63. Your app freezes when inserting a large batch into the local database. Why, and how do you fix it?

Two likely causes:

  • Inserting rows one-by-one in separate transactions — each commit has overhead, so thousands of individual inserts are very slow.
  • Doing heavy prep on the JS thread and/or going over the bridge repeatedly (old architecture), each call serialized.

Fixes:

  • Wrap inserts in a single transaction / batch so it's one commit, not thousands — dramatically faster.
  • Use a JSI-based SQLite library (op-sqlite / react-native-quick-sqlite) that avoids bridge serialization and is much faster for bulk operations.
  • For very heavy preparation, chunk the work or offload to native.
  • Don't re-render the UI mid-import; update once at the end.

Knowing "batch in a transaction" + "use a JSI SQLite lib to avoid bridge overhead" is the senior-level fix.

Q64. You add a new column to a table and existing users' apps crash on update. Why, and how do you fix it?

The on-device database was created with the old schema; your new code queries a column that doesn't exist there → crash on update.

Fix — migrations: databases track a schema version. On opening, if the stored version is older, run migration logic to alter the existing database (add the column, transform data). SQLite libraries and WatermelonDB/Realm all provide a migrations mechanism where you define how to go from version N to N+1.

Never "delete and recreate the database" in production to dodge migrations — it wipes user data. That's a classic trap; call it out. Handling schema migrations correctly is a frequent real-world interview topic, especially for offline-first apps.

Q65. Where should you store auth tokens, and why not AsyncStorage?

Auth tokens belong in secure storage — react-native-keychain (or EncryptedStorage / encrypted MMKV) — not plain AsyncStorage.

Why: AsyncStorage stores values in plain, unencrypted form that can be read from backups or on a rooted/jailbroken device. Keychain/Keystore-backed storage encrypts secrets at rest using the platform's secure facilities (iOS Keychain, Android Keystore), and can require device unlock or biometrics.

Senior point: even secure storage isn't invincible on a fully compromised device, so store short-lived tokens, minimize what you keep, and consider biometric-gated access for sensitive values. Treating tokens as secrets (not ordinary storage) is the key judgment interviewers check.

Q66. What is WatermelonDB/Realm and when would you pick it over plain SQLite?

WatermelonDB and Realm are higher-level reactive databases for RN:

  • WatermelonDB is built on SQLite but optimized for large datasets and offline-first: it's lazy (loads records as needed), reactive (observe queries → live UI), and scales to tens of thousands of records smoothly.
  • Realm is an object database with its own engine, offering reactive queries, relationships, and optional sync (Atlas Device Sync).

Pick over plain SQLite when: you need reactive UI, offline-first sync, complex relationships, and scalability without writing tons of SQL and manual change-propagation. Pick plain (JSI) SQLite when: you want lightweight, direct SQL control without a heavier abstraction. Matching the tool to scale + reactivity needs is the mature answer.

Q67. How would you implement an offline-first feature (e.g., notes that work without internet)?

Local store as the single source of truth:

  • The UI reads notes from the local database (WatermelonDB/Realm/SQLite) via observable queries, so it shows data instantly, online or offline.
  • When the user creates/edits a note, write to the local store first (instant, never lost) and mark it "needs sync."
  • A sync layer pushes unsynced changes to the server when connectivity returns (detected via NetInfo) and pulls remote changes into the local store. WatermelonDB has a built-in sync protocol.
  • The UI never talks to the network directly — it observes the local store, which the sync layer keeps fresh.

Senior depth: discuss conflict resolution — same note edited on two devices (last-write-wins, version numbers, or merge). Background sync can use react-native-background-fetch so sync runs even when the app isn't open.

Q68. What is react-native-fs (file storage) and when do you need it?

react-native-fs (and similar like Expo FileSystem) gives access to the device file system — reading/writing files in app-specific directories (Documents, Cache, Temp).

When you need it: storing large binaries that don't belong in a key-value store or database row — downloaded images/PDFs/videos, generated files, caches. You store the file on disk and keep its path/metadata in your database.

Senior points: use the correct directory by purpose — cache/temp directories can be purged by the OS (don't store must-keep data there), while the documents directory persists. Apps are sandboxed, so you can't write arbitrary paths. Choosing cache vs documents by data importance (purgeable vs must-keep) is the practical detail interviewers like.


Section 7: State Management & Architecture

Q69. The interviewer asks: "When do you actually need a state management library beyond useState?" How do you answer?

useState (and useReducer) are great for local state. You reach for more when:

  • Many distant components need the same state → avoid prop-drilling.
  • State is complex or updated from many places → centralize it.
  • You need server state (caching, refetch, invalidation) → that's React Query, a different category.

But you don't always need Redux. The progression is: useState → lift state up → Context for low-frequency shared state (theme, auth) → a client-state library (Zustand/Redux Toolkit/Jotai) for complex/global client state → React Query for server state. The mature answer is "match the tool to the need, and don't reach for Redux by default" — naming the client-vs-server-state split impresses.

Q70. What is the Context API, and what's its big performance pitfall?

The Context API lets you provide a value at a high level and consume it anywhere below without prop-drilling — great for low-frequency, app-wide data: theme, current user, locale.

The pitfall: when a Context value changes, every component that consumes it re-renders — and if you put frequently-changing state (or a large object) in one Context, you cause widespread re-renders. Also, passing a new object literal as the value each render makes all consumers re-render every time.

Fixes: split contexts by concern (don't put everything in one), memoize the value (useMemo), keep fast-changing state out of Context (use Zustand/Redux with selectors instead). Knowing Context is for low-frequency shared state — and why it's not a general state manager — is the key senior insight.

Q71. What is Redux, and what problem was it designed to solve? Explain the core flow.

Redux is a predictable client state container with a strict unidirectional flow:

  • A single store holds the app state.
  • The UI dispatches actions (plain objects describing "what happened").
  • Reducers (pure functions) take the current state + action and return the new state.
  • The UI subscribes and re-renders from the new state.
UI --dispatch(action)--> Reducer --new state--> Store --> UI
Enter fullscreen mode Exit fullscreen mode

Problem it solved: in large apps, scattered, unpredictable state mutations made bugs hard to trace. Redux centralizes state, makes updates pure and traceable (great DevTools/time-travel debugging), and enforces one-way flow. Caveat: classic Redux was boilerplate-heavy, which is exactly why Redux Toolkit exists (next question).

Q72. What is Redux Toolkit and why is it the recommended way to use Redux now?

Redux Toolkit (RTK) is the official, batteries-included way to write Redux that removes the old boilerplate:

  • createSlice generates actions + reducers together, with much less code.
  • It includes Immer, so you can write "mutating" reducer code (state.value++) that's safely turned into immutable updates.
  • configureStore sets up the store with good defaults (DevTools, thunk middleware).
  • RTK Query adds data fetching/caching (a React Query–like server-state solution within Redux).

Why recommended: classic Redux's verbosity (action types, creators, switch reducers, manual immutability) drove people away; RTK fixes all of it. In 2026, "use Redux" means "use Redux Toolkit." Mentioning createSlice, Immer, and RTK Query shows current knowledge.

Q73. Redux Toolkit vs Zustand vs Jotai vs MobX — how do you choose?

  • Redux Toolkit — structured, predictable, great DevTools/time-travel, strong for large apps/teams needing strict patterns; more ceremony than the lightweight options.
  • Zustand — minimal, hook-based store with very little boilerplate; you create a store and use it via a hook with selectors (fine-grained re-renders). Very popular for its simplicity.
  • Jotai — atom-based (bottom-up): tiny pieces of state ("atoms") you compose; great for granular, derived state.
  • MobX — observable/reactive; you mark state observable and components auto-react to what they use. Less boilerplate, but "magic" that some teams avoid.

The mature answer: no universal best. For new 2026 apps, many pick Zustand (simplicity) or Redux Toolkit (structure), and React Query alongside for server state. Match to team size, complexity, and preferred style, and acknowledge trade-offs rather than declaring a winner.

Q74. What is Zustand and why has it become so popular?

Zustand is a small, fast client-state library. You create a store with a hook:

const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}));

// in a component:
const count = useStore((s) => s.count);
Enter fullscreen mode Exit fullscreen mode

Why popular:

  • Almost no boilerplate — no providers, actions, or reducers ceremony.
  • Selectors give fine-grained re-renders — a component subscribes only to the slice it reads, so it re-renders only when that slice changes.
  • Works outside React too (you can read/update the store anywhere), and it's tiny.

It hits a sweet spot between useState/Context (too limited for global state) and Redux (more structure than many apps need). Knowing the selector-based fine-grained re-render benefit is the strong point.

Q75. What is the repository/service pattern and why add it in a React Native app?

A repository/service layer is the single place that owns access to a type of data, hiding where it comes from. Your hooks/components ask the repository for "the user"; it decides whether to return cached data or fetch from the network.

Why add it:

  • Single source of truth and one place for caching/transform logic.
  • Components/hooks stay focused on UI, not data plumbing.
  • Swappable sources — change APIs or add caching without touching the UI.
  • Testability — inject/mock the repository to test logic in isolation.

In RN, this often manifests as an api layer + React Query hooks wrapping it, keeping fetch logic out of components. Separating data access from UI is the backbone of a maintainable RN architecture.

Q76. How do you structure a scalable React Native project (folders/architecture)?

Two common, sound approaches:

  • Feature-based (recommended for scale): group by feature (features/auth, features/cart), each containing its components, hooks, state, and api. Keeps related code together and scales with teams.
  • Layer-based: group by type (components/, hooks/, services/, store/). Simple for small apps but spreads a feature across many folders as it grows.

Layered on top, many teams apply Clean Architecture ideas: presentation (components/screens), domain (business logic/use cases, pure TS), and data (api, repositories, storage).

Senior point: feature-based + a clear data layer (api/repos) + React Query for server state + a light client-state store scales well. Match structure to app size; over-architecting a small app is a red flag. Explaining feature-based organization and the client/server state split is the strong answer.

Q77. What is the container/presentational pattern, and is it still relevant with hooks?

The container/presentational split separates how things work (container: data fetching, state, logic) from how things look (presentational: pure UI from props).

With hooks, it evolved: instead of a separate container component, you put the logic in a custom hook (useProductList) and keep the component focused on rendering what the hook returns. So the spirit — separating logic from presentation for reuse and testability — is very much alive, just expressed via hooks rather than wrapper components.

Why it still matters: presentational components are easy to test and reuse (pure props in, UI out), and logic in hooks is testable in isolation. Showing you apply the principle through custom hooks (not dogmatic container components) is the modern, senior take.

Q78. A junior put all the fetching and business logic inside the screen component. What breaks, and how do you refactor?

Problems:

  • Huge, untestable components — logic tangled with JSX can't be unit-tested easily.
  • No reuse — logic trapped in one screen.
  • Re-render/effect bugs — fetching in the component with bad deps causes refetch loops or stale data.
  • Unmaintainable — everything coupled.

Refactor path:

  • Move server data into React Query hooks (caching, loading, error for free).
  • Move business logic into custom hooks / a domain layer.
  • Move API calls into an api/repository layer.
  • Keep the component focused on rendering state and dispatching events.

This is "extract logic into hooks + a data layer," and explaining the why (testability, reuse, fewer effect bugs) matters more than the buzzword.

Q79. What is the singleton pattern, where is it appropriate in RN, and the danger of overusing it?

A singleton is a single shared instance app-wide. Appropriate for genuinely single shared resources — a configured axios instance, an analytics client, a database connection, an MMKV instance.

The danger of overusing it: singletons create hidden global state that everything secretly depends on, making code hard to test (you can't easily swap the real instance for a fake) and hard to reason about (any code can mutate shared state from anywhere). They also don't trigger re-renders — using a mutable module-level singleton as "state" means the UI won't update.

Better approach: for shared state, use a proper store (Zustand/Redux); for shared services, a single configured instance is fine, but consider injecting it (via Context or imports) so tests can substitute fakes. Knowing when a singleton is a service vs misused as state is the senior judgment.

Q80. How do you decide how much architecture an app needs?

Match architecture to complexity and lifespan:

  • A small app: hooks + React Query + a light store (Zustand) + a thin api layer is plenty. Adding full Clean Architecture with many layers would be over-engineering.
  • A large, team-built, long-lived product: feature-based structure, clear domain/data layers, a structured store (RTK), strict boundaries, and conventions pay off in maintainability and parallel teamwork.

The senior mindset: architecture is a tool to manage complexity, not a trophy. Add structure when the pain of not having it shows up (hard testing, merge conflicts, tangled effects), not pre-emptively everywhere. Saying this demonstrates maturity beyond memorized patterns — and interviewers specifically listen for it.


Section 8: Native Modules & Dependency Patterns

Q81. What is a Native Module, and when do you actually need to write one?

A Native Module lets your JavaScript call native platform code (Kotlin/Java on Android, Swift/Obj-C on iOS) for capabilities React Native doesn't expose.

When you need one:

  • Access a native SDK with no RN wrapper (a payment SDK, a specific Bluetooth/hardware API, an ML library).
  • Reuse existing native code.
  • Do heavy native computation that shouldn't run on the JS thread.

You expose native methods to JS; in the old architecture this went over the bridge (async, serialized), and in the New Architecture you write a TurboModule called directly via JSI (faster, can be synchronous).

Senior point: before writing one, check if a community package already exists — and know that TurboModules are the modern way. Knowing when native code is justified (vs staying in JS) is the judgment interviewers want.

Q82. What's the difference between a Native Module and a Native UI Component?

  • A Native Module exposes native functionality/methods to JS (e.g., "get battery level," "start a native scanner") — it's about logic/APIs, not rendering.
  • A Native UI Component wraps an actual native view (a UIView/Android View) so you can use it as a React component (e.g., a native map view, a native video player, a platform-specific control). In the New Architecture, these are built with Fabric.

Why the distinction matters: if you need behavior/data, you write a module; if you need to render a native view inside your RN tree, you write a UI component (which integrates with the renderer/layout). Knowing both kinds — and that UI components plug into Fabric's rendering — shows a complete picture of RN↔native interop.

Q83. How do dependencies get "injected" in React Native (since there's no classic DI framework)?

RN doesn't use a classic DI container like Hilt/Dagger. Instead, dependency management is typically done with JavaScript patterns:

  • Module imports — import a single configured instance (e.g., a shared axios client) where needed.
  • React Context — provide services (an API client, a feature-flag service) at the top of the tree and consume them via useContext, so components don't hardcode a concrete dependency (great for swapping fakes in tests).
  • Custom hooks — wrap access to a dependency (useApi()), centralizing how it's obtained.
  • Constructor/parameter injection for plain classes/functions — pass dependencies in rather than importing them internally.

Why it matters for testing: depending on an abstraction provided via Context/props (not a hardcoded import) lets you inject a fake in tests. Explaining DI-by-Context/props (the React-idiomatic way) is the senior answer.

Q84. Why inject a dependency via Context/props instead of importing it directly?

If a component directly imports a concrete service (import { api } from './realApi'), it's tightly coupled to that implementation — in tests you can't easily swap it for a fake, and you can't reconfigure it per environment.

Injecting via Context/props means the component depends on an abstraction it receives, not a hardcoded module. Benefits:

  • In tests, provide a fake through the Context provider or as a prop — no real network/DB.
  • Swap implementations (real vs mock vs different env) without touching the component.
  • Clear contract of what the component needs.

This is the Dependency Inversion Principle applied the React way. It's what makes large RN codebases testable and flexible — and the testing payoff is exactly what interviewers probe.

Q85. What is Codegen in the New Architecture, and why does it matter?

Codegen is a build-time tool in the New Architecture that generates the type-safe native interface code (C++/Java/Obj-C glue) for your TurboModules and Fabric components from their JS/TypeScript type specs (called "specs").

Why it matters:

  • Type safety across the JS↔native boundary — the contract is defined once in TS and the native scaffolding is generated to match, reducing runtime type mismatches.
  • Less hand-written boilerplate for bridging.
  • It's how TurboModules/Fabric achieve their efficient, JSI-based interop.

You typically don't write Codegen output by hand; you define the spec and it generates the rest. Knowing that Codegen produces the type-safe native bindings from TS specs demonstrates real understanding of how the New Architecture's tooling fits together — a current, senior-level point.


Section 9: Performance, Memory & Rendering

Q86. What causes a React Native app to drop frames, and how do you keep 60/120fps?

Dropped frames (jank) happen when work exceeds the frame budget (~16ms at 60fps, ~8ms at 120Hz). The two thread families matter:

  • JS thread — runs React renders, logic, and (old arch) sends updates to native. Blocking it (heavy compute, too many re-renders) janks JS-driven UI.
  • UI/native thread — rendering, native animations, gestures.

Causes & fixes:

  • Too many/expensive re-renders (JS) → React.memo, useMemo/useCallback, narrower state, selectors.
  • Long synchronous JS work → chunk it, defer (InteractionManager), or offload to native.
  • Unoptimized lists → FlatList/FlashList with getItemLayout, memoized rows.
  • JS-driven animations → use useNativeDriver or Reanimated (run on UI thread).
  • Bridge traffic (old arch) → reduce chatty native calls; New Architecture/JSI helps.

Senior point: profile to find which thread is the bottleneck — JS-thread jank and UI-thread jank have different fixes. Saying "measure first" lands.

Q87. What is the most common cause of unnecessary re-renders, and how do you fix it?

The most common cause is components re-rendering when their data didn't actually change — usually because:

  • A parent re-renders, so all children re-render by default.
  • You pass new object/array/function instances as props each render, defeating memoization.
  • You read too-broad state (whole Context value, whole store) so any change re-renders you.

Fixes:

  • React.memo on components to skip re-render when props are shallow-equal — paired with useCallback/useMemo to keep prop identities stable.
  • Selectors (Zustand/Redux useSelector) so a component subscribes only to the slice it uses.
  • Split Context and keep fast-changing state out of it.
  • Avoid creating inline objects/functions in render where it matters.

Senior point: "re-render the smallest thing that depends on the change," and use React DevTools Profiler / why-did-you-render to find the culprits. Measure, then memoize precisely — don't memo everything.

Q88. The app's memory climbs while scrolling an image feed. What's going wrong?

Almost certainly image handling:

  • Loading full-resolution images into small cells — a huge photo in a thumbnail wastes memory.
  • No caching / no eviction — every image retained, so memory only grows.
  • Using ScrollView (or non-virtualized lists) so all rows/images stay mounted.

Fixes:

  • Use react-native-fast-image (caching, efficient native loading) and correctly sized images (resize on the server or request the right size).
  • Use FlatList/FlashList so off-screen rows (and their images) aren't all held; tune windowSize/removeClippedSubviews.
  • Avoid keeping large base64 image strings in JS memory.

Images are usually the #1 memory hog in real apps — and using FastImage + virtualized lists + correctly-sized images is the expected fix.

Q89. How do you find a memory leak in React Native (e.g., a listener or timer not cleaned up)?

Common leaks: not clearing setInterval/setTimeout, not removing event listeners/subscriptions (AppState, navigation, sockets, NetInfo), retaining large objects in closures, or keeping references after unmount.

How to find them:

  • "Update on unmounted component" warnings signal an async callback/subscription outliving the component.
  • Flipper (memory plugin) and the Hermes/Chrome debugger memory profiler to watch heap growth and take snapshots.
  • Xcode Instruments (iOS) / Android Studio Profiler for native-side memory.
  • Audit every useEffect that subscribes/creates timers for a matching cleanup return.

The practical rule: every subscription/timer/listener created in an effect has a matching teardown in its cleanup. Using Flipper/profilers to spot retained objects, plus the cleanup discipline, is the senior workflow.

Q90. What is Reanimated and why does it produce smoother animations than the Animated API?

Reanimated (2/3) runs animation and gesture logic on the UI thread using worklets — small JS functions that are compiled to run on the native UI thread, not the JS thread. So animations stay smooth even if the JS thread is busy (rendering, fetching, processing).

vs the Animated API: the classic Animated API runs on the JS thread unless you set useNativeDriver: true (which only supports transform/opacity). Reanimated removes that limitation — you can animate any value and drive complex, gesture-interruptible animations entirely on the UI thread (paired with react-native-gesture-handler).

Why it matters: for native-quality interactions (draggable sheets, shared element transitions, smooth gestures), Reanimated is the standard in 2026. Explaining worklets running on the UI thread is the core senior point.

Q91. What is a worklet in Reanimated, and how does it run off the JS thread?

A worklet is a JavaScript function (marked, often automatically, with 'worklet') that Reanimated can serialize and run on a separate UI-thread JS runtime. Because it executes on the UI thread, animation/gesture code inside it runs every frame without touching the (possibly busy) main JS thread, so it stays smooth.

Worklets can read/write shared values (state shared between the JS thread and UI thread) and respond to gestures immediately. You hop back to the JS thread when needed via runOnJS.

Why it matters: this is the mechanism that lets Reanimated keep 60/120fps under load — the animation logic literally isn't competing with your React renders. Knowing "worklets run on a UI-thread runtime, communicate via shared values, and runOnJS to return" is a strong, current, senior-level answer.

Q92. How do you profile and diagnose performance problems instead of guessing?

  • React DevTools Profiler — see which components render, how often, and how long — find unnecessary re-renders.
  • Flipper — inspect network, layout, performance, and memory (and React DevTools integration).
  • Hermes profiler / Chrome DevTools — JS CPU profiling to find hot functions and long tasks blocking the JS thread.
  • why-did-you-render — logs why a component re-rendered (great for hunting wasted renders).
  • The Perf Monitor (in-app dev menu) — shows JS thread and UI thread fps in real time, so you can see which thread is dropping frames.
  • Native profilers (Xcode Instruments, Android Studio Profiler) for native-side issues.

Senior mindset: "Measure first — find whether it's JS-thread or UI-thread, and which components/functions — then optimize." Guessing wastes effort and often fixes the wrong thing.

Q93. Why must you test performance in a release build, not dev mode?

In development mode, React Native runs with extra checks: dev warnings, prop-type validation, the bundler/HMR, no minification, and the debugger can route JS through Chrome (a totally different, slower execution path). So the app is much slower and not representative of production. You'll see jank that won't exist for users and get misleading numbers.

Always profile in a --release (production) build (with Hermes and minification on), which reflects real performance. Senior point: never judge performance — or claim "RN is slow" — from a dev build; and never profile while the JS debugger is attached, since that changes the runtime entirely. Knowing the dev vs release distinction catches a lot of candidates off guard and signals real experience.

Q94. How do you reduce your React Native app's size, and why does it matter?

Why it matters: smaller apps install faster, get more downloads (especially on slower networks and cheaper devices), and update faster.

Techniques:

  • Ship an Android App Bundle (.aab) so Google Play delivers device-specific APKs (only needed architectures/resources), and enable ABI splits.
  • Enable Hermes (smaller, faster than bundling full JS for JSC) and Proguard/R8 (Android) to shrink/obfuscate native+Java code.
  • Enable resource shrinking, compress images, use vector/@2x/@3x appropriately, and remove unused assets.
  • Audit dependencies — RN apps bloat from heavy libraries; remove or replace big ones.
  • Remove unused fonts/locales.

Mentioning App Bundles + Hermes + R8/Proguard + dependency audits shows real release experience.

Q95. What tools and habits help you ship a faster, leaner React Native app?

  • Hermes — faster startup, less memory.
  • The New Architecture (JSI/Fabric/TurboModules) — lower interop overhead, smoother UI.
  • FlatList/FlashList + memoized rows + getItemLayout — smooth lists.
  • Reanimated + gesture-handler — UI-thread animations/gestures.
  • React Query — efficient server-state caching (fewer redundant fetches).
  • React DevTools Profiler / Flipper / Perf Monitor — measure before optimizing.
  • Habits: memoize precisely, keep state narrow, avoid heavy work on the JS thread, profile in release.

Senior framing: a fast RN app comes from architecture + discipline during development (right list, right thread for animations, narrow re-renders) plus measurement, not last-minute fixes. Naming Hermes + New Architecture + the right list/animation tools signals you're current.


Section 10: Security

Q96. How do you store an API key or secret safely in a React Native app?

First, the hard truth: anything in your JS bundle or app binary can be extracted — RN bundles are notoriously easy to read (the JS can be unpacked). So:

  • Never hardcode secrets in JS source or commit them — they're trivial to extract from the bundle.
  • Keep secrets off the client — route sensitive calls through your own backend that holds the real secret.
  • For values that must be on-device, use secure storage (Keychain/Keystore via react-native-keychain), not AsyncStorage.
  • Use react-native-config/env files to keep keys out of source control (but understand they still end up in the binary).
  • Obfuscation (Hermes bytecode, R8/Proguard) is a speed bump, not real security.

Senior framing: "minimize and protect, but assume the client is hostile territory." Backend-held secrets beat client-side every time — especially for RN where the bundle is easy to inspect.

Q97. What is certificate pinning, and what attack does it stop?

Certificate pinning makes your app trust only a specific server certificate or public key, rather than any certificate a Certificate Authority signed.

Attack it stops: a man-in-the-middle (MITM) — where an attacker uses a rogue CA or proxy to present a fake-but-"valid" certificate and read/modify your traffic. With pinning, the connection is rejected unless the server's certificate matches your pinned value. In RN you implement it via libraries (e.g., react-native-ssl-pinning) or native networking config (Android Network Security Config, iOS App Transport Security with pinning).

Trade-off: pinned certs expire/rotate; if you pin and forget to update before rotation, you break your own app. So pin carefully with backup pins and a rotation plan. Naming this risk shows real-world experience.

Q98. How do permissions and the principle of least privilege work in React Native?

Least privilege: request only the permissions you truly need, when you need them, with a clear reason.

How it works: you declare permissions in AndroidManifest.xml and Info.plist (with usage-description strings on iOS — forget them and the app crashes), then request runtime permissions with a library like react-native-permissions. Request in context (ask for camera when the user taps "take photo," not at launch), explain why first, and handle denial gracefully (degrade the feature, don't crash). Modern platforms offer limited/approximate options (approximate location, limited photos). Showing you request contextually and handle denial — rather than dumping all permission prompts at launch — is the practical, mature answer.

Q99. What are common React Native security mistakes, and how do you protect data?

Common mistakes:

  • Secrets hardcoded in JS (easily extracted from the bundle).
  • Tokens in AsyncStorage instead of Keychain/secure storage.
  • Logging sensitive data (tokens/PII) — readable via logs/Flipper in dev or crash reports.
  • Disabling SSL/cert validation to "make it work."
  • Trusting deep-link/notification payloads without validation.
  • Outdated dependencies with known vulnerabilities.

Protections:

  • Secrets on the backend; on-device secrets in secure storage.
  • Validate/sanitize all external input (deep links, API data — use Zod at the boundary).
  • Enable Hermes + R8/Proguard and obfuscation for release.
  • Be careful with CodePush/OTA updates (only from trusted sources, signed) since they ship JS at runtime.

Spotting these in review is exactly what senior interviewers want to hear.

Q100. What security considerations apply to OTA updates (CodePush / Expo Updates)?

Over-the-air (OTA) updates let you push JavaScript bundle changes without an app-store release (CodePush, Expo Updates). That power has security implications:

  • The update channel must be trusted and authenticated — if an attacker could push a malicious bundle, they'd run code in your app. Use signed updates and secure your deploy credentials.
  • OTA can only update JS/assets, not native code — and app stores require that OTA updates don't change the app's core purpose.
  • Validate that updates come from your controlled service over HTTPS.

Senior point: treat the OTA pipeline as a security-critical path (signing, access control, integrity checks), because it's effectively remote code delivery to users' devices. Knowing OTA's power and its risk (signed updates, JS-only, trusted channel) is a sharp, current answer for 2026.


Section 11: Testing

Q101. What types of tests exist in React Native and what does each cover?

  • Unit tests (Jest) — test a single function/hook/reducer in isolation, no UI, very fast. The bulk of your tests.
  • Component tests (React Native Testing Library) — render a component in a test environment and interact with it (query by text/role, fire press/change events), asserting behavior — without a real device. Fast and high-value.
  • Integration tests — test multiple units together (a screen + its hooks + a mocked api).
  • E2E tests (Detox / Maestro) — run the whole app on a device/emulator, driving real user flows. Slower but realistic.

The testing pyramid: many unit tests, a solid layer of component tests, fewer slow E2E tests. Detox is the common RN E2E tool (gray-box, syncs with the app); Maestro is a newer, simpler option. Mentioning RNTL for components and Detox/Maestro for E2E shows current knowledge.

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

RNTL encourages testing what the user sees and does, not implementation details:

test('increments on press', () => {
  render(<Counter />);
  expect(screen.getByText('0')).toBeTruthy();
  fireEvent.press(screen.getByText('Increment'));
  expect(screen.getByText('1')).toBeTruthy();
});
Enter fullscreen mode Exit fullscreen mode

You query by visible text, accessibility role/label, or testID; fire events (fireEvent.press, fireEvent.changeText); and assert on what's rendered. For async UI, use findBy* queries or waitFor to wait for updates.

Philosophy: query the way a user would (text/role) over brittle internal selectors, so tests survive refactors. Knowing findBy/waitFor for async and testID as a last resort is the practical detail.

Q103. How do you test a component/hook that fetches data, and how do you mock the network?

Inject/mock the data source so tests are deterministic and offline:

  • Mock the api module with jest.mock, returning controlled data or errors.
  • Or use MSW (Mock Service Worker) to intercept network requests at the boundary and return canned responses — realistic and decoupled from implementation.
  • For React Query, wrap the component in a QueryClientProvider with a fresh QueryClient per test, and mock the fetcher.

Then assert the UI transitions: loading → success(data), or loading → error, using findBy*/waitFor for the async updates.

Senior point: prefer MSW (mock at the network layer) or mocking the api layer (an abstraction) over mocking fetch directly — it's more robust to refactors. Connecting testability to having a clean api/abstraction layer is the strong point.

Q104. What's the difference between a mock, a stub, and a fake — and which do you prefer?

  • Stub — returns canned responses ("always return this user").
  • Mock — also verifies interactions ("was this called once with these args?") — Jest's jest.fn() supports this.
  • Fake — a lightweight working implementation (e.g., an in-memory repository backed by an object).

Preference: fakes are often the most robust and readable for data sources — they behave like the real thing and don't break when you refactor internal call patterns. Mocks are useful when you specifically need to verify an interaction happened. MSW is a great middle ground for the network boundary. Good engineers pick the right one per situation — and naming that nuance impresses.

Q105. What makes a test "good" versus one that just pads coverage?

A good test:

  • Tests behavior (what the user sees/does), not implementation details — so refactoring doesn't break it.
  • Is deterministic — no flakiness from real timers, network, or random data (use Jest fake timers, mock the network).
  • Is readable — its name and body clearly state what's expected.
  • Fails for the right reason and catches real regressions.

Anti-patterns: chasing a coverage percentage with trivial tests, asserting on internal state/structure (brittle), over-mocking until the test re-asserts its own setup, and flaky E2E tests everyone learns to ignore. Saying "100% coverage isn't the goal; catching real bugs is" shows maturity.


Section 12: JavaScript / TypeScript Deep-Dive Scenarios

Q106. What is a closure, and give a real React Native bug caused by a stale closure.

A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished. It "closes over" those variables.

Stale closure bug in RN: a useEffect or setInterval captures a state value at the time it was created, and never sees later updates:

useEffect(() => {
  const id = setInterval(() => console.log(count), 1000); // always logs initial count
  return () => clearInterval(id);
}, []);   // empty deps → closure captured count once
Enter fullscreen mode Exit fullscreen mode

The interval's callback closed over the first count (0) and never updates. Fixes: add count to deps (recreate the effect), use the functional updater (setCount(c => c + 1)), or store the latest value in a ref. Explaining stale closures — the #1 hooks gotcha — is a strong, practical answer.

Q107. Explain var, let, and const, and what hoisting means.

  • var — function-scoped, hoisted and initialized as undefined, can be redeclared. Legacy; avoid.
  • let — block-scoped, hoisted but not initialized (the "temporal dead zone" — accessing before declaration throws), reassignable.
  • const — block-scoped like let, but cannot be reassigned (though a const object's contents can still mutate).

Hoisting means declarations are conceptually "moved to the top" of their scope: var declarations are hoisted and set to undefined; let/const are hoisted but in the temporal dead zone until their declaration runs; function declarations are fully hoisted.

Best practice: default to const, use let only when reassigning, never var. Knowing the TDZ and why const is the default shows solid JS fundamentals.

Q108. What's the difference between == and === in JavaScript?

  • == (loose equality) performs type coercion before comparing — so 0 == '', 1 == '1', and null == undefined are all true, which causes surprising bugs.
  • === (strict equality) compares value and type with no coercion — 1 === '1' is false.

Best practice: always use === (and !==) to avoid the unpredictable coercion rules; the only common, intentional == use is value == null to check for both null and undefined at once.

Why it matters in RN/React: subtle coercion bugs in conditionals (e.g., if (count == false)) cause wrong UI states. Knowing the coercion pitfalls and defaulting to === is expected JS knowledge.

Q109. What is this in JavaScript and why do arrow functions help in React?

this refers to the execution context — and in regular functions it's determined by how the function is called (not where it's defined), which historically caused bugs (losing this when passing methods as callbacks).

Arrow functions don't have their own this — they inherit this from the surrounding (lexical) scope. This is why, in class components, people either bind methods or use arrow-function class properties, and why event handlers as arrow functions "just work."

In modern RN (function components + hooks), you rarely deal with this at all — that's part of why hooks reduced a whole class of this-binding bugs. Explaining "regular functions: this depends on call site; arrow: inherits lexically" — and that hooks sidestep it — is the complete answer.

Q110. What are Promises, and what's the difference between Promise.all, Promise.race, and Promise.allSettled?

A Promise represents a future value — pending, fulfilled, or rejected. Combinators:

  • Promise.all([...]) — resolves when all resolve (returns all results); rejects as soon as any one rejects. Use for "I need every result."
  • Promise.race([...]) — settles as soon as the first one settles (resolve or reject). Use for racing/timeouts.
  • Promise.allSettled([...]) — waits for all to settle and returns each outcome (fulfilled/rejected) without rejecting — use when you want partial results and don't want one failure to lose everything.

Scenario: load user + posts + settings where all are required → Promise.all. Load from several sources but tolerate some failing → Promise.allSettled. Knowing allSettled for partial success is a sharp, often-missed detail.

Q111. What does TypeScript add over JavaScript, and why is it worth it in React Native?

TypeScript adds static typing on top of JavaScript — types are checked at compile time, catching errors before runtime.

Why it's worth it in RN:

  • Catch bugs early — wrong prop types, missing fields, typos in navigation params, mismatched API shapes — caught while coding, not in production.
  • Better autocomplete and refactoring — the editor knows your shapes, so navigation params, component props, and store state are autocompleted and safely renamable.
  • Self-documenting — types describe component props, hook returns, and API responses.
  • Safer state/navigation — typed route params and typed Redux/Zustand state prevent a whole class of runtime errors.

In 2026, TypeScript is the default for serious RN apps. Explaining concrete RN wins (typed props, navigation params, API shapes) — not just "it adds types" — is the strong answer.

Q112. What's the difference between interface and type in TypeScript, and how do generics help?

  • interface — describes the shape of an object; can be extended and merged (declaration merging); idiomatic for object/props shapes.
  • type (type alias) — more flexible: can represent unions, intersections, primitives, tuples, and mapped types, in addition to object shapes.

Practical guidance: use interface for object/component-prop shapes (extensible), and type for unions and complex compositions (e.g., a discriminated union for UI state). They overlap a lot; consistency matters more than the choice.

Generics let you write reusable, type-safe code parameterized by type — e.g., a useFetch<T>() hook that returns T, or a List<T> component. They preserve type information instead of resorting to any. Scenario: a typed API client get<T>(url): Promise<T> so callers get the right return type. Knowing interface vs type (unions!) and using generics for reusable typed utilities shows real TS depth.


Section 13: Advanced React Native Deep Dive — The Section Interviewers Spend the Most Time On (2026)

These are the questions that separate people who've shipped serious React Native apps from people who've only built tutorials. Expect a big chunk of a senior round here — the New Architecture (Fabric, TurboModules, JSI, Bridgeless), Hermes, Reanimated internals, Expo, and the modern data/state ecosystem.

Q113. Explain the full New Architecture: JSI, Fabric, TurboModules, Codegen, and Bridgeless mode.

The New Architecture replaces the old asynchronous, serialized Bridge with a faster, more direct system built on JSI:

  • JSI (JavaScript Interface) — a C++ layer letting JS hold direct references to native objects and call them synchronously, with no JSON serialization. The foundation everything else builds on.
  • TurboModules — native modules loaded lazily (only when first used → faster startup) and invoked directly via JSI, with sync calls possible.
  • Fabric — the new rendering system: a C++ core that creates/updates the native view tree more efficiently, enables concurrent React features, and improves interop and layout.
  • Codegen — generates type-safe native binding code from your TS specs for TurboModules/Fabric components.
  • Bridgeless mode — the culmination: the legacy bridge is fully removed; everything goes through JSI.

Why it matters: no serialization bottleneck, synchronous calls, lazy loading, smoother UI, and concurrent rendering. In 2026 the New Architecture (with Bridgeless) is the default direction. Walking through all five pieces and why each exists is one of the most impressive senior answers.

Q114. What concrete problems did the old Bridge have that the New Architecture solves?

The old Bridge was asynchronous, batched, and JSON-serialized — every JS↔native message was converted to a string and back.

Concrete problems:

  • Serialization overhead — large/frequent data (big lists, fast gestures, image data) was slow to ferry across.
  • No synchronous calls — you couldn't read a native value inline; everything was message-passing, complicating some interactions.
  • A single bottleneck — heavy traffic caused lag and dropped frames.
  • Startup cost — all native modules initialized eagerly.

The New Architecture fixes each: JSI removes serialization and enables synchronous access; TurboModules load lazily; Fabric renders more efficiently and supports concurrent features. So you get faster startup, smoother gestures/lists, and lower overhead. Tying each old problem to its New Architecture fix is exactly the depth interviewers probe.

Q115. How does Hermes work, and how does it interact with the New Architecture and debugging?

Hermes precompiles your JavaScript to bytecode at build time (ahead-of-time), so on launch the engine loads bytecode instead of parsing/compiling JS — yielding faster startup, lower memory, and a smaller footprint, especially on low-end devices.

Interaction with the New Architecture: Hermes is the default engine and works tightly with JSI (the New Architecture's foundation), and it supports modern JS features and the concurrent capabilities the new renderer enables.

Debugging: historically you debugged RN by running JS in Chrome (a different runtime — misleading for performance). Hermes supports direct debugging (via the Hermes debugger / modern dev tools) so you debug the actual engine your app uses.

Senior point: "Hermes = AOT bytecode → faster startup + less memory, and debug the real engine, not Chrome." Mentioning the Chrome-debugging caveat shows real experience.

Q116. Deep-dive: how does Reanimated keep animations on the UI thread, and what are shared values and runOnJS?

Reanimated runs animation/gesture logic in worklets — JS functions executed on a separate JS runtime on the UI thread. Because they run there, they're independent of the (possibly busy) main JS thread, so animations stay smooth under load.

Key pieces:

  • Shared values (useSharedValue) — values readable/writable from both the JS thread and the UI thread; the animation reads them every frame on the UI thread.
  • useAnimatedStyle — a worklet that maps shared values to styles, recomputed on the UI thread each frame.
  • runOnJS — call back into the main JS thread from a worklet when you need to (e.g., update React state or navigate after an animation/gesture).
  • runOnUI — schedule a worklet to run on the UI thread.

Why it matters: this architecture is what delivers native-quality, gesture-driven, interruptible animations. Explaining worklets + shared values + runOnJS is a strong, current, senior-level answer.

Q117. What is the difference between Expo (managed/bare), the dev client, and EAS — and when do you use bare React Native?

  • Expo managed workflow — you build with Expo's SDK and tooling without touching native iOS/Android projects. Fast setup, OTA updates, easy builds; historically limited if you needed custom native code.
  • Expo Dev Client — a custom development build that lets you use custom native modules while keeping Expo's workflow (this largely erased the old "managed can't do native" limitation).
  • EAS (Expo Application Services) — cloud build & submit pipeline (EAS Build/Submit/Update) for compiling apps and shipping OTA updates without managing native build infra.
  • Bare React Native — full access to native projects from the start; choose it when you need deep native control from day one or are integrating RN into an existing native app.

2026 reality: Expo (with the dev client + EAS, and Continuous Native Generation via prebuild + config plugins) is now recommended even for apps that need native modules — the old "Expo vs bare" hard line has blurred. Knowing the dev client + EAS + config plugins story signals you're current.

Q118. What is the difference between client state and server state, and which tools own each?

  • Client state — UI/app state your app fully owns and controls: modal open/closed, form input, selected tab, theme. Synchronous, lives only in the app.
  • Server state — data living on a server: fetched async, cached client-side, can be stale, can change outside your app. Examples: a product list, profile data.

Tools:

  • Server state → React Query (TanStack Query) or RTK Query — caching, background refetch, staleness, dedup, invalidation.
  • Client state → Zustand / Redux Toolkit / Jotai / Context — pure UI/app state with selectors for fine-grained re-renders.

Why it matters: jamming server state into Redux/useState means hand-writing caching/refetch/invalidation — error-prone. Separating the two is the single most impactful modern RN architecture decision. Articulating this split (and naming the tools) is a strong senior answer for 2026.

Q119. How does React Query's caching, staleness, and invalidation actually work?

React Query caches each query by its query key. Key concepts:

  • staleTime — how long fetched data is considered fresh; while fresh, React Query won't refetch (serves from cache). After it, data is stale and eligible for background refetch (on mount, refocus, reconnect).
  • gcTime (cacheTime) — how long unused (no observers) cached data stays before garbage collection.
  • Background refetch — stale data is shown instantly while a fresh fetch happens behind the scenes (no spinner flash).
  • Invalidation — after a mutation (e.g., creating an item), you call queryClient.invalidateQueries(['items']) to mark related queries stale so they refetch — keeping UI consistent with the server.

Why it matters: this gives instant, always-fresh-feeling data with minimal code. Explaining stale-while-revalidate (show cached, refetch in background) plus invalidate-after-mutation demonstrates real React Query depth.

Q120. How do you handle optimistic updates, and what's the rollback strategy?

An optimistic update updates the UI immediately as if a mutation succeeded — before the server confirms — so the app feels instant (e.g., a "like" fills in right away).

With React Query's useMutation:

  • onMutate — cancel in-flight queries, snapshot the current cache, and optimistically update the cache to the new value.
  • If the mutation fails, onError — roll back to the snapshot.
  • onSettled — invalidate/refetch to reconcile with the true server state.

Why the snapshot/rollback matters: if the server rejects (network error, validation), you must revert the optimistic change so the UI doesn't show a lie. Explaining the snapshot → optimistic update → rollback-on-error → reconcile flow is a strong, frequently-asked senior pattern.

Q121. What are concurrent features in React 18 (Suspense, transitions) and how do they apply to React Native?

React 18 (which modern RN uses, especially with Fabric) added concurrent rendering — React can interrupt, pause, and prioritize rendering work instead of doing it all synchronously:

  • startTransition/useTransition — mark non-urgent updates (e.g., filtering a big list) as transitions so urgent updates (typing) stay responsive while the heavy update renders in the background.
  • useDeferredValue — defer a value so expensive re-renders driven by it don't block urgent input.
  • Suspense — declaratively show fallbacks while components/data load (used with data libraries and lazy components).

In RN: the New Architecture (Fabric) is what unlocks concurrent rendering on native. Practically, transitions/useDeferredValue help keep typing/scrolling smooth during heavy updates. Knowing these exist and that Fabric enables them in RN is a current, senior-level point.

Q122. What is useRef really for, and how is it different from state?

useRef gives you a mutable container (ref.current) that persists across renders but does not trigger a re-render when changed — the opposite of state.

Two main uses:

  • Referencing components/native elements — e.g., a TextInput ref to call .focus().
  • Holding mutable values that shouldn't cause re-renders — timer ids, previous values, a "latest value" to escape stale closures, a flag like isMounted.

vs state: changing state re-renders the UI (use it for values the UI displays); changing a ref doesn't (use it for values you need to remember but don't render).

Classic interview point: putting something in a ref to avoid a stale closure (e.g., always having the latest callback in an interval) — vs using state which would re-render. Knowing "refs persist without re-rendering" and the focus/stale-closure use cases is expected.

Q123. How do useMemo/useCallback interact with React.memo, and when is memoization pointless?

React.memo skips re-rendering a component when its props are shallow-equal to last time. But if you pass new object/array/function instances every render, the shallow compare always sees "different" and React.memo does nothing. So you stabilize those props with useMemo (objects/arrays/computed values) and useCallback (functions).

When memoization is pointless (or harmful):

  • The component is cheap to render — memo overhead exceeds the savings.
  • Props always change anyway — memo never skips.
  • You memoize a value/function that isn't passed to a memoized child or used in deps — pure overhead.

Senior point: memoization isn't free (it has comparison/allocation cost), so apply it where profiling shows real wasted renders, not everywhere. "Measure, then memoize the right boundaries" is the mature answer, and naming when memo does nothing impresses.

Q124. What is the difference between FlatList's windowSize, maxToRenderPerBatch, initialNumToRender, and removeClippedSubviews?

These tune FlatList virtualization:

  • initialNumToRender — how many items to render in the first batch (keep small for fast first paint).
  • maxToRenderPerBatch — how many items render per batch as you scroll (higher fills faster but can jank; lower is smoother but may show blanks).
  • windowSize — how much content (in screens) to keep rendered around the viewport (e.g., 21 ≈ 10 screens above + current + 10 below). Larger = fewer blanks but more memory; smaller = less memory but possible blank flashes.
  • removeClippedSubviews — detaches off-screen views from the native hierarchy to save memory (helps on Android; use with care as it can cause glitches).

Plus getItemLayout — if rows are fixed height, lets FlatList skip measurement and jump instantly.

Senior point: these are trade-offs (smoothness vs memory vs blank cells) you tune by profiling for your data; there's no universal setting. Knowing what each does — and that they're trade-offs — is the depth interviewers want.

Q125. How do you implement a smooth, gesture-driven interaction (e.g., a draggable bottom sheet) at 60fps?

Combine react-native-gesture-handler (gesture recognition on the UI thread) with Reanimated (animation on the UI thread via worklets):

  • A PanGestureHandler/Gesture.Pan() updates a shared value (the sheet's position) directly on the UI thread as the finger moves — no JS-thread round-trip.
  • useAnimatedStyle maps that shared value to the sheet's transform, recomputed every frame on the UI thread.
  • On release, run a spring/decay animation (also on the UI thread) to snap to the nearest position based on velocity.
  • Use runOnJS only when you must touch React state (e.g., mark the sheet "open" for the rest of the app).

Why this is smooth: the entire drag→animate→snap loop runs on the UI thread, so it stays at 60/120fps even if the JS thread is busy. Explaining "gesture-handler + Reanimated shared values, all on the UI thread, runOnJS only when needed" is the senior-level answer.

Q126. What is react-native-mmkv/JSI-based storage, and why is synchronous storage a big deal?

MMKV (and other JSI-based storage) provides synchronous reads/writes via JSI — no bridge serialization, no await.

Why synchronous is a big deal:

  • Simpler code — you read a value inline (storage.getString('theme')) without await/promises/loading states, which is huge for values needed during render or at startup (theme, auth presence, feature flags).
  • Faster — JSI + memory-mapped storage is dramatically quicker than AsyncStorage's async, bridge-backed reads.
  • Better startup — you can read settings synchronously before the first render without an async flash.

Contrast: AsyncStorage is async (bridge-backed historically), forcing you to await and often render a placeholder first. The shift to synchronous JSI storage is a concrete benefit of the JSI era. Explaining why sync matters (render-time access, no async flash) is the strong, current point.

Q127. How do you do code-splitting / lazy loading in React Native to improve startup?

Loading everything at startup bloats the initial work. Techniques:

  • Inline requires (RAM bundle / inlineRequires) — modules are required lazily on first use instead of all at boot, so startup parses/initializes far less. (Often enabled by default with Hermes/Metro config.)
  • React.lazy + Suspense — lazily load heavy screens/components only when navigated to.
  • Lazy-init heavy libraries — don't initialize SDKs at module top level; init on first use or after first paint.
  • Defer non-critical work with InteractionManager until after the first interactions.

Senior point: the biggest startup wins come from Hermes (bytecode) + inline requires (lazy module init), plus deferring heavy screens with React.lazy. Knowing inline requires specifically — and that it's about lazy module initialization — is a sharp, often-missed performance answer.

Q128. What is hydration/state persistence (redux-persist, React Query persistence), and what's the gotcha?

Persistence saves state to storage and restores (hydrates) it on next launch so the app remembers where it was:

  • redux-persist — serializes (parts of) the Redux store to storage and rehydrates on startup.
  • React Query persistence — persists the query cache so cached data is available instantly on relaunch (offline-friendly).

The gotcha: rehydration is asynchronous, so on launch there's a moment before persisted state loads. If you render UI before hydration completes, you can flash the wrong/empty state (e.g., showing logged-out before the persisted token loads). The fix is to gate rendering until hydration finishes (redux-persist's PersistGate, or a loading state) — and don't persist everything (only what should survive; persisting transient/server state can cause stale-data bugs). Knowing the async-hydration flash gotcha and the gate is the senior-level detail.

Q129. Explain how navigation state persistence works and how to restore deep navigation after process death.

React Navigation lets you persist the navigation state: you save the current nav state (the screen/tab structure) to storage on change, and on launch restore it via initialState so the user lands back where they were after the OS killed the app.

Flow:

  • Save nav state on onStateChange to storage.
  • On launch, read the saved state, pass it as initialState, and render the navigator once it's ready.
  • Combine with persisted data so the restored screens have their data.

Gotchas: you typically enable this only in development or guard it carefully (you may not want to restore deep state in all cases), handle the async read before rendering the navigator, and distinguish system-killed (restore) from user-killed (fresh start). Explaining the save-on-change/restore-via-initialState flow and the system-vs-user-kill distinction is the senior answer.

Q130. When and why would you write native code, and what's the modern (TurboModule/Fabric) way to do it?

When: to access a native SDK/feature with no JS package, reuse existing native code, or run heavy native computation off the JS thread.

The modern way (New Architecture):

  • For logic/APIs → write a TurboModule: define a TypeScript spec, run Codegen to generate the type-safe native bindings, implement it in Kotlin/Swift/C++, and it's called directly via JSI (lazy-loaded, can be synchronous).
  • For rendering a native view → write a Fabric native component (also spec + Codegen) that integrates with the new renderer and Yoga layout.

vs the old way: old bridge-based modules were async, serialized, and eagerly loaded. Senior point: check for an existing community package first; if you must go native, TurboModules/Fabric + Codegen are the current, type-safe, performant approach. Knowing the spec→Codegen→implementation flow signals you understand the modern interop story.

Q131. What is react-native-fast-image/proper image handling, and what problems does it solve?

Default <Image> has limited caching control and can struggle with heavy image lists. react-native-fast-image (a native-backed image library) provides:

  • Aggressive native caching (memory + disk) and proper cache control.
  • Cancellation/replacement of stale loads (helps the "wrong image in recycled row" bug).
  • Priority control and better performance for image-heavy lists.

Problems it solves: memory bloat from re-downloading/re-decoding, flicker, and wrong-image flashes while scrolling. Combined with correctly sized images (request the right resolution from the server/CDN rather than downscaling huge images on device) and virtualized lists, it keeps image feeds smooth and memory-bounded. Knowing the caching + correct-sizing combination is the practical senior answer.

Q132. How do you debug a "white screen" or crash that only happens in release builds, not in dev?

Release-only issues are common and tricky because dev and release differ (minification, Hermes bytecode, no dev warnings, different error visibility).

Diagnose:

  • Crash/error reporting — integrate Sentry/Crashlytics to capture release crashes with source maps so stack traces map back to your code.
  • Check native logsadb logcat (Android) or Xcode device logs (iOS) for native crashes the JS layer hides.
  • Common causes: code that relied on a dev-only path, a missing env/config in release, a library not configured for Hermes/New Architecture, or an error that's silently caught in dev but fatal in release.
  • Reproduce in a local release build (--release) rather than store builds for faster iteration.

Senior point: "Sentry with uploaded source maps + native logs" is the workflow; relying on dev-mode behavior to judge release is the trap. Knowing release/dev differences and the source-map step is a strong, experience-revealing answer.

Q133. What is react-native-reanimated's useAnimatedStyle vs the old Animated.Value approach, and why migrate?

  • Old Animated API — you create Animated.Values and interpolate them; without useNativeDriver the animation runs on the JS thread, and the native driver only supports transform/opacity.
  • Reanimated useAnimatedStyle — a worklet that maps shared values to styles, recomputed on the UI thread every frame, supporting any property and complex, gesture-driven, interruptible animations independent of the JS thread.

Why migrate: Reanimated gives native-thread smoothness for everything (not just transform/opacity), far better gesture integration, and more expressive animations. For modern, polished interactions (shared transitions, draggable sheets, scroll-driven effects), Reanimated is the standard in 2026. Explaining the UI-thread worklet vs JS-thread Animated distinction — and the native-driver property limitation that Reanimated removes — is the senior-level answer.

Q134. Your FlatList re-renders all rows when one item changes. Why, and how do you fix it?

This happens because something makes every row's props change or the whole list re-renders:

  • renderItem or the row's callbacks are recreated every render (new function identity) → rows aren't React.memo-skippable.
  • The row component isn't memoized, so a parent re-render re-renders all rows.
  • extraData/data references change in a way that invalidates everything.

Fixes:

  • Wrap the row component in React.memo, and ensure its props (including callbacks) are stable via useCallback.
  • Define renderItem and keyExtractor as stable references (useCallback).
  • Pass only the minimal, stable props each row needs; avoid new inline objects.
  • Use a selector so the list subscribes only to the data it renders.

Senior point: the goal is "only the changed row re-renders." Diagnosing it with the React DevTools Profiler and fixing prop identity is exactly what's tested.

Q135. Your screen feels sluggish from too many re-renders. Walk me through diagnosing and fixing it.

Diagnose:

  • Open the React DevTools Profiler, record an interaction, and see which components render and how often — find ones rendering that shouldn't.
  • Use why-did-you-render to log the exact prop/state change causing each re-render.
  • Check the Perf Monitor to see if it's JS-thread bound (re-renders/logic) vs UI-thread bound (native/animation).

Fix:

  • Narrow state — split Context, use store selectors, keep fast-changing state local.
  • Stabilize propsuseCallback/useMemo, and React.memo on expensive children.
  • Move heavy compute off render (memoize, or off the JS thread).
  • For lists, memoize rows and stabilize renderItem (Q134).

Saying "profile with React DevTools/why-did-you-render first, then narrow re-render scope and stabilize props" — rather than memoizing blindly — is the answer that lands.

Q136. What changed in the React Native ecosystem recently that affects how you build apps in 2026?

A few major shifts worth naming:

  • The New Architecture (JSI, Fabric, TurboModules, Bridgeless) is now the default direction — removing the old serialized bridge for faster, synchronous, concurrent-capable apps.
  • Hermes is the default engine — AOT bytecode for faster startup and lower memory.
  • React 18 concurrent features (transitions, Suspense) are unlocked on native via Fabric.
  • The data/state stack matured: React Query/RTK Query for server state, Zustand/Redux Toolkit for client state, with a clear client-vs-server-state split.
  • Expo became the recommended way to build (even with native code) via the dev client, EAS, and config plugins / Continuous Native Generation.
  • Reanimated 3 + Gesture Handler for UI-thread animations, and FlashList for high-perf lists.

Tying these together — "modern RN is New Architecture + Hermes + Expo/EAS + React Query + Zustand/RTK + Reanimated/FlashList, with TypeScript everywhere" — signals you're genuinely current, not quoting a 2019 tutorial.

Q137. How do you test a component that uses navigation, React Query, and a store — injecting all the fakes?

The key is providing fake/controlled dependencies so the component renders deterministically:

  • Navigation — wrap in a NavigationContainer (or mock the navigation prop with a fake navigate/route), so navigation calls are observable without a real navigator.
  • React Query — wrap in a QueryClientProvider with a fresh QueryClient per test, and mock the fetcher (or use MSW to intercept the network) to drive loading/success/error.
  • Store (Zustand/Redux) — provide a test store with known initial state (Redux Provider with a test store; for Zustand, reset/seed the store before each test).

Then use React Native Testing Library to query by text/role and waitFor/findBy for async updates, asserting the UI per state. The senior point: well-architected RN is testable because dependencies are injected via providers/hooks — connecting testability back to architecture is the strong close.

Q138. A teammate says "React Native is slow / not good for serious apps." How do you respond?

Calmly and with nuance. Early RN had real friction (the bridge bottleneck, shader/startup costs, list performance), and a naively built app can jank. But in 2026:

  • The New Architecture (JSI/Fabric/Bridgeless) removed the bridge bottleneck and enables synchronous, concurrent-capable, smoother apps.
  • Hermes gives fast startup and low memory; Reanimated + Gesture Handler deliver native-thread, 60/120fps interactions; FlashList handles heavy lists.
  • Major apps ship on RN (and many large companies use it in production), with strong architecture (TypeScript, React Query, clean data layers).
  • For truly native needs, TurboModules/Fabric bridge to native cleanly.
  • Most "RN is slow" complaints trace to specific anti-patterns (non-virtualized lists, JS-thread animations, broad re-renders, profiling in dev mode) — all fixable.

The senior move is to reframe: it's rarely "RN can't," it's "is it on the New Architecture, architected well, animated on the UI thread, and measured in a release build?" — and to argue from profiling data, not vibes.


Final Tips: How to Actually Pass the Interview (Not Just Memorize)

Knowing answers is only half the game. Here's how to use this knowledge in the room:

  • Always explain the "why," not just the "what." Anyone can say "use FlatList." Saying why (virtualization vs rendering everything) is what separates senior from junior.
  • Think out loud. Given a scenario, narrate your reasoning: "First I'd check if it's JS-thread or UI-thread jank in the Perf Monitor… then look at re-renders in the Profiler… then memoize the right boundaries." Interviewers hire your thought process, not a memorized line.
  • Mention trade-offs. "I'd use Zustand here, but for a large team needing strict structure and time-travel debugging, Redux Toolkit earns its boilerplate." Trade-off awareness is the #1 senior signal.
  • Admit limits honestly. "I haven't shipped a custom TurboModule in production, but here's how I'd approach it." Honesty beats bluffing — interviewers can smell a bluff instantly.
  • Connect to real impact. Tie answers back to users (smooth scrolling, fast startup, no data loss) and the team (testable, maintainable code). That shows you build products, not just code.
  • Practice the scenario format. Re-read each question here as if a person is asking you across the table. Say the answer out loud. The gap between "I know this" and "I can explain this clearly under pressure" is closed only by speaking it.

Summary

This guide covered 138 real-world React Native interview questions — from component and lifecycle basics every junior must know, through JS async and threading, state management, the JSI/New Architecture, performance and Hermes, security, testing, JavaScript/TypeScript deep-dives, and the advanced ecosystem (React Query, Zustand/RTK, Reanimated, Expo, FlashList) that today's senior interviews lean on heavily.

But don't treat it as a script to memorize. Treat it as a map of how React Native actually fits together. Once you understand why each piece exists and what problem it solves, you can answer questions you've never even seen — because you'll be reasoning from understanding, not recalling from memory.

That's the difference between someone who passes an interview and someone who deserves the role.

Now close this tab, open your editor, and go build something. Then come back, read it again, and watch how much more it means once you've felt these problems yourself.

You've got this.


If this guide helped you, save it, share it with a friend who's job-hunting, and bookmark it for the night before your interview. Good luck — go get that offer.

Top comments (0)