Back to interview questions

100+ React Native Interview Questions and Answers

100+ React Native Interview Questions and Answers

Explore 120 React Native interview questions and answers on native UI, Hooks, lists, navigation, architecture, performance, testing, security, and releases.

Explore 120 React Native interview questions and answers on native UI, Hooks, lists, navigation, architecture, performance, testing, security, and releases.

By

Team Sherlock.sh

Add Sherlock as a preferred source on Google

React Native hiring requires more than React knowledge transplanted from a browser. Recruiters need to distinguish developers who can assemble a screen from engineers who understand native interaction, platform differences, device constraints, and the path from a JavaScript change to a reliable iOS or Android release. Practical discussions about a slow list, a permission request, or a failed release reveal that difference.

This guide to 100+ React Native Interview Questions and Answers contains 120 questions across beginner, intermediate, and experienced levels. It covers native components, layout, state, navigation, networking, storage, native integration, architecture, performance, accessibility, testing, and delivery. React Native core, React concepts, Expo services, and community libraries are identified separately rather than treated as interchangeable capabilities.

How can recruiters detect potential AI cheating, deepfakes, impersonation, and unauthorized assistance during React Native interviews?

A candidate may explain Hooks fluently yet struggle to diagnose a dropped frame, a keyboard overlap, or an update that requires a new native binary. Ask for reasoning tied to a concrete React Native screen and change one device, lifecycle, or release constraint to see whether the explanation remains consistent.

Sherlock AI helps recruiters detect potential AI-assisted cheating, deepfakes, candidate impersonation, and unauthorized real-time assistance during React Native interviews.

Combine integrity signals with a short code review or debugging scenario, and ask the candidate to explain what must be tested on a real device. Review signals in context rather than treating any single indicator as proof of misconduct.

React Native Interview Questions

Choose questions that match the role, supported platforms, and app architecture. State the React Native version, Expo or native build workflow, navigation library, and relevant device constraints before discussing a solution.

React Native Interview Questions for Beginners

Establish the foundations of native components, state, layout, input, lists, networking, accessibility, and development workflows.

  1. What is React Native, and how does it differ from a mobile website?

  2. How does React Native differ from React for the web?

  3. What is Expo, and is it required to use React Native?

  4. What does Metro do in a React Native project?

  5. What are JSX and TypeScript used for in React Native?

  6. Which core components would you use to build a simple screen?

  7. Why must visible text normally be placed inside a Text component?

  8. What is the difference between props and state?

  9. How do you update state when the new value depends on the previous value?

  10. Why should React Native state updates be immutable?

  11. What are the basic rules for calling React Hooks?

  12. When should a component use useEffect?

  13. How do you handle a button press?

  14. How do you create and combine styles?

  15. How does React Native styling differ from browser CSS?

  16. What are the important React Native Flexbox defaults?

  17. How do justifyContent and alignItems differ?

  18. How do you make a screen respond to changing window dimensions?

  19. How do you display local and remote images?

  20. When should you choose ScrollView instead of FlatList?

  21. How do you render a basic FlatList?

  22. Why do list items need stable keys?

  23. When is SectionList more appropriate than FlatList?

  24. How do you create a controlled TextInput?

  25. How should a form handle validation and a disabled submit state?

  26. How do you keep form controls usable when the keyboard opens?

  27. How do you write small platform-specific variations?

  28. How should a screen handle safe-area insets?

  29. How do you make a custom pressable accessible?

  30. How do you support larger text and readable mobile layouts?

  31. How do you fetch JSON and handle an unsuccessful HTTP response?

  32. How do you represent loading, empty, success, and error states?

  33. How can you persist a non-sensitive preference?

  34. Where should authentication tokens and application secrets be stored?

  35. Does React Native provide screen navigation by itself?

  36. What is Fast Refresh, and when is a full reload needed?

  37. What is the difference between a development build and a release build?

  38. How would you investigate a blank screen or a basic rendering error?

  39. What should a simple React Native component test verify?

  40. Why must a shared React Native screen still be tested on iOS and Android?

React Native Interview Questions for Intermediate

Explore reliable state and effects, navigation lifecycles, reusable UI, data synchronization, native capabilities, and test boundaries.

  1. When would you use a reducer instead of several state variables?

  2. How do you prevent unnecessary updates when sharing state through context?

  3. When does a React Native app need an external state or server-state library?

  4. How do stale closures affect timers and subscriptions?

  5. How should an Effect clean up a native event subscription?

  6. How do you prevent an older network response from overwriting newer results?

  7. How would you implement reliable infinite scrolling?

  8. How should pull-to-refresh interact with cached data and pagination?

  9. How do FlatList virtualization settings affect responsiveness and memory?

  10. When is getItemLayout appropriate for FlatList?

  11. How do memo and stable callbacks help a list screen?

  12. How would you model a form with validation and submission states?

  13. How do you coordinate keyboard avoidance with a scrollable form and navigation insets?

  14. When would you use Animated and its native driver?

  15. How do you choose between PanResponder and a gesture library?

  16. What is the difference between screen focus and component mounting?

  17. How should a screen load data when it becomes focused?

  18. How do you pass data between navigation screens safely?

  19. How do you design authentication-aware navigation?

  20. How should an app handle an incoming deep link?

  21. How is Expo Router different from React Native itself?

  22. When should platform-specific files replace Platform checks?

  23. What should you verify before adopting a native dependency?

  24. How do you request and handle runtime permissions?

  25. How should a mobile app handle network failures and retries?

  26. How would you design an offline mutation queue?

  27. How do you choose between key-value storage, a database, and secure storage?

  28. How should access tokens and other secrets be handled?

  29. How do you manage accessibility focus for a modal or dynamically updated screen?

  30. How do you test dynamic text and screen-reader behavior?

  31. What belongs in a unit test versus a component test?

  32. How do you test asynchronous React Native UI behavior?

  33. How should native modules be mocked in tests?

  34. Which problems require end-to-end tests on a device or simulator?

  35. How do you prepare a React Native screen for localization and RTL?

  36. What can an error boundary recover from?

  37. How do you avoid state updates and resource leaks after a screen leaves?

  38. How would you reconcile optimistic UI with a failed mutation?

  39. How should a reusable custom Hook expose an asynchronous operation?

  40. How do you debug a bug that occurs only on one mobile platform?

React Native Interview Questions for Experienced

Use architecture and production scenarios to assess native integration, measured performance, release safety, security, and operational ownership.

  1. How do you choose a React Native version and plan a safe upgrade?

  2. What changes with the React Native New Architecture?

  3. How does Fabric render an interface?

  4. What are TurboModules, and when should you create one?

  5. What does JSI provide, and what does it not guarantee?

  6. How does Codegen improve a native integration?

  7. What is Hermes, and how do you assess its production impact?

  8. How do you reason about JavaScript, UI, and native work?

  9. How does concurrent React rendering affect native application design?

  10. How would you audit dependencies before an architecture migration?

  11. How would you integrate React Native into an existing native app?

  12. How do you diagnose a slow screen using production-like evidence?

  13. How do you distinguish JavaScript frame drops from UI frame drops?

  14. How do you improve cold-start performance?

  15. How do you investigate increasing memory usage?

  16. How would you optimize a large, image-heavy feed?

  17. How do you diagnose incorrect layout measurements and view flattening?

  18. How should animation and gesture architecture change under heavy load?

  19. How do you make production crashes actionable?

  20. What can an over-the-air update change safely?

  21. How do you define and enforce update runtime compatibility?

  22. How would you roll back a faulty over-the-air release?

  23. How do you protect authentication secrets on mobile devices?

  24. How do you coordinate concurrent token refreshes?

  25. How do you prevent duplicate writes when requests are retried?

  26. How would you design a reliable offline synchronization system?

  27. How do you handle deep links as a security boundary?

  28. How do you decide between JavaScript and native implementation?

  29. How do iOS and Android signing affect release ownership?

  30. How do you make native builds reproducible in CI?

  31. How do you roll out a mobile release safely?

  32. What belongs in a React Native production test strategy?

  33. How do you test native integrations and permission behavior?

  34. How do you scale accessibility across a mobile codebase?

  35. How would you design observability without leaking private data?

  36. How do you design resilient background work?

  37. How do you evolve persisted data safely across app versions?

  38. How do you keep environment configuration and credentials separate?

  39. How do you manage platform divergence without duplicating the app?

  40. How would you evaluate a proposed React Native performance rewrite?

React Native Interview Questions and Answers

The answers follow the same order as the question bank and lead with responses candidates can adapt directly. Examples illustrate the named behavior rather than complete applications. Native configuration, installed library versions, and device behavior must be verified in the actual project.

React Native Interview Questions and Answers for Beginners

These answers connect React fundamentals to native interfaces and highlight where browser assumptions do not apply.

1. What is React Native, and how does it differ from a mobile website?

React Native is a framework for building native applications using React and JavaScript or TypeScript. On iOS and Android, its host components integrate with native platform UI rather than rendering a website inside a browser. A View, for example, participates in the native view hierarchy; it is not an HTML div.

I can share substantial application logic and UI code across platforms while still adapting interactions and integrating native capabilities. That does not make every screen identical or eliminate platform work: permissions, signing, device behavior, and some components remain platform-specific. A mobile website instead runs within browser constraints. A WebView can be embedded in a React Native app, but it is an optional component, not the basic rendering model.

2. How does React Native differ from React for the web?

React supplies the component model, state, Hooks, and rendering concepts; React Native supplies a renderer and APIs for native applications. React for the web typically uses React DOM and elements such as div and input. In React Native I use components such as View and TextInput, with native event and styling conventions.

Business rules and custom Hooks can often be shared, but browser code using document, CSS selectors, or DOM events does not automatically work in a native app. Likewise, navigation, persistence, and device permissions need appropriate native or framework integrations. I treat web support through additional tooling as a separate target rather than assuming every React Native API behaves like its browser counterpart.

3. What is Expo, and is it required to use React Native?

Expo is a React Native framework and toolchain, not a replacement for React Native. It provides libraries, project conventions, development tooling, and integrations such as Expo Router. React Native can also be used without Expo, particularly when an existing native application or unusual native requirement calls for direct project control.

Expo Go is a prebuilt application with a fixed native library set; it is not equivalent to a custom development build. A development build can include the native modules and configuration that my own app requires. Expo Application Services are optional services, not a requirement for all Expo projects. I choose the workflow based on native dependencies, team experience, and release requirements rather than assuming Expo prevents native customization.

4. What does Metro do in a React Native project?

Metro is the JavaScript bundler used by React Native. It resolves the application's module graph, transforms supported source code, and packages JavaScript and associated assets for the runtime. During development it serves code and supports the update workflow; production builds package the appropriate bundle or engine-specific compiled output through their build integration.

Metro does not replace Gradle or Xcode: those tools build the native Android and iOS applications. It also does not make a missing native library appear in an already installed binary. When adding native code, I may need a new native build even if Metro successfully bundles the JavaScript. TypeScript transformation removes type syntax, so I still run a separate type-checking step.

5. What are JSX and TypeScript used for in React Native?

JSX is syntax for describing React elements inside JavaScript; TypeScript adds static type checking. In a React Native project I normally use .tsx for a TypeScript module containing JSX and import native components instead of writing HTML tags. Types help catch incorrect props and data shapes before running the app, but do not validate untrusted network data at runtime.

This example accepts a required string prop and renders native text. The tooling must still transform the file, and a separate type check should be part of the project checks.

TSX example:

import {Text} from 'react-native';

type GreetingProps = {name: string};

export function Greeting({name}: GreetingProps) {
  return <Text>Hello, {name}</Text>;
}

import {Text} from 'react-native';

type GreetingProps = {name: string};

export function Greeting({name}: GreetingProps) {
  return <Text>Hello, {name}</Text>;
}

import {Text} from 'react-native';

type GreetingProps = {name: string};

export function Greeting({name}: GreetingProps) {
  return <Text>Hello, {name}</Text>;
}

6. Which core components would you use to build a simple screen?

I start with View for layout, Text for visible text, Image for images, and TextInput for editable text. Pressable supports a custom interactive surface, while Button supplies a simpler platform-style action. For scrolling I choose between a bounded ScrollView and a virtualized list component according to the content.

I compose these primitives into application components such as a profile card rather than expecting a core component for every product feature. Core components do not provide an entire navigation or data-management system. A reusable component should expose meaningful props and preserve accessibility behavior, so wrapping a label and icon in a custom control does not accidentally remove its role or accessible name.

7. Why must visible text normally be placed inside a Text component?

Visible text nodes belong inside Text, not directly inside a View. Text provides native text layout, styling, selection-related behavior, and accessibility integration. A View is a layout container, so writing a bare string as its child is not the same as placing text inside an HTML element.

Nested Text components can style spans within a text block; nested views use the normal layout system instead. Text-style inheritance is constrained to text subtrees rather than a browser-style cascade across arbitrary parent views. I also avoid expressions such as count && <Text>...</Text> when count can be zero: use an explicit boolean condition to avoid accidentally producing a numeric text node in a non-text container.

8. What is the difference between props and state?

Props are inputs supplied by a parent, while state is a component's retained memory between renders. I treat both as immutable snapshots. A component reads its props and state to describe the next UI, and a state setter requests another render rather than mutating the current render's variables.

For example, a product card might receive a product through props and keep its expanded state locally. If multiple screens need the same value, I move ownership to an appropriate common owner or store instead of maintaining conflicting copies. I avoid copying a prop into state unless I deliberately need an independent initial value; otherwise later prop changes can leave that local copy stale.

9. How do you update state when the new value depends on the previous value?

I pass a functional updater when a state change depends on the previous value. React applies queued updater functions to the pending state, which avoids relying on a stale snapshot during multiple updates. The updater must be pure; I do not send requests or modify external data inside it.

This counter increments from the previous count. Calling the setter does not change count immediately within the current event handler, and reading it immediately afterward still reads that render's snapshot. If I need to calculate a separate value for the same action, I make that calculation explicit rather than assuming the variable has already changed.

TSX example:

import {useState} from 'react';
import {Button, Text, View} from 'react-native';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text>{count}</Text>
      <Button title="Add one" onPress={() => setCount(c => c + 1)} />
    </View>
  );
}

import {useState} from 'react';
import {Button, Text, View} from 'react-native';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text>{count}</Text>
      <Button title="Add one" onPress={() => setCount(c => c + 1)} />
    </View>
  );
}

import {useState} from 'react';
import {Button, Text, View} from 'react-native';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text>{count}</Text>
      <Button title="Add one" onPress={() => setCount(c => c + 1)} />
    </View>
  );
}

10. Why should React Native state updates be immutable?

Immutable updates create new objects or arrays for changed state instead of modifying values from an earlier render. React relies on identity in several update and optimization decisions, and mutation can both hide changes and corrupt snapshots that other code still references. A shallow copy is sufficient only when the changed data is at that level.

For example, I update one item by mapping the array and creating a new object for that item. Unchanged items can retain their existing references. I also keep list keys stable: changing an item's contents should not require inventing a new identity for it.

JavaScript example:

export function toggleDone(items, id) {
  return items.map(item =>
    item.id === id ? {...item, done: !item.done} : item,
  );
}

export function toggleDone(items, id) {
  return items.map(item =>
    item.id === id ? {...item, done: !item.done} : item,
  );
}

export function toggleDone(items, id) {
  return items.map(item =>
    item.id === id ? {...item, done: !item.done} : item,
  );
}

11. What are the basic rules for calling React Hooks?

I call ordinary Hooks such as useState and useEffect at the top level of a function component or custom Hook, before conditional returns. I do not call them in event handlers, loops, conditions, or ordinary utility functions. Keeping their order stable lets React associate each call with the correct state and lifecycle behavior.

If a subscription is conditional, I still call the Effect unconditionally and put the condition inside its setup. A custom Hook shares logic, not a single shared state instance: separate calls normally have separate state. I enable the React Hooks lint rules to catch ordering and dependency mistakes. React's special use API has different conditional-call rules; I do not generalize that exception to ordinary Hooks.

12. When should a component use useEffect?

I use an Effect to synchronize a component with something outside React, such as a timer, subscription, or imperative native API. Values that can be calculated from props and state usually belong in rendering, and work caused by a specific press usually belongs in its event handler.

An Effect must declare its reactive dependencies and clean up resources it creates. Cleanup runs before setup is repeated with changed dependencies and when the component unmounts. Development Strict Mode may perform an extra setup-and-cleanup cycle to expose unsafe assumptions. This timer uses a functional state update so its callback does not capture an outdated count.

TSX example:

import {useEffect, useState} from 'react';
import {Text} from 'react-native';

export function Seconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => setSeconds(value => value + 1), 1000);
    return () => clearInterval(timer);
  }, []);
  return <Text>{seconds} seconds</Text>;
}

import {useEffect, useState} from 'react';
import {Text} from 'react-native';

export function Seconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => setSeconds(value => value + 1), 1000);
    return () => clearInterval(timer);
  }, []);
  return <Text>{seconds} seconds</Text>;
}

import {useEffect, useState} from 'react';
import {Text} from 'react-native';

export function Seconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => setSeconds(value => value + 1), 1000);
    return () => clearInterval(timer);
  }, []);
  return <Text>{seconds} seconds</Text>;
}

13. How do you handle a button press?

I pass a function to a control's onPress prop. Passing onPress={save} lets React Native call the handler when the user activates the control; writing onPress={save()} would call it during rendering instead. A core Button is convenient for a basic action, while Pressable gives more control over presentation and press states.

I keep rendering free of side effects and put the action in the handler. For asynchronous submissions I also model pending and failure states, prevent accidental repeated actions, and expose disabled state accessibly. A custom visual treatment still needs a meaningful label and role; appearance alone does not communicate behavior to assistive technology.

TSX example:

import {Button} from 'react-native';

type SaveButtonProps = {onSave: () => void; pending: boolean};
export function SaveButton({onSave, pending}: SaveButtonProps) {
  return <Button title={pending ? 'Saving' : 'Save'}
    onPress={onSave} disabled={pending} />;
}

import {Button} from 'react-native';

type SaveButtonProps = {onSave: () => void; pending: boolean};
export function SaveButton({onSave, pending}: SaveButtonProps) {
  return <Button title={pending ? 'Saving' : 'Save'}
    onPress={onSave} disabled={pending} />;
}

import {Button} from 'react-native';

type SaveButtonProps = {onSave: () => void; pending: boolean};
export function SaveButton({onSave, pending}: SaveButtonProps) {
  return <Button title={pending ? 'Saving' : 'Save'}
    onPress={onSave} disabled={pending} />;
}

14. How do you create and combine styles?

I use JavaScript style objects, commonly organized with StyleSheet.create, and pass them to a component's style prop. Property names use camelCase, and supported properties depend on the component and platform. A style array combines reusable base styling with conditional overrides; later entries take precedence for the same property.

I avoid mutating shared style objects and keep layout decisions readable rather than scattering magic values through JSX. StyleSheet.create improves organization and typing, but it is not a guarantee that a screen is faster. Dynamic values can remain inline when they genuinely depend on current props or state.

TSX example:

import {StyleSheet, Text} from 'react-native';

export function Status({failed}: {failed: boolean}) {
  return <Text style={[styles.label, failed && styles.error]}>
    {failed ? 'Unable to save' : 'Saved'}
  </Text>;
}
const styles = StyleSheet.create({
  label: {fontSize: 16, color: '#1F2937'},
  error: {color: '#B91C1C'},
});

import {StyleSheet, Text} from 'react-native';

export function Status({failed}: {failed: boolean}) {
  return <Text style={[styles.label, failed && styles.error]}>
    {failed ? 'Unable to save' : 'Saved'}
  </Text>;
}
const styles = StyleSheet.create({
  label: {fontSize: 16, color: '#1F2937'},
  error: {color: '#B91C1C'},
});

import {StyleSheet, Text} from 'react-native';

export function Status({failed}: {failed: boolean}) {
  return <Text style={[styles.label, failed && styles.error]}>
    {failed ? 'Unable to save' : 'Saved'}
  </Text>;
}
const styles = StyleSheet.create({
  label: {fontSize: 16, color: '#1F2937'},
  error: {color: '#B91C1C'},
});

15. How does React Native styling differ from browser CSS?

React Native styles are component props interpreted by the native renderer, not a browser stylesheet with the full CSS cascade. I use camelCase properties such as backgroundColor; numeric layout values usually represent density-independent units rather than CSS pixel strings. Supported percentages and other value forms are property-specific.

I cannot assume that selectors, pseudo-classes, CSS inheritance, or every browser property work. Text styling can inherit within nested Text elements, but a surrounding View does not provide general CSS-style text inheritance. Layout uses Yoga with defaults that differ from browser Flexbox. I check the installed React Native version and target platforms before using newer style properties, since a web example may look familiar without having equivalent native behavior.

16. What are the important React Native Flexbox defaults?

The default main axis is vertical because flexDirection defaults to column. React Native also defaults flexShrink to 0 and alignContent to flex-start, unlike common browser defaults. Its flex shorthand accepts a number rather than the full CSS shorthand syntax.

These differences matter when translating a web layout: a row will not appear just because I gave its children widths, and overflowing children will not necessarily shrink automatically. I explicitly set the intended direction and shrinking behavior. flex: 1 is commonly used to let a container occupy available space, but ancestors still need meaningful dimensions. I diagnose the parent constraints before adding arbitrary heights to fix a child that appears missing.

17. How do justifyContent and alignItems differ?

justifyContent distributes children along the main axis, while alignItems aligns them on the cross axis. The axes depend on flexDirection: with the default column layout the main axis is vertical; with a row it is horizontal. Neither property changes the order or identity of the children.

For example, a row can place its children at opposite ends while vertically centering them. Cross-axis stretching depends on the child's size constraints, so an explicit width or height can change what I observe. I reason from the container's direction and available space rather than memorizing โ€œhorizontalโ€ and โ€œverticalโ€ as fixed meanings.

TSX example:

import {View, Text} from 'react-native';

export function Summary() {
  return <View style={{flexDirection: 'row',
    justifyContent: 'space-between', alignItems: 'center'}}>
    <Text>Subtotal</Text>
    <Text>$20</Text>
  </View>;
}

import {View, Text} from 'react-native';

export function Summary() {
  return <View style={{flexDirection: 'row',
    justifyContent: 'space-between', alignItems: 'center'}}>
    <Text>Subtotal</Text>
    <Text>$20</Text>
  </View>;
}

import {View, Text} from 'react-native';

export function Summary() {
  return <View style={{flexDirection: 'row',
    justifyContent: 'space-between', alignItems: 'center'}}>
    <Text>Subtotal</Text>
    <Text>$20</Text>
  </View>;
}

18. How do you make a screen respond to changing window dimensions?

I use useWindowDimensions when rendering depends on the available app window. It updates when the window dimensions or font scale change, which is more reliable than reading dimensions once at module initialization. Rotation, split-screen use, and foldable devices can change the space without restarting the app.

I prefer flexible layout first, then introduce breakpoints only when the content needs a different arrangement. The window is not always the same as a particular component's measured size; for a nested region I can use onLayout. I also account for safe-area insets rather than assuming the entire window is unobstructed.

TSX example:

import {Text, View, useWindowDimensions} from 'react-native';

export function AdaptivePanel() {
  const {width} = useWindowDimensions();
  return <View style={{flexDirection: width >= 700 ? 'row' : 'column'}}>
    <Text>Overview</Text>
    <Text>Details</Text>
  </View>;
}

import {Text, View, useWindowDimensions} from 'react-native';

export function AdaptivePanel() {
  const {width} = useWindowDimensions();
  return <View style={{flexDirection: width >= 700 ? 'row' : 'column'}}>
    <Text>Overview</Text>
    <Text>Details</Text>
  </View>;
}

import {Text, View, useWindowDimensions} from 'react-native';

export function AdaptivePanel() {
  const {width} = useWindowDimensions();
  return <View style={{flexDirection: width >= 700 ? 'row' : 'column'}}>
    <Text>Overview</Text>
    <Text>Details</Text>
  </View>;
}

19. How do you display local and remote images?

I provide a local bundled image through a statically resolvable require, or a remote image through a source object containing a URI. Local assets can supply intrinsic size information; remote images generally need explicit layout dimensions or a constrained aspect ratio. resizeMode controls how the image fits its box, not the size of that box.

I choose appropriately sized assets, handle loading or failures where they matter, and give meaningful images an accessible description. Decorative imagery should not add redundant screen-reader announcements. Network access must respect platform transport-security requirements; I do not disable those protections to make an insecure URL load. An image library may add caching features, but those are separate from guarantees made by core Image.

20. When should you choose ScrollView instead of FlatList?

I use ScrollView for a modest, bounded set of content that can reasonably be rendered together, such as a short settings page. It renders its children eagerly. For a large or unbounded collection I usually choose FlatList, which maintains a rendering window instead of mounting every row at once.

Virtualization improves scalability but introduces its own constraints: offscreen item state may not remain mounted, stable keys matter, and expensive rows can still cause poor performance. A ScrollView also needs bounded height through its layout hierarchy. I do not wrap a large vertical FlatList inside a vertical ScrollView just to add a title; the list's header and footer components usually express that structure more appropriately.

21. How do you render a basic FlatList?

I provide an array through data, a renderItem function that returns a row, and stable item keys. I keep the data immutable so changes can be detected. A basic list can also define an empty state without wrapping the whole list in another scrolling container.

This example uses a domain ID rather than the array index. When a row depends on state outside data, such as the currently selected ID, I ensure a relevant list prop changes, commonly through extraData. I avoid storing important business state only inside virtualized rows because they may unmount outside the render window.

TSX example:

import {FlatList, Text} from 'react-native';

type Person = {id: string; name: string};
export function People({people}: {people: Person[]}) {
  return <FlatList
    data={people}
    keyExtractor={person => person.id}
    renderItem={({item}) => <Text>{item.name}</Text>}
    ListEmptyComponent={<Text>No people found</Text>}
  />;
}

import {FlatList, Text} from 'react-native';

type Person = {id: string; name: string};
export function People({people}: {people: Person[]}) {
  return <FlatList
    data={people}
    keyExtractor={person => person.id}
    renderItem={({item}) => <Text>{item.name}</Text>}
    ListEmptyComponent={<Text>No people found</Text>}
  />;
}

import {FlatList, Text} from 'react-native';

type Person = {id: string; name: string};
export function People({people}: {people: Person[]}) {
  return <FlatList
    data={people}
    keyExtractor={person => person.id}
    renderItem={({item}) => <Text>{item.name}</Text>}
    ListEmptyComponent={<Text>No people found</Text>}
  />;
}

22. Why do list items need stable keys?

Keys give React a stable identity for sibling elements across updates. I use a persistent item ID so inserting, deleting, or reordering records does not make React confuse one row with another. The key is not a display label and must be unique among the relevant siblings.

An array index is risky when the order can change: local input state or another row-specific value can appear attached to the wrong record. A random key is worse because it changes on every render and forces remounting. Keys also do not become ordinary component props, so I pass an ID separately if a row needs it. Stable keys preserve identity; they do not eliminate rendering work or substitute for immutable data updates.

23. When is SectionList more appropriate than FlatList?

I choose SectionList when the data has meaningful groups, such as contacts by initial or transactions by date, and the interface needs section headers or footers. Each section contains a data array and can carry metadata such as a title. A flat collection without grouping is usually simpler with FlatList.

I provide stable item identities and stable section keys when sections can change. Like other virtualized lists, SectionList does not promise that offscreen rows stay mounted, so durable selection or edits belong in state outside individual rows. I also test sticky-header behavior on both platforms rather than assuming identical defaults. Grouping should express the product's information structure, not be introduced merely as a performance trick.

24. How do you create a controlled TextInput?

A controlled TextInput receives its current text through value and reports edits through onChangeText. React state is the source of truth, so I update that state when the callback receives the new string. I keep the value a string and use suitable keyboard and capitalization settings for the field.

A placeholder is only a hint; it disappears after typing and is not a substitute for a persistent visible label. I also avoid aggressive formatting that fights the user's cursor or composition input. This example shows the controlled-data flow, not a complete validated form.

TSX example:

import {useState} from 'react';
import {Text, TextInput, View} from 'react-native';

export function NameField() {
  const [name, setName] = useState('');
  return <View>
    <Text>Name</Text>
    <TextInput accessibilityLabel="Name" value={name}
      onChangeText={setName} autoCapitalize="words" />
  </View>;
}

import {useState} from 'react';
import {Text, TextInput, View} from 'react-native';

export function NameField() {
  const [name, setName] = useState('');
  return <View>
    <Text>Name</Text>
    <TextInput accessibilityLabel="Name" value={name}
      onChangeText={setName} autoCapitalize="words" />
  </View>;
}

import {useState} from 'react';
import {Text, TextInput, View} from 'react-native';

export function NameField() {
  const [name, setName] = useState('');
  return <View>
    <Text>Name</Text>
    <TextInput accessibilityLabel="Name" value={name}
      onChangeText={setName} autoCapitalize="words" />
  </View>;
}

25. How should a form handle validation and a disabled submit state?

I keep field values, validation feedback, and submission status explicit. Basic validation can be derived from the current values, while server rejection is handled separately after submission. I disable submission while required input is invalid or a request is pending, and I also guard the handler rather than relying only on a disabled-looking button.

Errors should explain how to recover and should remain understandable to screen-reader users. I avoid showing every field as invalid before the user has interacted unless the product calls for that behavior. Client-side validation improves feedback but does not replace server validation or authorization. For a failed request, I preserve the user's input and allow an intentional retry instead of silently clearing the form.

26. How do you keep form controls usable when the keyboard opens?

I make sure the focused field and the action needed to finish editing remain reachable when the keyboard appears. KeyboardAvoidingView can adjust height, position, or padding, while a scrollable form can let the user reach content that no longer fits. The correct behavior and offset depend on the screen layout and platform.

I test actual keyboard opening, dismissal, field focus changes, and submission rather than judging only a static screenshot. Navigation headers, safe areas, and Android window behavior can all affect the available space. I avoid hard-coding one keyboard height because keyboard type, orientation, language, and device configuration vary. A form that works with a hardware keyboard or one simulator size may still fail on a smaller phone.

27. How do you write small platform-specific variations?

I use Platform.OS or Platform.select for a small, understandable difference in a shared component. When platform behavior diverges substantially, .ios and .android files can keep each implementation focused while preserving a shared interface. Module resolution chooses the appropriate platform file.

I avoid spreading checks throughout the app because that makes behavior difficult to test and maintain. Platform-specific logic belongs near the capability it adapts, and I still test the shared contract. A platform name does not tell me every device capability or permission state, so those require separate checks.

TypeScript example:

import {Platform} from 'react-native';

export const platformLabel = Platform.select({
  ios: 'iOS settings',
  android: 'Android settings',
  default: 'Settings',
});

import {Platform} from 'react-native';

export const platformLabel = Platform.select({
  ios: 'iOS settings',
  android: 'Android settings',
  default: 'Settings',
});

import {Platform} from 'react-native';

export const platformLabel = Platform.select({
  ios: 'iOS settings',
  android: 'Android settings',
  default: 'Settings',
});

28. How should a screen handle safe-area insets?

I use safe-area insets to keep important content clear of system UI and physical obstructions such as a camera cutout. For new work, I normally use react-native-safe-area-context, following its provider setup and the navigation library's integration guidance. The old core SafeAreaView is deprecated.

Insets describe safe edges, not a universal spacing value to add everywhere. I decide which container owns each edge so a header or tab bar does not cause duplicate padding. Backgrounds may intentionally extend edge to edge while text and controls remain inset. I test rotation and different device shapes, and I handle the keyboard separately because safe-area padding alone does not guarantee that an input stays visible.

29. How do you make a custom pressable accessible?

I give a custom pressable a meaningful role, an understandable name, and state that matches its behavior. I also provide sufficient touch area, readable contrast, and visible feedback. A touch handler alone does not explain a control's purpose to a screen reader.

For a toggle, the announced state should track the actual value rather than only changing color. I verify the result with VoiceOver and TalkBack because grouping and announcements differ between platforms. This example uses a button role with selected state; a product may choose a different semantic control when that better matches its behavior.

TSX example:

import {Pressable, Text} from 'react-native';

type FavoriteProps = {selected: boolean; onToggle: () => void};
export function Favorite({selected, onToggle}: FavoriteProps) {
  return <Pressable accessibilityRole="button"
    accessibilityLabel="Favorite"
    accessibilityState={{selected}} onPress={onToggle}
    style={{padding: 16}}>
    <Text>{selected ? 'Remove favorite' : 'Add favorite'}</Text>
  </Pressable>;
}

import {Pressable, Text} from 'react-native';

type FavoriteProps = {selected: boolean; onToggle: () => void};
export function Favorite({selected, onToggle}: FavoriteProps) {
  return <Pressable accessibilityRole="button"
    accessibilityLabel="Favorite"
    accessibilityState={{selected}} onPress={onToggle}
    style={{padding: 16}}>
    <Text>{selected ? 'Remove favorite' : 'Add favorite'}</Text>
  </Pressable>;
}

import {Pressable, Text} from 'react-native';

type FavoriteProps = {selected: boolean; onToggle: () => void};
export function Favorite({selected, onToggle}: FavoriteProps) {
  return <Pressable accessibilityRole="button"
    accessibilityLabel="Favorite"
    accessibilityState={{selected}} onPress={onToggle}
    style={{padding: 16}}>
    <Text>{selected ? 'Remove favorite' : 'Add favorite'}</Text>
  </Pressable>;
}

30. How do you support larger text and readable mobile layouts?

I let text respond to the user's font-size preference and design containers that can grow or wrap. Fixed-height text boxes and narrow rows are common causes of clipping when text becomes larger. I test long labels, translated content, and the largest supported accessibility text sizes instead of assuming the default font scale represents everyone.

I use clear contrast, sensible line spacing, and layouts that do not communicate meaning through color alone. If text must be truncated, essential information should remain available through another usable path. Turning off font scaling globally hides layout defects at the user's expense. A screen can pass a text-rendering assertion yet still need device checks for readable wrapping, focus order, and reachable controls.

31. How do you fetch JSON and handle an unsuccessful HTTP response?

I await fetch, check the HTTP status, and then parse the body according to the endpoint's contract. A completed HTTP response with a failure status does not normally make fetch reject, so checking response.ok matters. Transport failures and malformed JSON also need handling by the caller.

I treat parsed data as untrusted until its structure is validated. TypeScript annotations do not validate a server response at runtime. This helper deliberately returns unknown; a caller can validate it and choose an appropriate user-facing error. A screen should additionally handle cancellation or stale responses when its request inputs change.

TypeScript example:

export async function fetchJson(
  url: string,
  signal?: AbortSignal,
): Promise<unknown> {
  const response = await fetch(url, {signal});
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return response.json();
}

export async function fetchJson(
  url: string,
  signal?: AbortSignal,
): Promise<unknown> {
  const response = await fetch(url, {signal});
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return response.json();
}

export async function fetchJson(
  url: string,
  signal?: AbortSignal,
): Promise<unknown> {
  const response = await fetch(url, {signal});
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return response.json();
}

32. How do you represent loading, empty, success, and error states?

I model the request state explicitly rather than inferring everything from an empty array. Before the first response, an empty collection may mean โ€œnot loadedโ€; after a successful response, it may mean โ€œno results.โ€ Failure needs a separate state with a clear recovery action.

For an initial load I can show progress, then render content or an empty-state explanation. During refresh I may keep already loaded content visible and show a less disruptive progress indicator. I also distinguish an error while refreshing usable cached data from an error that leaves the screen with nothing to display. This makes the UI honest about what it knows and avoids misleading empty states or spinners that never finish.

33. How can you persist a non-sensitive preference?

I use an appropriate persistence library rather than assuming React component state survives an app restart. For a small non-sensitive preference, a key-value store such as community AsyncStorage can be sufficient. AsyncStorage is not a current React Native core API and is not encrypted secret storage.

I handle read and write failures and provide a safe default for a missing or invalid value. Structured values need serialization and, as the app evolves, a migration strategy. Loading a stored preference is asynchronous, so the initial UI may need a loading decision to avoid a visible theme flash. I also avoid writing on every render; persistence follows a deliberate user action or a controlled synchronization path.

34. Where should authentication tokens and application secrets be stored?

I keep privileged application secrets on the server; putting them in a JavaScript bundle, environment variable compiled into the app, or native binary does not make them private. User authentication credentials that must persist on a device belong in platform-backed secure storage through an appropriate maintained library, not an ordinary unencrypted preference store.

Expo SecureStore is one option when Expo modules are configured, but it is not a core React Native API or a guarantee against a compromised device. I minimize what is stored, avoid logging credentials, clear local session material on sign-out, and design server-side expiry and revocation. I also handle missing or invalidated stored values rather than assuming they survive every reinstall, backup, or biometric change.

35. Does React Native provide screen navigation by itself?

React Native core does not provide a complete application navigation system. I normally use a library such as React Navigation, or Expo Router when the project uses its routing conventions. These tools build on React Native but have their own APIs, configuration, and version requirements.

A navigator manages transitions and navigation state, such as stacks and tabs, rather than merely toggling which component is visible. I choose a structure that matches the product and platform expectations, including back behavior and deep links. A screen becoming unfocused does not necessarily mean its component unmounted. That distinction matters for subscriptions and data loading, so I check the navigation library's lifecycle rather than assuming a browser page model.

36. What is Fast Refresh, and when is a full reload needed?

Fast Refresh updates development JavaScript after edits and often preserves function-component state when it is safe to do so. It shortens feedback time, but state preservation is not guaranteed. Some module structures cause a full reload, and certain edits reset state or rerun Effects even when their dependency values did not change.

I perform a clean reload when checking initialization behavior or investigating a bug that could be hidden by preserved development state. Adding or changing native code usually needs rebuilding the app, not just refreshing JavaScript. Fast Refresh is a development convenience, not an over-the-air production release system. A feature appearing to work after several refreshes is therefore not evidence that a fresh app launch works correctly.

37. What is the difference between a development build and a release build?

A development build is configured for iteration and debugging; a release build is configured for distribution and production behavior. Development tools and checks add overhead, and core debugging features are disabled in release builds. I use a production-like release configuration when evaluating performance or reproducing a release-only issue.

In Expo terminology, a development build is also a custom native app containing the project's native dependencies and development tooling, unlike the fixed native runtime supplied by Expo Go. Neither a successful JavaScript bundle nor a successful debug build proves that signing, production configuration, native optimization, or store packaging is correct. I validate the release artifact with the intended environment and protect signing credentials separately from ordinary app configuration.

38. How would you investigate a blank screen or a basic rendering error?

I first separate a JavaScript error, a layout issue, and an app-startup or native failure. I reproduce the smallest failing case, inspect the first relevant error and stack trace, and verify imports, exports, data assumptions, and the root component. A view with no available height can look blank even when rendering succeeded.

I use React Native DevTools for JavaScript and component inspection, and platform logs or native debugging tools when the failure is below that layer. I check whether the problem appears after a fresh launch, only on one platform, or only in a release build. Clearing caches is a targeted troubleshooting step when evidence points to stale tooling state, not a substitute for identifying the cause.

39. What should a simple React Native component test verify?

A component test should verify observable behavior: the user can find the control, activate it, and see the expected result or callback. I prefer assertions about meaningful text, accessible roles, and state changes over implementation details such as an internal Hook's value. For a submit button, I would cover enabled and disabled behavior as well as the callback.

A focused test should set up its inputs clearly and keep unrelated network or native dependencies controlled. Snapshot output can detect changes but does not explain whether an interaction still works. JavaScript component tests do not execute the real native view system, so they cannot prove keyboard layout, actual touch behavior, native permissions, or screen-reader announcements. Those need additional integration or device testing.

40. Why must a shared React Native screen still be tested on iOS and Android?

Shared source code reduces duplication, but iOS and Android still have different native implementations, system conventions, permissions, and lifecycle behavior. A screen can compile on both platforms yet fail because of keyboard handling, back navigation, safe-area assumptions, text metrics, or a native dependency's configuration.

I test the important user journeys on both platforms and include representative screen sizes, supported OS versions, and accessibility settings. Simulators and emulators are useful, but physical-device checks remain important for capabilities such as camera access, biometrics, performance, and real network transitions. I prioritize the matrix by product risk rather than expecting one device to represent all users. Shared logic tests complement this coverage; they do not replace it.

React Native Interview Questions and Answers for Intermediate

These answers explain implementation choices, common failure modes, and the limits of component-level tests or JavaScript-only fixes.

41. When would you use a reducer instead of several state variables?

I use a reducer when several state values change together according to named events. It makes transitions explicit and keeps rules such as โ€œa successful submission clears the pending errorโ€ in one pure function. Independent values, such as whether a help tooltip is open, usually remain ordinary state.

The reducer returns new state without network calls, storage writes, or other side effects. An event handler performs the operation and dispatches its outcome. I prefer a small discriminated action type over a generic action that can mutate any field, because invalid transitions become easier to spot and test. A reducer organizes state logic; it does not automatically share state across screens or prevent rendering.

TypeScript example:

type State = { count: number };
type Action = { type: 'increment' } | { type: 'reset' };
export function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return { count: 0 };
  }
}

type State = { count: number };
type Action = { type: 'increment' } | { type: 'reset' };
export function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return { count: 0 };
  }
}

type State = { count: number };
type Action = { type: 'increment' } | { type: 'reset' };
export function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return { count: 0 };
  }
}

42. How do you prevent unnecessary updates when sharing state through context?

I keep context values focused and stable, then measure whether consumers actually update too often. Splitting authentication, theme, and rapidly changing screen state avoids broadcasting unrelated changes to every consumer of one large context. State that only one screen needs should generally stay near that screen.

When a provider supplies an object containing functions, memoizing the functions and object can avoid changes caused only by newly allocated references. This does not stop updates when the underlying value genuinely changes. A memoized component still receives new context values that it reads. For very frequent, granular updates, a store with suitable subscriptions may be a better fit than multiplying context providers. I also avoid storing derived values independently when they can be calculated from the source state, because synchronization mistakes cost more than a small calculation.

43. When does a React Native app need an external state or server-state library?

I add an external state tool when the app has sharing, synchronization, or debugging needs that local state and context no longer handle clearly. Client state, such as an unsaved editor session, has different requirements from server state, such as a cached product list with freshness and retry rules.

A server-state library can manage cache keys, request deduplication, invalidation, and refetching. It does not determine the backendโ€™s authorization or business conflict rules. A client store can provide selective subscriptions and explicit update conventions, but every screen does not need to depend on it. I first identify ownership and lifetime, then choose a maintained library compatible with the project. On mobile, I also decide what survives process death, what is cleared on logout, and how foregrounding or connectivity changes affect stale data. React Native itself does not supply a complete server cache.

44. How do stale closures affect timers and subscriptions?

A callback captures the values from the render that created it. If a timer or subscription retains that callback, it can keep reading an old state value even after the component renders again. I fix this by choosing the appropriate dependency lifecycle, a functional state update, or an intentionally maintained ref for imperative reads, rather than suppressing dependency warnings.

A functional updater is enough when the next state depends only on previous state. If the callback also depends on a changing user or filter, that dependency needs separate treatment. Cleanup remains necessary: a correct calculation can still run repeatedly after the screen no longer needs it.

TSX example:

import { useEffect, useState } from 'react';
export function useElapsedSeconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return seconds;
}

import { useEffect, useState } from 'react';
export function useElapsedSeconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return seconds;
}

import { useEffect, useState } from 'react';
export function useElapsedSeconds() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return seconds;
}

This counts timer callbacks, not reliable wall-clock elapsed time while a mobile app is suspended.

45. How should an Effect clean up a native event subscription?

I retain the subscription returned by the native API and remove that exact subscription in the Effect cleanup. Cleanup runs before the Effect is set up again with changed dependencies and when the component unmounts. The setup and cleanup must remain safe under development Strict Modeโ€™s extra setup/cleanup cycle.

For example, AppState can tell a screen that the application became active, but this is separate from navigation focus. I only attach the listener for the lifetime that owns the work. A callback that starts an asynchronous operation also needs cancellation or result invalidation; removing the event listener does not cancel work it already started.

TSX example:

import { useEffect } from 'react';
import { AppState } from 'react-native';
export function useOnActive(onActive: () => void) {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', state => {
      if (state === 'active') onActive();
    });
    return () => subscription.remove();
  }, [onActive]);
}

import { useEffect } from 'react';
import { AppState } from 'react-native';
export function useOnActive(onActive: () => void) {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', state => {
      if (state === 'active') onActive();
    });
    return () => subscription.remove();
  }, [onActive]);
}

import { useEffect } from 'react';
import { AppState } from 'react-native';
export function useOnActive(onActive: () => void) {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', state => {
      if (state === 'active') onActive();
    });
    return () => subscription.remove();
  }, [onActive]);
}

The caller can memoize onActive when unnecessary resubscription matters.

46. How do you prevent an older network response from overwriting newer results?

I associate each response with the request that is still relevant and ignore results after that request loses ownership. Aborting a supported request saves work, but I also guard state updates because asynchronous steps may finish despite cancellation. Search terms, account changes, and navigation changes can all invalidate an earlier response.

This Hook uses cleanup to invalidate the previous URLโ€™s result. The caller must validate the returned data before relying on its shape; the example deliberately returns unknown.

TSX example:

import { useEffect, useState } from 'react';
export function useJson(url: string) {
  const [result, setResult] = useState<unknown>(null);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    let active = true;
    const controller = new AbortController();
    setResult(null);
    setError(null);
    (async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data: unknown = await response.json();
        if (active) setResult(data);
      } catch (e) {
        if (active) setError(e instanceof Error ? e.message : 'Request failed');
      }
    })();
    return () => { active = false; controller.abort(); };
  }, [url]);
  return { result, error };
}

import { useEffect, useState } from 'react';
export function useJson(url: string) {
  const [result, setResult] = useState<unknown>(null);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    let active = true;
    const controller = new AbortController();
    setResult(null);
    setError(null);
    (async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data: unknown = await response.json();
        if (active) setResult(data);
      } catch (e) {
        if (active) setError(e instanceof Error ? e.message : 'Request failed');
      }
    })();
    return () => { active = false; controller.abort(); };
  }, [url]);
  return { result, error };
}

import { useEffect, useState } from 'react';
export function useJson(url: string) {
  const [result, setResult] = useState<unknown>(null);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    let active = true;
    const controller = new AbortController();
    setResult(null);
    setError(null);
    (async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data: unknown = await response.json();
        if (active) setResult(data);
      } catch (e) {
        if (active) setError(e instanceof Error ? e.message : 'Request failed');
      }
    })();
    return () => { active = false; controller.abort(); };
  }, [url]);
  return { result, error };
}

A production data layer also defines loading, cache, timeout, and retry behavior rather than inferring all states from a null result.

47. How would you implement reliable infinite scrolling?

I treat the end-of-list callback as a request to load more, not proof that exactly one new page should be fetched. A request guard, a known next cursor, and an end-of-data flag determine whether loading is allowed. The serverโ€™s cursor is usually safer than deriving the next page from the current visible item count.

I scope the pagination session to its filter and account, deduplicate items by stable server identity, and reject responses from an obsolete session. A refresh invalidates the old pagination chain. Loading and failure indicators belong in the footer so existing content remains usable, and a failed page needs an explicit retry path. onEndReachedThreshold only controls when the callback is eligible; it does not supply concurrency control. I test rapid scrolling, repeated callbacks, empty pages, changed filters, and network responses arriving out of order.

48. How should pull-to-refresh interact with cached data and pagination?

I make refresh a separate operation from loading the next page. Refresh requests the latest first page, establishes a new pagination generation, and prevents an older page response from being appended afterward. Keeping existing cached content visible during that request usually provides a better experience than blanking the list.

The refresh indicator is controlled state: it stays true while the refresh is active and resets on either success or failure. After success, I replace or reconcile the list according to the productโ€™s ordering rules and use the new next cursor. After failure, I retain usable cached data and show that it may be stale. I distinguish โ€œrefresh failedโ€ from โ€œthere is no data.โ€ If the user changes accounts or filters, caches and refresh ownership must change with those keys rather than allowing one screenโ€™s response into another screenโ€™s data.

49. How do FlatList virtualization settings affect responsiveness and memory?

Virtualization reduces the amount of mounted content, but its configuration trades memory against responsiveness and the risk of blank areas. A larger window retains more items around the viewport. Larger render batches can fill the list faster while consuming more JavaScript time before interactions can be handled. I tune these settings against the actual row complexity and target devices.

I first remove expensive row work and use stable identities before changing several parameters at once. The initial render count should cover the intended viewport without unnecessarily mounting many rows. Detaching offscreen native views with removeClippedSubviews is not equivalent to freeing all item memory, and it can introduce visual problems with certain layouts. I compare release-build scrolling, memory, and interaction traces, including rapid flings and image loading. A small development dataset is not sufficient evidence that a feed will remain smooth.

50. When is getItemLayout appropriate for FlatList?

I use getItemLayout when an itemโ€™s length and position can be calculated accurately without measuring it. It is particularly useful for fixed-height rows and scrolling to a distant index. The offset must account for separators and any other content included in the actual geometry; guessing a height can make scrolling and virtualization incorrect.

This example assumes every row is exactly 64 layout units tall, separators are exactly 1 unit tall, and there is no list header or content inset affecting the offsets.

TypeScript example:

export const getItemLayout = (_data: unknown, index: number) => ({
  length: 64,
  offset: index * 65,
  index,
});

export const getItemLayout = (_data: unknown, index: number) => ({
  length: 64,
  offset: index * 65,
  index,
});

export const getItemLayout = (_data: unknown, index: number) => ({
  length: 64,
  offset: index * 65,
  index,
});

I would not apply this unchanged to wrapping text, variable-size images, expandable rows, or unrestricted text scaling. For those cases I use a layout strategy that matches the actual dimensions and handle failed programmatic scrolling rather than pretending the rows have fixed geometry.

51. How do memo and stable callbacks help a list screen?

memo can skip a row render when its props have not changed, and stable callbacks can keep otherwise identical props from changing solely because a new function was allocated. Neither mechanism fixes a mutable data model, stale closure, or expensive native layout. I profile first and optimize the part that contributes meaningful work.

I pass the row only the values it needs, preserve object identity for unchanged items, and include real dependencies in callbacks. If rendering depends on selection outside data, the list must receive that changing information through its props, commonly extraData. A custom equality function must compare behavior-affecting props, including functions, or it can preserve a stale handler. Memoization also has comparison and maintenance costs. A tiny inexpensive row may not benefit, especially when most of its meaningful props change on every update.

52. How would you model a form with validation and submission states?

I separate field values and validation messages from the submission lifecycle. Submission normally moves through idle, submitting, success, or failure, with a clear rule preventing duplicate attempts while one is active. Validation should run against the submitted values; the server remains authoritative even when the client has already checked them.

This type makes mutually exclusive submission states explicit:

TypeScript example:

export type Submission =
  | { status: 'idle' }
  | { status: 'submitting'; requestId: string }
  | { status: 'success'; recordId: string }
  | { status: 'failure'; message: string };

export type Submission =
  | { status: 'idle' }
  | { status: 'submitting'; requestId: string }
  | { status: 'success'; recordId: string }
  | { status: 'failure'; message: string };

export type Submission =
  | { status: 'idle' }
  | { status: 'submitting'; requestId: string }
  | { status: 'success'; recordId: string }
  | { status: 'failure'; message: string };

I preserve useful input after a failure, connect errors to their fields, and make status changes accessible. A disabled button improves interaction but does not guarantee exactly-once server processing; payments or other consequential writes need backend duplicate protection. If fields remain editable during submission, the response must be reconciled with the submitted snapshot rather than silently treating newer edits as saved.

53. How do you coordinate keyboard avoidance with a scrollable form and navigation insets?

I establish which layer owns each inset before adding keyboard avoidance. Navigation headers, safe-area padding, automatic scroll insets, and Android window resizing can otherwise compensate for the same space twice. The form needs a bounded, flexible viewport and enough scrollable space for the focused field and its error message.

KeyboardAvoidingView can adjust height, position, or padding, while a ScrollView handles content taller than the remaining viewport. Its keyboardShouldPersistTaps setting affects whether a tap reaches a button while the keyboard is open. keyboardVerticalOffset must reflect the actual surrounding geometry, not a copied constant. I test both platforms with large text, landscape, a bottom input, multiline fields, and different keyboard types. Avoidance does not universally guarantee that the desired field scrolls into view; complex forms may require explicit focus/scroll coordination or a compatible keyboard-aware library.

54. When would you use Animated and its native driver?

I use core Animated for declarative value-driven animations such as opacity or transforms. With supported properties, the native driver sends the animation description to native so frame updates do not depend on JavaScript processing every frame. Layout properties such as height are not a general native-driver option.

TSX example:

import { useEffect, useRef } from 'react';
import { Animated, Text } from 'react-native';
export function FadeInMessage() {
  const opacity = useRef(new Animated.Value(0)).current;
  useEffect(() => {
    const animation = Animated.timing(opacity, {
      toValue: 1, duration: 250, useNativeDriver: true,
    });
    animation.start();
    return () => animation.stop();
  }, [opacity]);
  return <Animated.View style={{ opacity }}><Text>Ready</Text></Animated.View>;
}

import { useEffect, useRef } from 'react';
import { Animated, Text } from 'react-native';
export function FadeInMessage() {
  const opacity = useRef(new Animated.Value(0)).current;
  useEffect(() => {
    const animation = Animated.timing(opacity, {
      toValue: 1, duration: 250, useNativeDriver: true,
    });
    animation.start();
    return () => animation.stop();
  }, [opacity]);
  return <Animated.View style={{ opacity }}><Text>Ready</Text></Animated.View>;
}

import { useEffect, useRef } from 'react';
import { Animated, Text } from 'react-native';
export function FadeInMessage() {
  const opacity = useRef(new Animated.Value(0)).current;
  useEffect(() => {
    const animation = Animated.timing(opacity, {
      toValue: 1, duration: 250, useNativeDriver: true,
    });
    animation.start();
    return () => animation.stop();
  }, [opacity]);
  return <Animated.View style={{ opacity }}><Text>Ready</Text></Animated.View>;
}

I keep a valueโ€™s driver choice consistent, stop work at the appropriate lifecycle boundary, and respect reduced-motion preferences. A native-driven opacity transition does not make expensive JavaScript calculations or native view layout free. Gesture-heavy interactions may benefit from a separate animation/gesture library, whose installation and threading model need their own review.

55. How do you choose between PanResponder and a gesture library?

I choose PanResponder for relatively simple responder-based interactions when its JavaScript event handling meets the requirements. For complex gesture competition, native recognizers, or gesture-linked animation under load, I evaluate a maintained gesture library and its integration with the chosen animation system.

The decision depends on nested scrolling, simultaneous gestures, cancellation, accessibility alternatives, and expected JavaScript load, not just how short the demo is. A drag inside a scroll view needs an explicit rule for when the drag takes ownership and when scrolling should continue. I test interrupted touches, multi-touch, navigation gestures, and both platforms. A library can move particular processing away from the main JavaScript runtime, but that is library-specific behavior, not an automatic property of every React Native gesture. I also provide a non-drag action when dragging is the only way to complete an essential task.

56. What is the difference between screen focus and component mounting?

Mounting means a component has entered Reactโ€™s tree; focus means its navigation screen is currently active according to the navigator. A stack screen can remain mounted while another screen covers it, so an Effect with an empty dependency array does not run again merely because the user returns.

I use mount lifetime for resources that belong to the mounted component and focus lifetime for work that is relevant only while that screen is active. App foreground/background state is a third, separate signal. For example, a camera preview may need both focus and foreground status before consuming resources. Exact retention behavior depends on the navigator and its options, so I avoid assuming that every blur unmounts a screen or that every navigation event triggers an Effect. Cleanup should follow the lifetime that actually owns the resource.

57. How should a screen load data when it becomes focused?

I make focus-driven loading an explicit navigation concern, usually through React Navigationโ€™s useFocusEffect, and decide whether cached data is fresh enough before refetching. The callback should be stable for unchanged dependencies, and its cleanup invalidates work when the screen blurs, unmounts, or its dependencies change.

This example delegates transport and error interpretation to an injected loader; the Hook only manages focus ownership:

TSX example:

import { useCallback, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
export function useFocusedCount(load: () => Promise<number>) {
  const [count, setCount] = useState<number | null>(null);
  const [failed, setFailed] = useState(false);
  useFocusEffect(useCallback(() => {
    let active = true;
    setFailed(false);
    load().then(
      value => { if (active) setCount(value); },
      () => { if (active) setFailed(true); },
    );
    return () => { active = false; };
  }, [load]));
  return { count, failed };
}

import { useCallback, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
export function useFocusedCount(load: () => Promise<number>) {
  const [count, setCount] = useState<number | null>(null);
  const [failed, setFailed] = useState(false);
  useFocusEffect(useCallback(() => {
    let active = true;
    setFailed(false);
    load().then(
      value => { if (active) setCount(value); },
      () => { if (active) setFailed(true); },
    );
    return () => { active = false; };
  }, [load]));
  return { count, failed };
}

import { useCallback, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
export function useFocusedCount(load: () => Promise<number>) {
  const [count, setCount] = useState<number | null>(null);
  const [failed, setFailed] = useState(false);
  useFocusEffect(useCallback(() => {
    let active = true;
    setFailed(false);
    load().then(
      value => { if (active) setCount(value); },
      () => { if (active) setFailed(true); },
    );
    return () => { active = false; };
  }, [load]));
  return { count, failed };
}

The caller should provide a stable loader. Ignoring a result prevents stale UI updates; it does not cancel the underlying operation or create a cache.

58. How do you pass data between navigation screens safely?

I pass the minimum serializable information needed to identify the destination, such as a record ID and an optional tab, rather than passing a complete mutable domain object. The destination reads the current record from its data layer. This avoids two screens holding inconsistent copies and supports persisted navigation state and deep links.

I define route parameter types to catch ordinary mistakes during development, then validate any parameters originating outside the app because TypeScript does not validate runtime input. Functions, component instances, and sensitive tokens should not be route payloads. Large objects also make navigation state harder to debug and restore. If a destination needs an unsaved draft shared with another screen, I give that draft an explicit owner and stable identifier rather than hiding application state in navigation parameters.

59. How do you design authentication-aware navigation?

I derive the available navigation tree from authentication state instead of merely navigating away from the login screen. While credentials are being restored or validated, I show a deliberate loading state. After sign-in or sign-out, the appropriate screen group becomes available and the previous sessionโ€™s protected navigation history must no longer be reachable.

Navigation guards improve the user flow but do not authorize data access; the backend must enforce permissions for every protected operation. I clear account-scoped caches and pending work on sign-out, and I decide how a validated deep-link destination should be resumed after login. A token existing in storage is not proof that it is still valid. I also handle expired sessions during an operation without creating repeated redirects or allowing an old accountโ€™s response to populate the newly signed-in accountโ€™s screens.

60. How should an app handle an incoming deep link?

I route an incoming link through the appโ€™s configured linking system, validate its route and parameters, and wait until navigation and authentication restoration are ready. Cold-start links and links received while the app is already running need coverage; implementing only one path leaves common failures.

A link is untrusted input. I allow expected destinations, reject malformed identifiers, and avoid performing destructive actions simply because a URL requested them. Sensitive operations still require authorization and, where appropriate, user confirmation. Platform configuration matters: a JavaScript route mapping alone does not establish an iOS Universal Link or Android App Link association. I test installed release-like builds, authenticated and signed-out sessions, unavailable records, repeated delivery, and links from different entry points. Custom schemes are convenient, but I do not assume that a scheme name establishes exclusive ownership or proves who sent the link.

61. How is Expo Router different from React Native itself?

Expo Router is an optional routing layer with file-based route conventions for Expo-based React Native and web applications. React Native provides native rendering and platform APIs; it does not interpret a directory of files as the applicationโ€™s screen routes. Expo Router adds that structure and integrates navigation behavior with linking.

I choose it when its conventions fit the project rather than assuming every React Native app must use it. Layout files, dynamic routes, and route groups are router concepts, not React Native core APIs. Existing projects may instead configure React Navigation directly or use another supported navigation solution. The router does not remove the need to model authentication, validate external parameters, and configure platform link associations. I check the installed Expo Router and Expo SDK documentation together because features and recommended configuration evolve across releases.

62. When should platform-specific files replace Platform checks?

I use small Platform checks for small differences and platform-specific files when a component or adapter has substantially different implementations. Keeping a common public interface allows the caller to stay platform-independent without filling one file with unrelated branches.

For example, a shared screen can import a platform adapter by its base name while Metro resolves ReceiptPreview.ios.tsx or ReceiptPreview.android.tsx. I keep shared validation, data fetching, and business rules outside those files so a visual difference does not become two independently evolving applications. Both implementations need tests for the same public contract. File splitting is an organizational choice, not proof of equivalent behavior: native permissions, lifecycle events, and UI conventions can still differ. I also verify how the bundler resolves a fallback when the app supports web or additional platforms.

63. What should you verify before adopting a native dependency?

I verify compatibility with the exact React Native version, New Architecture, target platforms, and native build workflow before adopting a native dependency. A package that installs successfully in JavaScript can still fail during CocoaPods or Gradle integration or call native code absent from the installed binary.

I inspect maintenance, release notes, licensing, permission requirements, transitive native dependencies, and the supported installation path. In an Expo workflow, I check whether the package works in the selected client or requires a development build and config plugin. Adding native code requires rebuilding the binary; refreshing JavaScript is not enough. I test the feature in a minimal integration and then in the appโ€™s real build configuration, including release mode and supported OS versions. A dependencyโ€™s popularity is useful context, but documented support and a reproducible integration test are better evidence.

64. How do you request and handle runtime permissions?

I request permission when the user reaches the feature that needs it, explain the purpose, and handle denial as an expected outcome. Native configuration must declare the required capability or usage description before the runtime request can work correctly. Android and iOS permission models differ, and some permissions have limited or blocked states beyond a simple yes/no.

This Android-only helper assumes the camera permission is already declared in the manifest:

TypeScript example:

import { PermissionsAndroid, Platform } from 'react-native';
export async function requestAndroidCamera(): Promise<boolean> {
  if (Platform.OS !== 'android') return false;
  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.CAMERA,
  );
  return result === PermissionsAndroid.RESULTS.GRANTED;
}

import { PermissionsAndroid, Platform } from 'react-native';
export async function requestAndroidCamera(): Promise<boolean> {
  if (Platform.OS !== 'android') return false;
  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.CAMERA,
  );
  return result === PermissionsAndroid.RESULTS.GRANTED;
}

import { PermissionsAndroid, Platform } from 'react-native';
export async function requestAndroidCamera(): Promise<boolean> {
  if (Platform.OS !== 'android') return false;
  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.CAMERA,
  );
  return result === PermissionsAndroid.RESULTS.GRANTED;
}

This minimal helper collapses non-granted outcomes. A production implementation should preserve the permission status so its caller can distinguish denied from never-ask-again, and provide an iOS implementation through the chosen camera or permission library. I recheck permissions when appropriate after returning from settings and avoid repeatedly prompting users who already declined.

65. How should a mobile app handle network failures and retries?

I classify failures before retrying them. A temporary network interruption or eligible server failure may be retriable, while invalid input or an authorization failure usually requires a different response. Retry attempts are bounded and delayed with backoff and jitter so many clients do not immediately hammer a recovering service.

A lost response does not prove that a write failed on the server, so consequential mutations need an idempotency contract or a reconciliation step before repetition. I respect server retry guidance when supplied and avoid retry loops after logout or a changed request context. Connectivity status is only a hint: being connected to Wi-Fi does not guarantee access to the API. The UI should distinguish cached content, active retry, and a final error, and offer a manual recovery path. App suspension and foregrounding also require deliberate timeout and resumption behavior.

66. How would you design an offline mutation queue?

I model an offline queue as durable operations with identity and lifecycle, not as an array of anonymous requests. Each entry records its account, operation ID, payload version, dependencies, and state so a restart does not lose the intent or replay it for the wrong user.

The local change and queue insertion should be atomic where the storage system permits it. A worker sends eligible operations with bounded retries and a backend deduplication contract, then records acknowledgement before removing them. Ordering is necessary for dependent writes; unrelated operations may be processed separately. Conflicts need a product rule, such as version checks and explicit user resolution, rather than assuming the last response is correct. I expose pending and failed changes to the user and define logout behavior. Background execution is opportunistic on mobile, so correctness cannot depend on a JavaScript timer running indefinitely.

67. How do you choose between key-value storage, a database, and secure storage?

I choose storage according to access pattern, sensitivity, and consistency requirements. Small non-sensitive preferences fit a key-value store. Related records, queries, transactions, or a durable outbox usually justify a database. Credentials belong in an appropriate platform-backed secure-storage solution rather than an ordinary preference store.

AsyncStorage is a community package, not a current React Native core API, and it should not be assumed to encrypt secrets. SQLite access likewise comes through a library such as Expo SQLite, whose configuration and encryption options must be checked separately. Secure storage is not a replacement for a large relational database or a guaranteed permanent backup. I also define schema versions, account partitioning, logout cleanup, corruption recovery, and data-retention requirements. The right choice includes the failure behavior: a missing theme preference is tolerable, while silently losing an acknowledged offline business operation may not be.

68. How should access tokens and other secrets be handled?

I keep server credentials off the client and minimize the lifetime and exposure of user tokens. A value embedded in JavaScript, a native resource, or a bundled environment file can be extracted; changing the file extension does not make it a server-side secret. Public configuration and confidential credentials must be treated differently.

For tokens that need persistence, I use a maintained platform-backed secure-storage library with appropriate accessibility and backup settings. I avoid logging tokens or putting them in route parameters, analytics, or crash attachments. Logout clears local session material and account-specific data, while revocation and authorization remain server responsibilities. Secure storage reduces certain at-rest risks but cannot make a fully compromised device trustworthy. Refresh behavior, biometric access policies, reinstall behavior, and recovery from inaccessible stored values must be designed and tested for the chosen platforms and library.

69. How do you manage accessibility focus for a modal or dynamically updated screen?

I preserve a meaningful focus sequence when the interface changes, rather than moving accessibility focus on every render. When a modal appears, the screen reader should encounter its heading or first useful control, and background content should not remain an accidental interaction target. Dismissing the modal should return the user to a sensible place, often the control that opened it.

Native modal behavior and platform-specific accessibility properties help, but they need verification with VoiceOver and TalkBack. If a focus move is necessary, I wait until the target native element exists and is laid out, then use the focus API supported by the projectโ€™s React Native version. For a non-modal status update, a restrained announcement or live-region behavior is often preferable to stealing focus. I test repeated opening, asynchronous content, errors, dismissal, and reduced-motion settings, including cases where the original opener is no longer present.

70. How do you test dynamic text and screen-reader behavior?

I test the actual user journey with larger text and the platform screen readers, not just whether accessibility props exist in a component test. Labels, roles, state, reading order, hints, and focus movement must make sense together. A visible label can still be announced twice or hidden inside an incorrectly grouped parent.

I exercise maximum supported text scaling, long translated strings, landscape, narrow windows, keyboard navigation where relevant, and content that arrives asynchronously. Controls must remain reachable and text should not be clipped merely to preserve the original visual dimensions. Automated tests can verify semantic props and important state changes, while device checks reveal announcements and native focus behavior that JavaScript tests cannot reproduce. I also test loading, empty, error, and disabled states because accessibility often fails outside the ideal content-loaded screen.

71. What belongs in a unit test versus a component test?

I use unit tests for isolated rules such as validation, reducers, formatting, and conflict resolution. Component tests verify how rendered controls and messages respond to user interactions and supplied data. Separating pure logic from native adapters makes both kinds of tests easier to understand and less dependent on elaborate mocks.

For a checkout screen, a unit test can verify that a quantity cannot become negative, while a component test verifies that an invalid quantity produces an accessible error and prevents submission. I prefer assertions about user-visible behavior over implementation details such as a private Hookโ€™s internal state. Neither test proves that a permission dialog, keyboard, or native navigation transition works on a device. Those boundaries need integration or end-to-end coverage. The test suite should make its mocked assumptions visible rather than presenting a fully mocked screen as proof of native correctness.

72. How do you test asynchronous React Native UI behavior?

I wait for the observable asynchronous result rather than sleeping for an arbitrary duration. I control network responses or other asynchronous dependencies, assert the loading state, resolve or reject the operation, and assert the resulting UI. Success, failure, and obsolete-response behavior deserve separate tests.

This example targets React Native Testing Libraryโ€™s asynchronous rendering API and assumes the applicationโ€™s StatusScreen accepts the shown loader prop:

TSX example:

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

test('shows the loaded status', async () => {
  const load = jest.fn().mockResolvedValue('Ready');
  await render(<StatusScreen load={load} />);
  expect(await screen.findByText('Ready')).toBeTruthy();
  expect(load).toHaveBeenCalledTimes(1);
});

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

test('shows the loaded status', async () => {
  const load = jest.fn().mockResolvedValue('Ready');
  await render(<StatusScreen load={load} />);
  expect(await screen.findByText('Ready')).toBeTruthy();
  expect(load).toHaveBeenCalledTimes(1);
});

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

test('shows the loaded status', async () => {
  const load = jest.fn().mockResolvedValue('Ready');
  await render(<StatusScreen load={load} />);
  expect(await screen.findByText('Ready')).toBeTruthy();
  expect(load).toHaveBeenCalledTimes(1);
});

The application component and compatible Jest/RNTL configuration are prerequisites, not supplied by React Native core. I use findBy for eventual appearance and appropriate waiting utilities for other state changes; I avoid causing repeated side effects inside retrying assertions. Fake timers require deliberate advancement and cleanup.

73. How should native modules be mocked in tests?

I mock the boundary my JavaScript code depends on, with behavior that matches the native moduleโ€™s contract. If the real method returns a Promise, the mock should resolve or reject asynchronously as expected; if it returns a subscription, the mock should support removal. A constant successful return value hides error paths and lifecycle mistakes.

I keep native access behind a small adapter so most tests do not need to understand native internals. Dedicated contract or integration tests verify that the adapter and installed module agree. Mocks must be reset between tests and should include cancellation, unavailable capabilities, and permission failures when those affect behavior. I avoid replacing an entire navigation or native subsystem with a permissive mock simply to make rendering succeed. A passing mock-based test proves the JavaScript response to the modeled behavior, not that the binary contains or correctly links the native dependency.

74. Which problems require end-to-end tests on a device or simulator?

I use device or simulator end-to-end tests when correctness crosses the native boundary: permission prompts, keyboard behavior, deep-link startup, navigation gestures, secure-storage integration, and process restart are typical examples. A JavaScript component test cannot demonstrate that native configuration or an OS interaction is correct.

I prioritize a small set of high-value journeys such as sign-in, core task completion, offline recovery, and upgrade persistence. Tests need deterministic data and explicit synchronization rather than long sleeps. Simulators are useful, but real devices remain important for hardware capabilities, biometrics, memory pressure, screen-reader behavior, and realistic performance. I also include the release configuration because development tooling can hide timing or packaging failures. End-to-end tests complement lower-level tests; using them for every business-rule combination makes feedback slow and failures harder to diagnose.

75. How do you prepare a React Native screen for localization and RTL?

I externalize user-facing text, use locale-aware formatting, and design layouts for text expansion and right-to-left reading. Translating strings after fixing every width is too late: long labels, plurals, dates, and mixed-direction identifiers can change the screenโ€™s layout substantially.

I prefer logical spacing such as start and end where appropriate, then inspect directional icons and gestures individually. Not every image should be mirrored. I18nManager.isRTL can inform platform-specific directional behavior, but forcing RTL is a development tool and changes may require a restart. Translation selection and message formatting normally come from an internationalization library rather than React Native core.

TypeScript example:

export function formatTotal(amount: number, locale: string, currency: string) {
  return new Intl.NumberFormat(locale, {
    style: 'currency', currency,
  }).format(amount);
}

export function formatTotal(amount: number, locale: string, currency: string) {
  return new Intl.NumberFormat(locale, {
    style: 'currency', currency,
  }).format(amount);
}

export function formatTotal(amount: number, locale: string, currency: string) {
  return new Intl.NumberFormat(locale, {
    style: 'currency', currency,
  }).format(amount);
}

I verify the target runtimeโ€™s locale support and test real translations, large text, screen-reader order, and mixed Arabic or Hebrew text with numbers.

76. What can an error boundary recover from?

An error boundary can replace a failing descendant React subtree with a fallback when React catches an error during rendering or relevant lifecycle work. It lets the rest of the application remain usable and provides a place to report a sanitized error with component context.

It is not a general exception handler. Ordinary event-handler exceptions, asynchronous Promise rejections, and native process crashes require their own handling; a boundary also cannot recover from an error in its own fallback. I place boundaries around meaningful recovery units, such as a screen or optional panel, rather than wrapping every tiny component. Resetting a boundary should address the underlying state or retry condition, not create an endless render/crash loop. For network failures, an explicit loading/error state is usually more appropriate than throwing every expected failure into a boundary. Native crash reporting remains a separate production concern.

77. How do you avoid state updates and resource leaks after a screen leaves?

I stop each resource at the lifecycle boundary that owns it: clear timers, remove subscriptions, cancel supported requests, and invalidate results that no longer belong to the screen. A blurred screen can still be mounted, so unmount cleanup alone is insufficient for resources that should only run while the screen is focused.

I distinguish a stale state update from a retained resource. Ignoring a late response protects UI correctness, but it does not release a socket or stop a native sensor. Conversely, removing a listener does not necessarily stop an operation it already triggered. Shared resources such as a global cache should be owned outside the screen and should not be destroyed just because one consumer leaves. I test repeated navigation, rapid account changes, and background/foreground transitions while observing listener counts and memory rather than relying on the absence of a warning.

78. How would you reconcile optimistic UI with a failed mutation?

I associate the optimistic change with a specific mutation and reconcile that change with the serverโ€™s outcome. For a simple isolated toggle, storing the previous value may be sufficient. With overlapping edits, restoring an entire old snapshot can erase newer successful work, so I track operation identity or rebase pending changes over authoritative data.

The UI should distinguish pending from confirmed state and provide a useful failure path. I avoid optimistic behavior for actions whose incorrect appearance would be misleading or dangerous without a clear confirmation model. A timeout means the outcome may be unknown, not necessarily rejected, so retries need the same duplicate-protection contract as other writes. On success, I reconcile server-generated IDs, versions, or normalized values; on conflict, I apply the productโ€™s resolution rule. Reactโ€™s optimistic-state facilities can help represent the UI, but they do not provide the backend transaction or conflict protocol.

79. How should a reusable custom Hook expose an asynchronous operation?

I give the Hook a small contract that describes its state and operations without hiding important lifecycle decisions. Consumers should know whether starting a second operation cancels the first, queues behind it, or runs concurrently. The Hook should expose errors and a deliberate reset or retry path rather than returning only data and a vague boolean.

A useful state model distinguishes idle, pending, successful, and failed outcomes while retaining existing data where the UX requires it. Inputs that affect the request belong in its identity and dependency model. The Hook can manage cancellation and result ownership, but shared caching, persistence, or authentication may belong in a separate service. I keep returned commands stable when consumers rely on their identity, and I document whether errors are returned as state or thrown to the caller. A custom Hook shares logic; each call does not automatically share the same state.

80. How do you debug a bug that occurs only on one mobile platform?

I reproduce the failure with an explicit platform, OS version, device, architecture, and build configuration before changing shared code. I compare native logs and JavaScript errors, then reduce the problem to the smallest screen or integration that still fails. The same JSX does not guarantee identical keyboard, permission, layout, or lifecycle behavior across platforms.

I check platform-specific props, native configuration, dependency versions, and recent changes to the iOS or Android project. If the issue appears only in release builds, I inspect optimization, signing/configuration differences, bundled assets, and symbolicated crash evidence rather than assuming the development debugger is representative. I avoid broad platform conditionals until the cause is understood; otherwise the workaround can conceal a misconfiguration. The fix needs a regression check on the failing platform and a check that the shared behavior still works on the other one.

React Native Interview Questions and Answers for Experienced

These answers focus on trade-offs that depend on runtime compatibility, platform requirements, and production evidence rather than generic optimization rules.

81. How do you choose a React Native version and plan a safe upgrade?

I choose a supported React Native release that is compatible with the app's native dependencies, build tools, and, when used, Expo SDK. The official pages checked for this October 2026 review identify 0.87 as latest, with 0.87 and 0.86 marked Active. A scheduled release date is not evidence that a release has shipped.

I inventory native modules, minimum OS versions, React compatibility, and toolchain requirements before upgrading. I apply template/native-project changes as well as JavaScript dependency changes, then test installation, authentication, navigation, permissions, persistence, and release builds on both platforms. I measure startup, crashes, and frame performance against the previous build. For a large jump, intermediate upgrades can isolate breakage. I keep a documented rollback and staged rollout plan; a successful package installation alone does not establish upgrade safety.

82. What changes with the React Native New Architecture?

The New Architecture changes React Native's rendering and native integration foundations: Fabric is the renderer, TurboModules support native-module integration, and JSI enables JavaScript-runtime interaction with C++. It enables closer integration with modern React scheduling rather than routing every interaction through the old serialized asynchronous bridge.

This is not an optional future migration for current releases. React Native 0.82 made the New Architecture the only runtime architecture; older flags cannot restore the Legacy Architecture there. Some legacy libraries can still work through interoperability layers, which is different from running the old architecture.

I evaluate compatibility and measured behavior rather than promising an automatic performance improvement. A screen that performs expensive JavaScript computation, loads oversized images, or mounts too many views can remain slow. Migration work includes dependency verification, native build changes, regression testing, and production monitoring.

83. How does Fabric render an interface?

Fabric is React Native's renderer: it translates React's description of an interface into native host views. Conceptually, rendering produces the React element structure and C++ shadow nodes, commit prepares the next tree and its layout, and mounting applies changes to the host views. Yoga participates in layout; platform-specific measurement still matters for content such as text.

The shadow tree is not a browser DOM, and a function component does not necessarily create a native view. View flattening can eliminate unnecessary host-view layers while preserving layout behavior. The immutable internal representation supports safe scheduling and reconciliation.

I keep component rendering pure because React may repeat or abandon render work. Network calls, subscriptions, and mutations belong outside render. For an actual layout regression, I inspect component structure, measured sizes, and native mounting behavior rather than treating Fabric as a guarantee that every render immediately reaches the screen.

84. What are TurboModules, and when should you create one?

A TurboModule exposes native functionality through a typed interface integrated with React Native's New Architecture. I create one when a required platform capability or SDK is unavailable through a maintained, compatible library. I would not write a custom module merely to wrap logic that already works reliably in JavaScript.

I define a small contract, implement each target platform, and specify errors, cancellation, resource ownership, and lifecycle behavior. Expensive operations should not become synchronous calls that block the caller. A Promise-shaped API also needs a native implementation that actually schedules expensive work appropriately.

This TypeScript specification is illustrative; it still requires Codegen configuration, registration, and native implementations:

TypeScript example

import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  readBatteryLevel(): Promise<number>;
}

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

import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  readBatteryLevel(): Promise<number>;
}

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

import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  readBatteryLevel(): Promise<number>;
}

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

getEnforcing is appropriate only when the module is required; it throws if registration is missing.

85. What does JSI provide, and what does it not guarantee?

JSI is a C++ interface for interacting with a JavaScript runtime. It allows integrations to expose native-backed functions and objects without requiring the Legacy Architecture's JSON-style bridge messaging for every interaction. It is an integration mechanism, not a worker pool, a UI framework, or a guarantee of zero overhead.

A synchronous native function can still block the JavaScript caller. Large conversions, allocations, locks, and crossings between execution contexts can also be expensive. Native code must respect runtime ownership, lifetimes, and thread-affinity requirements; accessing one runtime freely from arbitrary threads is unsafe.

I keep the boundary narrow and move genuinely expensive work to an appropriate execution context. Results return through a controlled interface, with cancellation and teardown considered. Before introducing a custom JSI integration, I compare a maintained native module, batching, and simpler algorithmic fixes, since low-level ownership mistakes can produce crashes that JavaScript error boundaries cannot recover from.

86. How does Codegen improve a native integration?

Codegen turns a typed native-module or native-component specification into integration scaffolding. It reduces hand-maintained agreement between JavaScript and platform interfaces, so a contract change can surface during generation or compilation instead of failing only when a user invokes the feature.

I treat the spec as a public API: supported types, nullability, event payloads, and method semantics need deliberate design. Codegen does not implement the underlying platform feature or prove that native code respects permissions, thread rules, cancellation, or resource cleanup. It also supports a defined type subset rather than every expressive TypeScript construct.

During upgrades I regenerate with the React Native version used by the app and verify both platform builds. I avoid manually patching generated files as the primary fix, because regeneration would erase those changes. Tests cover the behavioral contract and platform failure cases in addition to successful generation.

87. What is Hermes, and how do you assess its production impact?

Hermes is a JavaScript engine optimized for React Native. It is not Fabric, Metro, or a compiler that turns the whole React component tree into native machine-code UI. React Native integrates a compatible Hermes version, and release builds can use precompiled bytecode to reduce JavaScript loading work.

I assess the engine using the actual release configuration and representative devices. Startup time, memory, responsiveness, bundle artifacts, and crash diagnostics matter more than a microbenchmark alone. Development-mode overhead or a different execution environment can invalidate a comparison.

I also preserve the exact source maps and release identifiers needed to interpret production JavaScript traces. Engine upgrades can change performance characteristics, so I test the app's own startup path and heavy interactions again. I do not independently swap engine versions without checking the React Native integration's compatibility requirements, or assume an engine improvement fixes native layout and image-decoding bottlenecks.

88. How do you reason about JavaScript, UI, and native work?

I separate JavaScript execution, native UI work, and other native tasks rather than assuming that all work happens on one thread. React application logic commonly runs on the JavaScript thread, while host-view mutations must respect the platform's UI-thread requirements. Native libraries may dispatch work to other queues, and renderer scheduling is more nuanced than a permanently fixed three-thread diagram.

The practical question is which resource an operation blocks. Parsing a large response or filtering a huge array synchronously can delay JavaScript interactions. Heavy view mounting or drawing can hurt UI responsiveness even when JavaScript is idle. A native method's Promise return value does not itself prove its implementation avoids the main thread.

I trace the operation from event to visible result, measure queueing and execution, and move or reduce the actual bottleneck. Cross-thread handoffs must preserve cancellation, ordering, and lifetime safety.

89. How does concurrent React rendering affect native application design?

Concurrent rendering lets React prioritize and interrupt rendering work; it does not make arbitrary JavaScript run in parallel. I keep rendering pure, use stable state ownership, and avoid assuming that every attempted render commits. Urgent input feedback should remain responsive while less urgent rendering can be deferred.

A transition marks state updates as non-urgent, but wrapping an expensive synchronous calculation in startTransition does not move that calculation to a worker. I may still need indexing, smaller data sets, incremental processing, or a native implementation for CPU-heavy work.

I verify the React Native and React versions together rather than copying web-framework assumptions. Native applications do not automatically gain browser hydration, a DOM, or server routing from concurrent React support. My tests include rapid interactions and interrupted navigation, and I measure whether deferred work improves the actual interaction instead of adding transitions everywhere.

90. How would you audit dependencies before an architecture migration?

I build a dependency inventory around native risk, not just the number of npm packages. For each library I record its supported React Native versions, architecture support, platform implementations, maintenance activity, installation requirements, and whether it relies on removed APIs or custom build patches.

I prioritize critical-path integrations such as navigation, storage, payments, camera, authentication, and crash reporting. A JavaScript import succeeding does not prove that native registration, event delivery, cleanup, or release compilation works. I create small integration checks and test both fresh installs and upgrades with existing user data.

Where interoperability makes an older module work, I distinguish that from a fully migrated implementation and retain regression coverage. A blocker may justify an upgrade, replacement, bounded patch, or contribution upstream. I make that choice before scheduling the release, with an owner and a fallback rather than an undocumented dependency override.

91. How would you integrate React Native into an existing native app?

I would introduce React Native behind a well-defined native navigation boundary rather than replace the entire application at once. I select a contained flow, integrate the version-matched Android and iOS build setup, register its JavaScript entry point, and define who owns the React runtime and screen lifecycle. The host app remains responsible for native responsibilities such as signing and platform configuration.

I pass minimal initial data into the React Native surface and expose explicit native interfaces for shared capabilities instead of duplicating authentication or persistence. Navigation, back handling, safe areas, accessibility focus, and restoration must work across the boundary. I test repeated entry and exit, process recreation, background transitions, and a release build without Metro. Runtime initialization can affect first-entry latency, so I measure it before choosing prewarming or reuse. A pilot should have a reversible routing switch and agreed ownership of crashes, upgrades, and native dependency conflicts.

92. How do you diagnose a slow screen using production-like evidence?

I start with a repeatable user interaction and measure it in a release or production-like profiling build on representative devices. I record the app version, device, OS, data size, network conditions, and whether caches are warm. Development-mode timings can be dominated by checks and tooling that users do not experience.

I separate waiting for data from JavaScript computation, React rendering, native layout, drawing, and image work. React profiling can identify expensive commits; platform traces show main-thread stalls and native resource pressure. I compare distributions and slow-device results rather than one favorable average. Then I change one suspected cause, rerun the same scenario, and check correctness and memory alongside latency. For example, reducing repeated row renders is useful only if those renders are the measured bottleneck. I preserve a baseline and a regression budget so the improvement remains observable after later releases.

93. How do you distinguish JavaScript frame drops from UI frame drops?

JavaScript frame drops indicate that the JavaScript runtime cannot complete interaction and rendering work promptly; UI frame drops indicate that native frame production misses its deadline. They can occur independently or together. A native-driven scroll or animation may remain smooth while a JavaScript press handler is delayed, whereas expensive native layout or drawing can stutter despite relatively idle JavaScript.

I correlate JavaScript profiles with platform traces instead of treating an FPS overlay as a complete diagnosis. I look for long synchronous calculations, repeated renders, excessive logging, image decoding, layout churn, and GPU pressure. The frame budget depends on refresh rate: approximately 16.7 milliseconds at 60 Hz and 8.3 milliseconds at 120 Hz, with multiple stages sharing that budget. Moving a computation into a synchronous native call does not make the caller free. The fix must target the blocked stage and be checked under the same interaction and device conditions.

94. How do you improve cold-start performance?

I break cold start into native process initialization, runtime and bundle preparation, module initialization, initial rendering, and time until the first useful interaction. I measure cold and warm starts separately; a cached second launch can conceal the problem users experience after installation or process termination.

I remove unnecessary startup work, defer noncritical initialization, and avoid loading data or modules that the first screen does not need. Hermes bytecode can help JavaScript startup, but it does not eliminate native SDK initialization, asset loading, or expensive application code. I examine the dependency graph and evaluate supported lazy-loading techniques against the actual bundler configuration. A splash screen should cover necessary work without becoming a substitute for reducing it. I compare traces before and after each change, including low-end devices and first-run migrations. A startup optimization fails if it merely shifts an unacceptable pause to the first tap or breaks offline entry.

95. How do you investigate increasing memory usage?

I first establish whether memory grows without bound or rises to a stable cache plateau. I repeat a bounded scenario, such as opening and closing a screen twenty times, then compare memory and retained objects after the system has had a chance to reclaim temporary allocations. One high reading is not sufficient evidence of a leak.

I inspect JavaScript retention and native allocations separately. Common causes include forgotten listeners, timers, subscriptions, large closures, unbounded caches, decoded images, native objects retained across screen exits, and abandoned asynchronous work. Heap snapshots can reveal retaining paths; platform memory tools help find native growth that a JavaScript heap view misses. I verify cleanup at the lifecycle that actually ends the resource, since a navigation screen may remain mounted while unfocused. After a fix, I rerun the original loop and stress memory pressure. I also check that aggressive cache eviction does not create excessive network or rendering work.

96. How would you optimize a large, image-heavy feed?

I optimize an image-heavy feed as a pipeline: data arrival, row rendering, image transfer, image decoding, layout, and memory retention. Virtualizing rows helps, but it does not automatically make oversized images or expensive item components cheap. I request appropriately sized assets, reserve stable image dimensions where possible, and choose a cache policy suited to the image library and privacy requirements.

I profile realistic scrolling before changing list window or batch settings. Smaller windows can reduce memory but expose blank areas; larger batches may delay interaction. Stable item identity and inexpensive row props help avoid unnecessary work, while predictable geometry improves scrolling calculations. I avoid prefetching every image in a large feed and test fast scrolling, slow networks, dynamic text, and device rotation. If a different list implementation is proposed, I benchmark it with the actual row complexity and verify recycling-related state behavior rather than assuming it is universally faster.

97. How do you diagnose incorrect layout measurements and view flattening?

I verify what is being measured, when the measurement runs, and which coordinate system the consumer expects. A composite React component does not automatically expose native measurement methods; the ref must reach an appropriate native-backed view. I normally coordinate layout-sensitive measurement through useLayoutEffect and guard against a missing or unmounted ref.

For example, this reports a native view's window coordinates through the documented callback API:

TSX example:

import {useLayoutEffect, useRef} from 'react';
import {View} from 'react-native';

type Rect = {x: number; y: number; width: number; height: number};
export function Anchor({onMeasure}: {onMeasure: (rect: Rect) => void}) {
  const ref = useRef<View>(null);
  useLayoutEffect(() => {
    ref.current?.measureInWindow((x, y, width, height) => {
      onMeasure({x, y, width, height});
    });
  }, [onMeasure]);
  return <View ref={ref} style={{width: 80, height: 40}} />;
}

import {useLayoutEffect, useRef} from 'react';
import {View} from 'react-native';

type Rect = {x: number; y: number; width: number; height: number};
export function Anchor({onMeasure}: {onMeasure: (rect: Rect) => void}) {
  const ref = useRef<View>(null);
  useLayoutEffect(() => {
    ref.current?.measureInWindow((x, y, width, height) => {
      onMeasure({x, y, width, height});
    });
  }, [onMeasure]);
  return <View ref={ref} style={{width: 80, height: 40}} />;
}

import {useLayoutEffect, useRef} from 'react';
import {View} from 'react-native';

type Rect = {x: number; y: number; width: number; height: number};
export function Anchor({onMeasure}: {onMeasure: (rect: Rect) => void}) {
  const ref = useRef<View>(null);
  useLayoutEffect(() => {
    ref.current?.measureInWindow((x, y, width, height) => {
      onMeasure({x, y, width, height});
    });
  }, [onMeasure]);
  return <View ref={ref} style={{width: 80, height: 40}} />;
}

The parent should keep the callback stable and trigger remeasurement when relevant geometry changes; this example does not track scrolling automatically. Fabric can flatten layout-only views to reduce native hierarchy cost, so React component nesting is not a guaranteed one-to-one native tree. I investigate flattening-related assumptions explicitly instead of disabling the optimization across the app.

98. How should animation and gesture architecture change under heavy load?

I keep latency-sensitive animation work away from a busy JavaScript event loop when the chosen animation system supports that execution model. React Native Animated can serialize supported animations to its native driver; a gesture or animation library may provide a separate UI-runtime model. These are different APIs with different limitations, not automatic behavior for all JavaScript.

I avoid sending every animation frame through React state, minimize unnecessary crossings between runtimes, and move only work that is appropriate for that runtime. Large synchronous worklets can still block UI responsiveness. I verify which properties and event types are supported before selecting the native driver, and keep one consistent driver for a given animated value. Complex gesture arbitration, interruption, cancellation, reduced-motion settings, and accessibility alternatives are part of the design. I test while the app is loading data or rendering a heavy screen: a smooth isolated demo does not establish that the production interaction remains responsive.

99. How do you make production crashes actionable?

I make every crash trace attributable to the exact native build and JavaScript update that produced it. JavaScript source maps must match the deployed bundle; native symbol files must match the binary. A source map from a nearby commit can produce a believable but wrong location, so build identifiers and artifact retention are essential.

I capture sanitized breadcrumbs, device and OS information, release channel, runtime version, and relevant feature flags without recording tokens or sensitive user content. I distinguish JavaScript exceptions, native crashes, memory termination, and Android application-not-responding events because they require different evidence and remediation. I group failures by underlying cause and prioritize impact rather than raw event count alone. For an over-the-air release, the native build identifier is insufficient: the update identifier also matters. I reproduce the smallest failing scenario, add an appropriate regression test, and verify that the affected release population improves after rollout.

100. What can an over-the-air update change safely?

An over-the-air update can replace compatible JavaScript and assets through an update system such as Expo Updates; it cannot rewrite the native code already installed in an app binary. Adding a native module, changing native capabilities or permissions, or upgrading an incompatible native dependency generally requires a new build. React Native core does not itself provide a hosted update service.

I evaluate compatibility against the installed runtime, not just whether Metro can create a bundle. Even a JavaScript-only change can break an older native API contract or persisted-data assumption. The update must also comply with the distribution platform's applicable policies; OTA is not a way to bypass review requirements. I test a candidate update on the same binary and runtime cohort that will receive it, roll out gradually, and monitor startup and critical flows. I keep a compatible recovery path and avoid irreversible migrations that would make a rollback unsafe.

101. How do you define and enforce update runtime compatibility?

I treat the runtime version as a compatibility boundary between an installed native binary and the JavaScript updates it may load. A channel identifies an update audience or stream; it does not by itself establish native compatibility. I choose a runtime-version policy that changes whenever the native contract changes and make that policy part of the build and update pipeline.

For an Expo project, a fingerprint policy is one available approach:

JSON example:

{
  "expo": {
    "runtimeVersion": {
      "policy": "fingerprint"
    }
  }
}

{
  "expo": {
    "runtimeVersion": {
      "policy": "fingerprint"
    }
  }
}

{
  "expo": {
    "runtimeVersion": {
      "policy": "fingerprint"
    }
  }
}

This fragment illustrates the runtime policy, not a complete EAS Update configuration. I still verify build profiles, channels, native configuration, and update targeting. A policy is only effective if the artifacts and configuration used by CI are correct. I test updates against representative binaries in each supported runtime cohort and retain their identifiers. JavaScript-only database or API changes also need compatibility analysis because matching native runtimes does not guarantee application-level correctness.

102. How would you roll back a faulty over-the-air release?

I first stop or limit the faulty rollout, identify affected runtime cohorts, and determine whether the problem is in JavaScript, native code, configuration, or migrated data. For a compatible JavaScript regression, I can use the update system's supported mechanism to republish a previous update or direct clients to an embedded update. I verify the destination is valid for each affected binary before acting.

Rollback is not instantaneous: clients may be offline, update checks may be deferred, and adoption depends on the configured download and launch behavior. I monitor actual running update identifiers and critical-flow health rather than only the dashboard operation. A previous bundle may be unsafe after an irreversible local database change, so recovery sometimes requires a forward fix. Native regressions need a new store binary or another native-release remedy. I rehearse rollback with a production-like build and preserve enough release metadata to execute it without guessing during an incident.

103. How do you protect authentication secrets on mobile devices?

I minimize what the app stores and assume that a distributed mobile binary cannot keep an embedded service credential secret. Long-lived server secrets stay on a backend. For user credentials that must persist, I use an appropriate platform-backed secure storage library and choose access-control and backup behavior deliberately; ordinary asynchronous key-value storage is not a substitute.

Access tokens should be short-lived where the authentication design permits, refresh credentials should be revocable and protected, and logout should clear local credentials and relevant caches. Secure storage reduces exposure at rest but does not make a compromised running process trustworthy. Biometric gates, device integrity signals, and certificate controls each have separate limits. I never log tokens or pass them through navigation URLs. I test reinstall, restore, device-lock, and account-switch behavior because persistence differs across platforms. Authorization remains server-enforced, and recovery must not silently restore another user's cached data.

104. How do you coordinate concurrent token refreshes?

I coordinate refresh through one shared in-flight operation per authentication session, so simultaneous failures do not start competing refreshes. Every waiting request receives the same outcome, retries at most within a bounded policy, and uses the newly committed credential. I distinguish an expired credential from other authorization failures rather than refreshing on every forbidden response.

This helper demonstrates only same-process deduplication:

TypeScript example:

export function makeSingleFlight<T>(operation: () => Promise<T>) {
  let inFlight: Promise<T> | undefined;
  return (): Promise<T> => {
    if (!inFlight) {
      inFlight = Promise.resolve().then(operation).finally(() => {
        inFlight = undefined;
      });
    }
    return inFlight;
  };
}

export function makeSingleFlight<T>(operation: () => Promise<T>) {
  let inFlight: Promise<T> | undefined;
  return (): Promise<T> => {
    if (!inFlight) {
      inFlight = Promise.resolve().then(operation).finally(() => {
        inFlight = undefined;
      });
    }
    return inFlight;
  };
}

export function makeSingleFlight<T>(operation: () => Promise<T>) {
  let inFlight: Promise<T> | undefined;
  return (): Promise<T> => {
    if (!inFlight) {
      inFlight = Promise.resolve().then(operation).finally(() => {
        inFlight = undefined;
      });
    }
    return inFlight;
  };
}

The authentication layer must additionally track a session generation: a refresh started before logout or account switching must not install credentials into a newer session. I handle rotated refresh tokens atomically, avoid infinite refresh loops, and clear the session on terminal failure. This helper does not coordinate separate processes or make replaying a non-idempotent request safe.

105. How do you prevent duplicate writes when requests are retried?

I make retried writes idempotent at the server boundary rather than assuming the client can infer whether a timed-out request committed. A logical operation receives a stable identifier that is reused across its retries. The server validates the caller, associates the identifier with the operation and payload, and returns the recorded outcome or rejects incompatible reuse.

For example, a persisted queue item can keep the operation identity separate from the number of transport attempts:

TypeScript example:

type PendingWrite = {
  operationId: string;
  accountId: string;
  payload: {itemId: string; quantity: number};
  attempts: number;
};

export function nextAttempt(write: PendingWrite): PendingWrite {
  return {...write, attempts: write.attempts + 1};
}

type PendingWrite = {
  operationId: string;
  accountId: string;
  payload: {itemId: string; quantity: number};
  attempts: number;
};

export function nextAttempt(write: PendingWrite): PendingWrite {
  return {...write, attempts: write.attempts + 1};
}

type PendingWrite = {
  operationId: string;
  accountId: string;
  payload: {itemId: string; quantity: number};
  attempts: number;
};

export function nextAttempt(write: PendingWrite): PendingWrite {
  return {...write, attempts: write.attempts + 1};
}

The operation identifier must be generated once with an appropriate unique-ID facility, not regenerated by nextAttempt. The actual API must explicitly support the deduplication contract; adding an arbitrary header does not create it. I define retention and expiry, conflict handling, and what happens after an ambiguous outcome. I also prevent one account's queued operation from replaying under another account's credentials.

106. How would you design a reliable offline synchronization system?

I separate local persistence, synchronization state, and server authority. The app needs a durable local representation of pending work, stable operation identifiers, and explicit states such as pending, sending, acknowledged, and failed. A network indicator is only a hint; the request outcome determines whether synchronization succeeded.

I specify conflict semantics before choosing a library: some data can use last-write-wins, while inventory, balances, or collaborative edits need stronger server rules. I preserve ordering where operations depend on each other, apply bounded retries with backoff, and make writes idempotent. Account identity belongs in queue ownership so sign-out cannot replay old work under a different session. I design migrations for queued payloads and reconcile responses transactionally where possible. Tests include process termination between server commit and local acknowledgement, duplicate delivery, expired credentials, partial batches, and clock skew. The UI should show pending or conflicted state honestly rather than presenting an optimistic update as permanently committed.

107. How do you handle deep links as a security boundary?

I treat every incoming link as untrusted input, even when it appears to target my own app. I allowlist supported schemes or hosts and routes, parse parameters against an explicit schema, and verify authorization after resolving the requested resource. A link to an order identifier must not grant access to that order.

Verified Android App Links and iOS Universal Links associate a domain with an app, but they do not validate every parameter or authenticate the user. Custom schemes can be claimed by other applications, which matters especially for authentication redirects. I use the authentication provider's supported redirect and PKCE flow rather than inventing a token-bearing URL. I never put credentials or private content in links that can leak through logs or history. I test cold-start and already-running delivery, repeated links, malformed encodings, and signed-out routing. If login is required, I retain only a validated destination and recheck access after authentication.

108. How do you decide between JavaScript and native implementation?

I choose native implementation when the requirement depends on platform APIs, an existing native SDK, a measured computational bottleneck, or interaction that cannot meet its latency target through the current JavaScript path. I keep ordinary business logic in shared TypeScript when it remains clear, testable, and fast enough. Native code introduces build, deployment, maintenance, and platform-specific testing costs.

I design the boundary around useful operations rather than many tiny calls. Data transfer, ownership, cancellation, error translation, threading, and runtime teardown must be explicit. A synchronous native API can still block JavaScript or the UI thread, so moving code across the boundary is not itself an optimization. For example, a native image-processing operation may accept a file reference and return a result reference instead of copying large pixel arrays through JavaScript. I prototype and benchmark the whole path, including initialization and crossings, then compare that benefit with simpler algorithmic or data-model changes.

109. How do iOS and Android signing affect release ownership?

Signing establishes release identity and control over updates, so credentials must be owned by the organization rather than one developer's laptop. On Android, I distinguish the upload key from the app-signing key when using Play App Signing. On iOS, certificates, provisioning profiles, bundle identifiers, entitlements, and the distribution method must align.

I document who can create builds, access signing material, submit releases, and recover from credential loss. CI receives the minimum necessary access, with secrets kept out of repositories and build logs. Managed signing services can simplify operations, but the team still needs account ownership, role management, and recovery procedures. I verify that production and nonproduction identities cannot accidentally cross. Credential rotation is not one universal procedure: platform capabilities and consequences differ. I test installation and upgrades using distribution-like builds, because a development build working locally does not establish that the signed release has the right entitlements or update identity.

110. How do you make native builds reproducible in CI?

I make the build inputs explicit: source revision, lockfiles, Node and package-manager versions, React Native and React versions, native dependency resolution, Xcode or Android toolchain, build configuration, and environment variables. A repeatable build process matters more than relying on the state of a developer machine.

CI should install from lockfiles, use controlled toolchain images, separate dependency caches from authoritative inputs, and retain logs plus release artifacts. For Expo prebuild workflows, I choose whether native projects are generated or maintained directly and apply that choice consistently; config plugins and generation inputs are part of the build definition. I keep signing secrets isolated and avoid exposing them to untrusted pull-request code. I run a clean-build check when investigating cache-dependent failures. Reproducibility does not necessarily mean byte-identical signed archives, since timestamps and signing can vary, but the same declared inputs should reliably produce equivalent, traceable application behavior.

111. How do you roll out a mobile release safely?

I separate release readiness from release exposure. Before rollout, I verify the signed artifact, environment, migration behavior, critical journeys, and observability. I define success metrics and explicit pause criteria, then expose the change to a limited audience using the distribution mechanism available for that release type.

Native store releases and OTA updates have different rollout and recovery controls. Neither guarantees immediate adoption or immediate removal of a bad version. I monitor crashes, startup failures, authentication, transaction outcomes, and performance by build and update identifier, not just overall averages. A remote feature flag can reduce exposure when it was designed into the app, but it cannot repair an arbitrary native crash before the flag is read. I keep backend changes compatible with older clients and avoid combining unrelated high-risk changes in one rollout. Expansion is based on sufficient representative evidence, with an identified owner available to pause or recover the release.

112. What belongs in a React Native production test strategy?

I use complementary test layers rather than ask one tool to prove everything. Pure logic tests cover reducers, parsing, migrations, and retry decisions. Component tests verify user-visible behavior and asynchronous state transitions. Contract tests check the boundary between JavaScript and native or backend APIs. Device-level tests cover installation, navigation, permissions, keyboard interaction, and other integration-sensitive journeys.

I add performance and accessibility checks for the flows where regressions would matter, and run a focused release-build smoke suite on both platforms. Mocks make tests deterministic but can conceal native configuration errors, so the highest-risk integrations need real implementations in at least one test layer. I choose the device and OS matrix from supported users and known failure modes rather than maximizing the number of configurations indiscriminately. Failures must be diagnosable through artifacts and logs. A healthy strategy includes ownership of flaky tests and evidence that tests catch realistic faults, not only a coverage percentage.

113. How do you test native integrations and permission behavior?

I test native integrations at both their JavaScript contract and their actual platform boundary. Unit or component tests can verify success, denial, cancellation, and error handling through a controlled adapter. They cannot prove that a native dependency was linked correctly, that an entitlement exists, or that an operating-system permission dialog behaves as expected.

On devices or simulators, I exercise fresh installation, already-granted permission, denial, restricted or unavailable capability, revocation in settings, and returning to the app. I include relevant OS-version differences and distinguish a permission request from the underlying capability being available. Some capabilities need physical hardware, and some cannot be meaningfully tested in Expo Go because its native binary is fixed. I use the project's own development or release-like build where required. Tests must reset state intentionally rather than assume reinstall clears every platform credential or permission. I also verify that denied access leaves a useful, accessible recovery path instead of a dead screen.

114. How do you scale accessibility across a mobile codebase?

I build accessibility into shared components and acceptance criteria rather than leave it to a final audit. Controls need meaningful names, roles, state, focus behavior, adequate interaction targets, and layouts that remain usable with larger text. A component library can encode these defaults, but composed screens still need review because grouping and navigation order depend on context.

I combine automated checks with VoiceOver and TalkBack testing on representative devices. I cover modal transitions, dynamic announcements, error recovery, reduced motion, contrast, hardware input where relevant, and localization. Test identifiers do not substitute for accessible labels, and hiding difficult content from assistive technology is not a fix. I track regressions with the same ownership as functional defects and include accessibility in design reviews. Platform behavior differs, so a successful test on iOS does not establish Android behavior. Release readiness should reflect completed critical journeys, not merely the presence of accessibility properties in source code.

115. How would you design observability without leaking private data?

I define an explicit telemetry schema with the minimum fields needed to diagnose a problem. I prefer allowlisted event attributes over collecting arbitrary application state and trying to redact it later. Build identifiers, coarse device information, operation categories, timings, and sanitized error codes are often enough to identify a regression without recording message bodies or credentials.

For example, a pure mapper can prevent an application object from being passed through directly:

TypeScript example:

type FailureInput = {
  operation: 'loadFeed' | 'saveDraft';
  code: string;
  accessToken?: string;
  userText?: string;
};
export function failureEvent(input: FailureInput) {
  const allowed = new Set(['TIMEOUT', 'OFFLINE', 'UNAUTHORIZED']);
  return {
    event: 'operation_failed',
    operation: input.operation,
    code: allowed.has(input.code) ? input.code : 'OTHER',
  };
}

type FailureInput = {
  operation: 'loadFeed' | 'saveDraft';
  code: string;
  accessToken?: string;
  userText?: string;
};
export function failureEvent(input: FailureInput) {
  const allowed = new Set(['TIMEOUT', 'OFFLINE', 'UNAUTHORIZED']);
  return {
    event: 'operation_failed',
    operation: input.operation,
    code: allowed.has(input.code) ? input.code : 'OTHER',
  };
}

type FailureInput = {
  operation: 'loadFeed' | 'saveDraft';
  code: string;
  accessToken?: string;
  userText?: string;
};
export function failureEvent(input: FailureInput) {
  const allowed = new Set(['TIMEOUT', 'OFFLINE', 'UNAUTHORIZED']);
  return {
    event: 'operation_failed',
    operation: input.operation,
    code: allowed.has(input.code) ? input.code : 'OTHER',
  };
}

This is an illustrative allowlist, not a complete privacy system. I also review crash breadcrumbs, URLs, screenshots, SDK defaults, retention, access controls, consent requirements, and deletion workflows. I test redaction paths and avoid putting raw errors into telemetry when their messages can contain sensitive payloads.

116. How do you design resilient background work?

I design background work as interruptible, delayed, and constrained by the operating system. A JavaScript timer is not a guarantee that work will continue after the app is suspended. I choose an appropriate platform-backed facility, such as a scheduled background task, and verify its availability and limits through the library used by the app.

I persist enough progress to resume, make operations idempotent, honor cancellation and execution limits, and avoid assuming a precise interval. The OS may defer work based on battery, connectivity, usage, or other conditions. User force-quit behavior and platform restrictions also affect execution. For a sync task, I process a bounded batch, record acknowledgements safely, and let the next opportunity continue rather than trying to keep the process alive indefinitely. I provide foreground recovery so correctness does not depend on background scheduling. Tests include process termination and delayed execution on physical devices; an app remaining active in a debugger is not representative evidence.

117. How do you evolve persisted data safely across app versions?

I version persisted data and treat migrations as production code with failure and rollback behavior. A migration should transform a known old schema into a known new schema, preserve user data where possible, and avoid leaving a partially transformed store. Database transactions help when supported, but coordination with files or other stores may require additional recovery design.

This pure example handles one small preference-schema transition:

TypeScript example:

type V1 = {version: 1; darkMode: boolean};
type V2 = {version: 2; theme: 'light' | 'dark' | 'system'};
export function migratePreference(value: V1 | V2): V2 {
  if (value.version === 2) return value;
  return {version: 2, theme: value.darkMode ? 'dark' : 'light'};
}

type V1 = {version: 1; darkMode: boolean};
type V2 = {version: 2; theme: 'light' | 'dark' | 'system'};
export function migratePreference(value: V1 | V2): V2 {
  if (value.version === 2) return value;
  return {version: 2, theme: value.darkMode ? 'dark' : 'light'};
}

type V1 = {version: 1; darkMode: boolean};
type V2 = {version: 2; theme: 'light' | 'dark' | 'system'};
export function migratePreference(value: V1 | V2): V2 {
  if (value.version === 2) return value;
  return {version: 2, theme: value.darkMode ? 'dark' : 'light'};
}

Actual persisted input must be parsed and validated before calling this typed helper; TypeScript does not validate JSON at runtime. I test all supported upgrade paths, corrupt or missing data, interrupted writes, and older binaries encountering newer data. OTA rollback needs special care: a previous bundle may not understand a schema created by a newer update. Additive, backward-compatible changes often reduce that risk.

118. How do you keep environment configuration and credentials separate?

I separate public application configuration from credentials that authorize privileged operations. An API base URL or feature identifier may belong in a client build; a private service key does not. Environment-variable tooling changes how values enter the bundle, not whether users can inspect them afterward. Expo variables with the EXPO_PUBLIC prefix are intended for client-side use and are not secret storage.

Build and release credentials stay in controlled CI or signing infrastructure with least-privilege access. I avoid logging them, including them in generated app configuration, or exposing them to untrusted build steps. Runtime user tokens are a different category and need the authentication and secure-storage controls appropriate to the app. I validate environment selection in the release pipeline so a production binary cannot accidentally use a staging identity or endpoint. Secret scanning is useful defense in depth, but the main protection is an architecture that never requires a shared privileged secret inside the distributed client.

119. How do you manage platform divergence without duplicating the app?

I share business rules and stable interfaces while allowing platform-specific behavior where users or operating systems require it. Small differences can use Platform selection; larger differences belong in platform-specific modules or components. The shared caller should depend on a clear contract rather than contain many nested platform branches.

For example, the same import can resolve to separate implementations:

TypeScript example:

// haptics.ios.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return true;
}

// haptics.ios.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return true;
}

// haptics.ios.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return true;
}

TypeScript example:

// haptics.android.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return false;
}

// haptics.android.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return false;
}

// haptics.android.ts
export function prefersSelectionFeedbackByDefault(): boolean {
  return false;
}

These values are arbitrary product-policy defaults for this example, not platform capability claims: Android can support selection haptics too. The functions do not query hardware or trigger haptics; a real implementation must inspect the chosen API and device capability. I keep contract tests common and behavior tests platform-specific. I also document intentional UX differences, such as back navigation or permission education, so teams do not repeatedly erase them in the name of reuse. Shared code percentage is not the success metric: correctness, consistency of business behavior, and maintainable native experiences are.

120. How would you evaluate a proposed React Native performance rewrite?

I ask for a measurable problem, a representative benchmark, and a hypothesis explaining why the proposed rewrite should improve it. Rewriting a component in native code or replacing a list library is expensive if the real bottleneck is an oversized response, repeated state updates, or image decoding. I first establish which stage dominates the user-visible delay.

I compare a small prototype against the existing implementation under the same data, device, build mode, and interaction. I measure tail latency, frame stability, startup, memory, and power where relevant, not just one average. I also evaluate accessibility, correctness, cancellation, testability, platform coverage, and upgrade cost. A rewrite needs a migration path and a way to detect regressions after rollout. If a simpler change achieves the performance target with lower maintenance risk, I prefer it. The decision should be reversible where possible and supported by evidence that the improvement matters to the affected users.

React Native Documentation and Further Reading

The official React Native versions page lists 0.87 as the latest stable release at review. Examples emphasize modern React Native and distinguish New Architecture behavior from legacy migration concerns. React, Expo SDK, navigation, native toolchain, and operating-system versions must be matched to the actual project; an Expo preview or documentation entry is not assumed to be a stable release.

These curated primary references support further study. Source review and local syntax or pure-logic checks do not establish native compilation, device behavior, accessibility conformance, store approval, or production security.

Team Sherlock.sh

Building fraud intelligence for live high-stake video interactions (financial discussions, hiring interviews, board meeting etc).

Catch candidate fraud before it costs you.

Sherlock AI helps you detect AI-assisted, proxy, and deepfake candidates, so every hiring decision is backed by evidence.

Ask AI about Sherlock.sh

Similar Posts