CoreHRx Jobs

Apply in one click, save jobs and take free mock tests.

Register free Log in
Hiring? Post jobs with CoreHRx
My account
user

View & update profile
Interview guide

React Native Interview Questions and Answers: 60 Questions from Easy to Hard

This guide is for freshers and experienced mobile developers in India preparing for React Native interviews at product startups, IT services firms and in-house app teams. It has 60 questions in three levels. Easy covers core components, styling, Flexbox, lists, hooks and navigation basics. Medium covers what you meet in the first few years on a real app: FlatList performance, navigation patterns, state management, secure storage, permissions, animations and release builds. Hard covers the old bridge and the New Architecture (JSI, TurboModules, Fabric, Hermes), startup time, app size, memory leaks and design questions. Answer each question aloud before reading ours, then take the mock test to practise under a timer.

What React Native interviews cover

  • Core components and styling: View, Text, Image, TextInput, Pressable, StyleSheet and Flexbox, where flexDirection is column by default.
  • Lists and performance: FlatList and SectionList, keys, re-renders, memoisation and what makes a long list smooth on low-end Android phones.
  • App structure: React Navigation (stacks, tabs, deep links, login flows), state with hooks and a store, networking, offline data and secure storage.
  • Platform work: Android and iOS differences, permissions, release builds, signing, store releases and over-the-air updates.
  • For experienced roles: the old bridge versus the New Architecture (JSI, TurboModules, Fabric, Hermes), Reanimated, startup time, app size and native crashes.

How to prepare (freshers and experienced developers)

  • Freshers: build one small app end to end, such as an expense tracker with a list, a form, navigation and saved data, and be ready to explain every screen of it.
  • Learn React itself well first: props, state, effects, keys and re-renders come up in every React Native interview.
  • Run your app on a real Android phone in a release build at least once; many performance and crash questions come from that experience.
  • Experienced developers: prepare two or three stories of real problems you solved, such as a slow list, a release-only crash or a New Architecture upgrade, with numbers before and after.
  • Read the official React Native, React Navigation and Expo docs for the versions you claim, because defaults and recommended APIs change between releases.

Common mistakes and interview-day tips

  • Do not answer with web habits: there is no CSS cascade and no div, and text must always be inside a Text component.
  • Say which version you mean when it matters, for example that the New Architecture became the default in React Native 0.76.
  • When asked about performance, say how you would measure first (Perf Monitor, profiler, a release build) before naming fixes.
  • In live coding, keep components small, use stable keys in lists and clean up effects; interviewers notice a missing cleanup quickly.
  • If you have not used something, such as writing a native module, say so and explain how you would approach it instead of guessing.

React Native interview questions: Easy questions (freshers)

20 questions

1. What is React Native, and how is it different from a hybrid app that runs inside a WebView?

React Native is a framework from Meta for building Android and iOS apps with React and JavaScript or TypeScript. Your components become real native views, such as a ViewGroup on Android and a UIView on iOS, so scrolling, text input and gestures feel native. A hybrid app built with Cordova or Ionic draws HTML and CSS inside a WebView instead. The trade-off is that you still need some native knowledge for builds, permissions and libraries. Its motto is learn once, write anywhere, not write once, run anywhere.

2. Name the main core components of React Native and what each one maps to on Android and iOS.

View is the basic container, like a div on the web; it becomes a ViewGroup on Android and a UIView on iOS. Text shows text (TextView, UITextView), Image shows pictures (ImageView, UIImageView), TextInput takes keyboard input (EditText, UITextField) and ScrollView is a scrollable container. On top of these you get FlatList and SectionList for long lists, Pressable for touches, Modal, Switch and ActivityIndicator. Interviewers ask this to check you know there is no HTML in a React Native app.

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

export function ProfileCard({ name, city, photoUrl }) {
  return (
    <View style={styles.card}>
      <Image source={{ uri: photoUrl }} style={styles.avatar} />
      <Text style={styles.name}>{name}</Text>
      <Text>{city}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, alignItems: 'center' },
  avatar: { width: 64, height: 64, borderRadius: 32 },
  name: { fontSize: 18, fontWeight: '600' },
});

3. This badge crashes with the error that text strings must be rendered within a Text component, but only when the cart is empty. Why?

In React Native every piece of text must sit inside a Text component; a View cannot render a bare string or number. When items is empty, items.length is 0, and 0 && anything evaluates to 0, not false. React renders that 0 directly inside the View, and React Native throws because it is raw text outside Text. On the web you would only see a stray 0. Fix it with a real boolean, items.length > 0, or a ternary that returns null. Also note that font styles do not pass from a View to its children; only nested Text inherits them.

Example
function CartBadge({ items }) {
  return (
    <View style={styles.badge}>
      {items.length && <Text>{items.length} items</Text>}
    </View>
  );
}

4. How does styling work in React Native compared with CSS on the web?

There is no CSS file and no cascade. You pass JavaScript objects to the style prop, with camelCase names such as backgroundColor and fontSize, and sizes are unitless numbers in density-independent pixels, so 16 looks about the same on a budget phone and a flagship. StyleSheet.create keeps styles in one place. The style prop also takes an array, where later entries win and false or null entries are skipped, which makes conditional styles easy. Percentages, such as width: '50%', are written as strings.

Example
<Pressable
  style={[styles.button, disabled && styles.buttonDisabled]}
  disabled={disabled}
  onPress={onPay}
>
  <Text style={styles.label}>Pay now</Text>
</Pressable>

const styles = StyleSheet.create({
  button: { backgroundColor: '#1b5e20', padding: 12, borderRadius: 8 },
  buttonDisabled: { opacity: 0.5 },
  label: { color: '#fff', fontSize: 16, textAlign: 'center' },
});

5. How is Flexbox in React Native different from Flexbox in CSS?

The rules are the same, but some defaults differ. flexDirection defaults to column, so children stack vertically unless you set row. alignContent defaults to flex-start instead of stretch, and flexShrink defaults to 0 instead of 1, so items do not shrink to fit on their own. The flex property takes only a single number. Every View is already a flex container, so you never write display: flex, and layout is calculated by the Yoga engine. The most common mistake from web developers is expecting a row and getting a column.

Example
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
  <Image source={logo} style={{ width: 40, height: 40 }} />
  <Text style={{ flex: 1, marginLeft: 8 }} numberOfLines={1}>
    Sharma Electronics, Indore
  </Text>
  <Text>4.5</Text>
</View>

6. What does flex: 1 do, and why is a child with flex: 1 sometimes invisible?

A positive flex value makes a component take a share of the free space along its parent's main axis. With flex: 1 and flex: 2 on two siblings, the first gets one third of the space and the second two thirds. A child can only grow if its parent has a size. If the parent has neither a fixed height nor flex itself, its height is 0 and the flex children disappear. That is why the root View of a screen usually has flex: 1, so the chain from the screen down has real space to share.

Example
<View style={{ flex: 1 }}>
  <View style={{ flex: 1, backgroundColor: '#ffe0b2' }} />
  <View style={{ flex: 2, backgroundColor: '#ffb74d' }} />
</View>

7. When would you use a ScrollView and when a FlatList?

ScrollView renders all of its children at once, which is fine for a short screen like a sign-up form or a settings page. FlatList is virtualised: it renders only the items near the visible area and removes ones far off screen, so memory and render time stay low even with thousands of rows. Use FlatList, or SectionList for grouped data, whenever the list comes from an API or can grow. Putting 500 order rows inside a ScrollView makes the screen slow to open and can run a low-memory Android phone out of memory.

Example
<FlatList
  data={orders}
  keyExtractor={item => item.orderId}
  renderItem={({ item }) => <OrderRow order={item} />}
  ListEmptyComponent={<Text>No orders yet</Text>}
/>

8. Why does FlatList need a unique key for each item, and what goes wrong if you use the index?

React uses the key to track which row is which between renders, and FlatList also uses it to cache rows. keyExtractor tells FlatList where the key is; by default it looks for item.key, then item.id, and falls back to the index. With the index as the key, inserting a new message at the top shifts every key, so React matches rows wrongly: state such as a half-typed reply or an expanded card shows up on the wrong item, and more rows re-render than needed. Use a stable id from your data, as a string.

9. What is the difference between props and state in a React Native component?

Props are inputs passed from the parent; the component reads them but must not change them. State is data the component owns and changes over time, created with useState, such as the text in a search box or whether a dropdown is open. When state or props change, the component re-renders. A good rule: if a value can be calculated from props or other state, do not also store it in state, or the two copies will drift apart. Lift state up to the nearest common parent when two sibling components need the same value.

10. A button calls setCount(count + 1) three times in one press, but the count goes up by only one. Why?

State updates are not applied immediately. During this render count is a fixed value, say 0, so all three calls ask React to set the count to 1. React batches them and re-renders once, with 1. When the next value depends on the previous one, pass an updater function: setCount(c => c + 1). React queues the updaters and runs each on the result of the one before, so three calls give 3. The same reason explains why logging count right after setCount still prints the old value.

Example
const [count, setCount] = useState(0);

function addThree() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

11. What is the cleanup function in useEffect, and why does it matter in a mobile app?

The function you return from useEffect runs before the effect runs again and when the component unmounts. Use it to undo whatever the effect started: remove listeners, clear timers, cancel requests. Screens mount and unmount often as users navigate, so a missing cleanup leaves listeners alive, which gives duplicate handlers, updates to screens that are gone and memory that keeps growing. The dependency array decides when the effect re-runs: empty means once after the first render, and listed values mean whenever one of them changes.

Example
useEffect(() => {
  const sub = AppState.addEventListener('change', next => {
    if (next === 'active') {
      refreshBalance();
    }
  });
  return () => sub.remove();
}, []);

12. Which should you use for a tappable element: Button, TouchableOpacity or Pressable?

Button is quick but renders a platform-styled button that takes a title, a color and onPress; it has no style prop, so it rarely matches a design. TouchableOpacity dims its child when pressed and is still common in older code. Pressable is the newer core API the docs point to: it reports press in, press out and long press, and its style prop can be a function of the pressed state, so you can build any look. Add hitSlop for small icons and an accessibilityLabel so screen readers can announce it.

Example
<Pressable
  onPress={addToCart}
  onLongPress={showDetails}
  hitSlop={8}
  accessibilityRole="button"
  accessibilityLabel="Add to cart"
  style={({ pressed }) => [
    styles.chip,
    pressed && { opacity: 0.6 },
  ]}
>
  <Text>Add</Text>
</Pressable>

13. How do you build a mobile number field that is not hidden behind the keyboard?

Make TextInput controlled: keep the value in state, update it in onChangeText and pass it back through value. For an Indian mobile number use keyboardType phone-pad, maxLength 10 and autoComplete tel, and strip anything that is not a digit. The keyboard covers the bottom of the screen, so wrap the form in KeyboardAvoidingView, usually with behavior padding on iOS, or put it in a ScrollView. For email fields remember to turn autoCapitalize off, because by default TextInput capitalises the first letter of sentences.

Example
const [phone, setPhone] = useState('');

<KeyboardAvoidingView
  style={{ flex: 1 }}
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
  <TextInput
    value={phone}
    onChangeText={t => setPhone(t.replace(/[^0-9]/g, ''))}
    keyboardType="phone-pad"
    maxLength={10}
    autoComplete="tel"
    placeholder="10-digit mobile number"
  />
</KeyboardAvoidingView>

14. How do you show a local image and an image from a URL, and why does the remote one sometimes not appear?

A local image is loaded with require and a fixed path, such as require('./assets/logo.png'). The bundler packs it into the app and knows its size, and you can add @2x and @3x files for sharper screens. A remote image uses a source object with a uri. React Native cannot know its size before downloading it, so you must give it a width and height or a flex size; without them it renders at zero size and looks missing. Also use https, pick resizeMode cover or contain, and do not load huge originals into small thumbnails.

Example
<Image source={require('./assets/logo.png')} />

<Image
  source={{ uri: 'https://cdn.example.com/p/1234.jpg' }}
  style={{ width: 120, height: 120 }}
  resizeMode="cover"
/>

15. How do you write code that behaves differently on Android and iOS?

For small differences use the Platform module: Platform.OS is 'android' or 'ios', and Platform.select picks a value per platform, with a default key as a fallback. Platform.Version gives the Android API level or the iOS version, for features that need a newer OS. When a whole component differs, create Header.android.js and Header.ios.js and import './Header'; Metro picks the right file for each platform when it bundles. Keep platform checks in small components at the edges, so the rest of the code stays shared.

Example
import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  title: {
    fontFamily: Platform.select({
      ios: 'Avenir',
      android: 'sans-serif-medium',
      default: 'System',
    }),
  },
});

if (Platform.OS === 'android' && Platform.Version >= 33) {
  // Android 13+: ask for the notification permission
}

16. How do you call a REST API in React Native, and what does fetch not do for you?

React Native ships the fetch API, so you can call an API with async and await, or use a library such as axios. The catch is that fetch rejects only on a network failure; a 404 or a 500 still resolves, so you must check response.ok or response.status yourself. fetch has no timeout option either, so add one with AbortController, and show loading and error states in the UI. Release builds block plain http by default (App Transport Security on iOS, cleartext rules from Android 9), so use https.

Example
async function getOrders(token) {
  const res = await fetch('https://api.example.com/orders', {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  return res.json();
}

17. What is AsyncStorage, and what should you not store in it?

AsyncStorage, now the community package @react-native-async-storage/async-storage, is a simple key-value store that survives app restarts. Every call is asynchronous and returns a Promise, and it stores only strings, so you JSON.stringify objects and parse them back. It suits small things such as the chosen language, an onboarding-seen flag or a cached list. It is not encrypted, so never store passwords, auth tokens or card details in it; use the Keychain or Keystore through a secure storage library. It is not a database for large data either.

Example
import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.setItem('prefs', JSON.stringify({ lang: 'hi' }));

const raw = await AsyncStorage.getItem('prefs');
const prefs = raw ? JSON.parse(raw) : { lang: 'en' };

18. How do you move between screens and pass data with React Navigation?

Wrap the app in NavigationContainer and create a stack navigator, usually the native stack. Each screen gets a navigation prop, or calls the useNavigation hook. navigation.navigate('ProductDetails', { productId: 42 }) opens a screen with params, and the target reads them from route.params. goBack returns to the previous screen, and push always adds a new copy of a screen. Pass small values such as an id rather than a whole object, and load the rest on the details screen so the data is never stale.

Example
const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Products" component={ProductsScreen} />
        <Stack.Screen name="ProductDetails" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

function DetailsScreen({ route }) {
  const { productId } = route.params;
  // load the product by productId
}

19. What is the difference between starting a project with Expo and with a bare React Native project?

Expo is a framework and tool set on top of React Native: the Expo SDK modules (camera, notifications, file system), Expo Router, and services such as EAS Build and EAS Update. The React Native docs now recommend starting new apps with a framework such as Expo. A bare project gives you the android and ios folders to manage yourself in Android Studio and Xcode. Expo no longer means no native code: development builds and config plugins let you add native libraries. Only Expo Go, the sandbox app, is limited to the modules it already contains.

20. What tools do you use to debug a React Native app during development?

Fast Refresh shows code changes within a second and keeps component state. The Dev Menu (shake the device, Ctrl+M or Cmd+M on the Android emulator, Cmd+D on the iOS simulator) lets you reload, open the debugger and show the Perf Monitor. LogBox shows warnings and errors on screen. Since 0.76, React Native DevTools is the default debugger, with a console, breakpoints and the React Components and Profiler tabs. For native crashes, read adb logcat or the Xcode console. Remember that debug builds are slower than release builds.

Ready to check yourself? Take the free React Native mock test

React Native interview questions: Medium questions (1-3 years)

20 questions

21. A FlatList of 2,000 transactions in a Bengaluru fintech app stutters while scrolling. What do you check and tune?

First test a release build, because dev mode is much slower. Then make each row cheap: wrap the row in React.memo, pass a stable renderItem, and avoid new objects and functions per row. Use a stable keyExtractor. If rows have a fixed height, add getItemLayout so FlatList skips measuring them. Tune windowSize (default 21, counted in visible screen lengths), maxToRenderPerBatch and initialNumToRender to trade memory against blank areas. Resize images to thumbnail size. If it is still slow, profile before switching to a recycling list.

Example
const ROW_HEIGHT = 72;
const renderTxn = ({ item }) => <TxnRow txn={item} />;

<FlatList
  data={transactions}
  keyExtractor={item => item.id}
  renderItem={renderTxn}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
  windowSize={7}
  maxToRenderPerBatch={8}
/>

22. Why does FlatList have an extraData prop, and what other mistake stops a list from updating?

FlatList is a PureComponent: it skips re-rendering when its props are shallow-equal. If rows depend on something that is not in data, such as selectedId or the chosen currency, and renderItem is memoised, a change never reaches the rows. Passing that value as extraData tells FlatList to re-render. The second classic bug is mutating state: pushing into the same array and calling setCart(cart) hands React the same reference, so it skips the update completely. Always create a new array or object when you change list data.

Example
// Bug: same array reference, the list never updates
const addItemBuggy = item => {
  cart.push(item);
  setCart(cart);
};

// Fix: a new array every time
const addItem = item => {
  setCart(prev => [...prev, item]);
};

<FlatList
  data={cart}
  extraData={selectedId}
  renderItem={renderCartRow}
/>

23. When do React.memo, useCallback and useMemo actually help, and when are they wasted?

React.memo skips re-rendering a component when its props are shallow-equal. That only works if the parent passes stable props, which is what useCallback (stable functions) and useMemo (stable objects or expensive results) are for. In the snippet the first Row still re-renders every time, because a new arrow function and a new style object are created on each parent render. They pay off in long lists, heavy charts and context values, and are wasted on cheap components. Wrong dependency arrays give stale values. Measure with the React Profiler first.

Example
const Row = React.memo(function Row({ item, onPress, style }) {
  return (
    <Pressable onPress={() => onPress(item.id)} style={style}>
      <Text>{item.name}</Text>
    </Pressable>
  );
});

// Breaks memo: new function and object on every render
<Row item={item} onPress={id => open(id)} style={{ padding: 12 }} />

// Keeps memo: stable references
const handlePress = useCallback(id => open(id), [open]);
<Row item={item} onPress={handlePress} style={styles.row} />

24. This OTP resend countdown shows 59 and then stays stuck. Find the bug.

The effect runs once because of the empty dependency array, so the interval callback is a closure over the first render, where seconds is 60. Every tick calls setSeconds(60 - 1), so the value sticks at 59. This is a stale closure. Fix it with the updater form, setSeconds(s => s - 1), which always receives the latest value, and stop at zero. Adding seconds to the dependencies also works but recreates the interval every second. Timers are not reliable in the background, so for OTP screens compute the time left from an end timestamp.

Example
const [seconds, setSeconds] = useState(60);

useEffect(() => {
  const id = setInterval(() => {
    setSeconds(seconds - 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

25. How do you decide between Context, Redux Toolkit, Zustand and a data-fetching library such as TanStack Query?

Split state by kind first. Server data, such as orders or a product catalogue, is best handled by TanStack Query or RTK Query, which give caching, refetching, retries and loading states for free. Client state that many screens share, such as the cart or the signed-in user, fits Redux Toolkit or Zustand, which let components subscribe to small slices. Context suits values that rarely change, like theme or language. Local UI state stays in useState. The usual mistake is copying every API response into Redux by hand and rebuilding a weaker cache.

26. Why does every screen re-render whenever anything in this AuthContext changes, and how do you fix it?

Every component that reads AuthContext re-renders when the provider's value changes by reference. Here value is a new object on every render of AuthProvider, and lastSeen changes often, so all consumers re-render even if they only need user. Wrap the value in useMemo with its real dependencies, and move fast-changing data into its own context or into a store with selectors. Context has no built-in selector, so it works best for values that change rarely, such as theme, language or the signed-in user.

Example
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [lastSeen, setLastSeen] = useState(Date.now());

  return (
    <AuthContext.Provider
      value={{ user, setUser, lastSeen, setLastSeen }}
    >
      {children}
    </AuthContext.Provider>
  );
}

27. How do you structure navigators so that detail screens open without the bottom tab bar?

Nest the navigators. The root is a stack; its first screen is the tab navigator, and screens that should hide the tabs, such as order details or checkout, are registered in the root stack next to it. Opening OrderDetails from a tab then pushes it over the whole tab navigator, so the tab bar is gone and back returns to the same tab. The React Navigation docs recommend this over hiding the tab bar screen by screen. Each navigator keeps its own history, so decide where every screen lives before writing code.

Example
function HomeTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Orders" component={OrdersScreen} />
      <Tab.Screen name="Account" component={AccountScreen} />
    </Tab.Navigator>
  );
}

<Stack.Navigator>
  <Stack.Screen
    name="Tabs"
    component={HomeTabs}
    options={{ headerShown: false }}
  />
  <Stack.Screen name="OrderDetails" component={OrderDetails} />
  <Stack.Screen name="Checkout" component={Checkout} />
</Stack.Navigator>

28. What is the right way to handle login and logout with React Navigation?

Do not navigate to Home after login and then try to block the back button. Render different screens based on auth state instead: while the token is being read from secure storage show a splash, with no token register only the sign-in screens, and with a token register the app screens. When the token changes, React Navigation moves to the first screen of the new set by itself, and back cannot return to the login screen because it no longer exists. Logout then simply clears the token and the state.

Example
<Stack.Navigator>
  {userToken == null ? (
    <Stack.Screen name="SignIn" component={SignInScreen} />
  ) : (
    <>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Profile" component={ProfileScreen} />
    </>
  )}
</Stack.Navigator>

29. How do you set up deep links so that a link like myshop://product/42 opens the product screen?

Register the URL scheme natively (an intent filter on Android, URL types on iOS; universal links and App Links also need a verified domain), then give NavigationContainer a linking config that maps paths to screens. React Navigation parses the path, finds the screen and passes id as a param, also when the link launches the app from cold. Path params arrive as strings, so id is '42', not 42; add a parse function or convert it yourself. Test with npx uri-scheme open or adb, and handle a product that no longer exists.

Example
const linking = {
  prefixes: ['myshop://', 'https://shop.example.in'],
  config: {
    screens: {
      Tabs: {
        screens: {
          Orders: 'orders',
        },
      },
      Product: {
        path: 'product/:id',
        parse: { id: Number },
      },
    },
  },
};

<NavigationContainer linking={linking}>{/* ... */}</NavigationContainer>

30. Where should a React Native app keep the login token, and why not in AsyncStorage?

AsyncStorage writes plain, unencrypted data, which can be read on a rooted phone or pulled out of a device backup. Tokens belong in the platform's secure store: the Keychain on iOS and the Keystore-backed storage on Android, used through a library such as react-native-keychain or expo-secure-store. A common pattern keeps the short-lived access token in memory and only the refresh token in secure storage, and clears both on logout. Never put API secrets in the JavaScript bundle; anything shipped in the app can be extracted.

31. In a city search screen, results for an older query sometimes replace newer ones. How do you fix it?

It is a race: the request for 'pa' can finish after the request for 'pune', and its stale results overwrite the newer ones. Debounce typing so fewer requests go out, and cancel the previous request when the query changes with AbortController, which React Native's fetch supports. The effect cleanup clears the timer and aborts the old request, and the resulting AbortError is ignored. Another way is to tag each request with an id and drop responses that are not the latest. TanStack Query handles this by keying results by query.

Example
useEffect(() => {
  if (query.length < 2) return;
  const controller = new AbortController();
  const timer = setTimeout(async () => {
    try {
      const res = await fetch(
        `${API}/cities?q=${encodeURIComponent(query)}`,
        { signal: controller.signal },
      );
      setResults(await res.json());
    } catch (e) {
      if (e.name !== 'AbortError') setError(e);
    }
  }, 300);
  return () => {
    clearTimeout(timer);
    controller.abort();
  };
}, [query]);

32. How do you make a layout work on small phones, tablets and when the user picks a large system font?

Build with Flexbox and percentages instead of fixed widths, and use flexWrap for chips and grids. When you need the window size, use the useWindowDimensions hook, which updates on rotation, split screen and font scale changes; Dimensions.get is a one-time read that can go stale. Text follows the system font size by default (allowFontScaling is true), so avoid fixed heights on text containers or text gets clipped, and cap extreme sizes with maxFontSizeMultiplier instead of turning scaling off. Changing numColumns needs a new key on the FlatList.

Example
function ProductGrid({ products }) {
  const { width } = useWindowDimensions();
  const columns = width >= 600 ? 3 : 2;

  return (
    <FlatList
      key={columns}
      numColumns={columns}
      data={products}
      renderItem={renderProduct}
    />
  );
}

33. A card has a soft shadow on iOS but none on Android. Why, and how do you fix it?

shadowOffset, shadowOpacity and shadowRadius are iOS-only styles. Android draws shadows from elevation instead, and shadowColor only tints that shadow on Android 9 and above. So set both, usually with Platform.select. Give the card a solid background colour, because a transparent view does not show an elevation shadow on Android and iOS cannot draw the shadow efficiently. On iOS, overflow hidden on the same view clips the shadow, so clip on an inner view. From React Native 0.76 the boxShadow style works on both platforms, but only with the New Architecture.

Example
const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.15,
        shadowRadius: 6,
      },
      android: { elevation: 4 },
    }),
  },
});

34. How do you handle the Android hardware back button on a form with unsaved changes?

Use BackHandler. A hardwareBackPress listener that returns true says you handled the press, so the default back action does not run; returning false lets it run. With React Navigation, register the listener inside useFocusEffect, so it is active only while this screen is focused, and remove the subscription in the cleanup, or the discard dialog will pop up on other screens too. Keep the check cheap and show a confirmation instead of silently blocking. iOS has no hardware back button, so cover the header back button and swipe gesture there as well.

Example
useFocusEffect(
  useCallback(() => {
    const onBack = () => {
      if (!hasUnsavedChanges) return false;
      Alert.alert('Discard changes?', '', [
        { text: 'Stay', style: 'cancel' },
        { text: 'Discard', onPress: () => navigation.goBack() },
      ]);
      return true;
    };
    const sub = BackHandler.addEventListener(
      'hardwareBackPress',
      onBack,
    );
    return () => sub.remove();
  }, [hasUnsavedChanges, navigation]),
);

35. How do runtime permissions differ between Android and iOS, for example for the camera?

Both need a declaration and a runtime request. Android lists the permission in AndroidManifest.xml and asks at runtime for dangerous ones such as CAMERA, and from Android 13 also POST_NOTIFICATIONS. iOS needs a purpose string such as NSCameraUsageDescription in Info.plist; without it the app is terminated the moment it accesses the camera. Ask in context, just before the user scans a KYC document, not at launch. Handle the blocked state: after a permanent denial you cannot ask again, so explain why and offer Linking.openSettings().

Example
async function ensureCamera() {
  if (Platform.OS !== 'android') return true; // iOS asks on first use
  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.CAMERA,
    {
      title: 'Camera access',
      message: 'We need the camera to scan your PAN card.',
      buttonPositive: 'Allow',
    },
  );
  if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
    Linking.openSettings();
  }
  return result === PermissionsAndroid.RESULTS.GRANTED;
}

36. What does useNativeDriver: true do in the Animated API, and what are its limits?

Without the native driver, every frame of an Animated animation is calculated on the JavaScript thread and sent to native, so when JavaScript is busy, say parsing a large API response, the animation stutters. With useNativeDriver: true, React Native sends the whole animation description to native before it starts, and the UI thread runs it frame by frame without JavaScript. The limit is that only non-layout properties are supported: transform and opacity work, but width, height, margin or top do not. Animate scale and translate instead of size.

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

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

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

37. How do shared values and useAnimatedStyle work in Reanimated?

A shared value from useSharedValue holds animation state that both the JavaScript thread and the UI thread can read and write. useAnimatedStyle takes a small function, a worklet, that runs on the UI thread and turns shared values into styles. When you assign withSpring or withTiming to .value, the animation runs on the UI thread without re-rendering React. Common mistakes are reading .value during render, which the docs warn against because the component does not re-render when it changes, and keeping animation state in useState.

Example
const scale = useSharedValue(1);

const heartStyle = useAnimatedStyle(() => ({
  transform: [{ scale: scale.value }],
}));

const onLike = () => {
  scale.value = withSequence(withSpring(1.3), withSpring(1));
};

<Animated.View style={[styles.heart, heartStyle]} />

38. The app works in debug mode but crashes at launch in the release build from the Play Store. How do you investigate?

Reproduce it locally with a release build on a real device, then read the native log with adb logcat; most launch crashes leave a Java or native stack trace there. Common causes are R8 or ProGuard removing classes that a library loads through reflection (fix with keep rules), a missing API URL or key in the release config, code that only works because of dev-only checks, and a JavaScript error that release builds report as a fatal exception. Use a crash reporter and upload source maps so JS stack traces point to your own code.

39. What are the steps and gotchas in shipping an Android release build of a React Native app?

Create an upload keystore once and keep it and its passwords out of git, in gradle.properties on the build machine or in CI secrets, then point the release signingConfig at them. Build an Android App Bundle with ./gradlew bundleRelease, because Google Play requires AAB for new apps, using Play App Signing. Increase versionCode on every upload; versionName is what users see. Test the release build before uploading, then use the internal testing track and a staged rollout. Back up the keystore: a lost upload key means a reset request in Play Console.

Example
android {
    defaultConfig {
        versionCode 42
        versionName "2.3.0"
    }
    signingConfigs {
        release {
            storeFile file(MYAPP_UPLOAD_STORE_FILE)
            storePassword MYAPP_UPLOAD_STORE_PASSWORD
            keyAlias MYAPP_UPLOAD_KEY_ALIAS
            keyPassword MYAPP_UPLOAD_KEY_PASSWORD
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

40. In an Expo app you need a library with custom native code that Expo Go does not include. What do you do?

Expo Go is a fixed app with a set of native modules built in, so it cannot load new native code. Switch to a development build: install expo-dev-client, add the library with npx expo install so its version matches your SDK, and add its config plugin in app.json if it has one. Then build the app locally with npx expo run:android or in the cloud with EAS Build, and keep using Metro and Fast Refresh as before. Native changes need a new build, while JavaScript changes do not.

Ready to check yourself? Take the free React Native mock test

React Native interview questions: Hard questions (experienced)

20 questions

41. Explain how the old React Native architecture worked, and why the bridge became a bottleneck.

In the old architecture JavaScript ran on its own thread, layout was calculated with Yoga on a background shadow thread, and native views were created on the main thread. JavaScript and native talked only through the bridge: messages were serialised to JSON, batched and delivered asynchronously. So every call paid for serialisation, synchronous calls were impossible, and the two sides could drift apart: during a fast scroll, JS-driven updates arrived late and lists showed blank areas. Native modules were also typically initialised at startup, used or not.

42. What is JSI, and what does it make possible?

JSI, the JavaScript Interface, is a C++ API that lets the JavaScript engine and native code hold references to each other. Native code can expose a host object to JavaScript, and JavaScript calls its methods directly, even synchronously, without converting arguments to JSON. It does not depend on one engine, so it works with Hermes as well as others. JSI is the base of TurboModules and Fabric, and libraries such as Reanimated and MMKV use it. The trade-off: a synchronous call blocks the JS thread, so slow native work must stay asynchronous.

43. How does a TurboModule differ from a legacy native module, and what does Codegen do?

A TurboModule is the New Architecture's native module. You describe its interface in a typed TypeScript or Flow spec, and Codegen generates the C++ and platform glue from it, so the native code and the JavaScript callers share one contract and a mismatch shows up at build or type-check time, not as a crash in production. Calls go through JSI, so a cheap method can be synchronous, and modules load lazily when first used. Codegen finds specs by file name, so the file must start with Native, as in NativeBattery.ts.

Example
// NativeBattery.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getBatteryLevel(): Promise<number>;
  isPowerSaveOn(): boolean;
}

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

44. What does the Fabric renderer change for app developers?

Fabric is the New Architecture renderer. The shadow tree and layout logic live in shared C++ instead of separate Java and Objective-C code, and it talks to JavaScript through JSI. Two practical results: layout can be read synchronously, so a tooltip measured in useLayoutEffect is placed before the user sees it jump, and React's concurrent features work, such as startTransition, Suspense and automatic batching. Rendering behaves the same on both platforms. Old components keep working through an interop layer, but libraries should ship Fabric components.

45. You lead the upgrade of a three-year-old app to the New Architecture. How do you plan it?

First upgrade React Native version by version with the Upgrade Helper, since 0.76 and later turn the New Architecture on by default and the old one is being phased out. Audit every native dependency: check React Native Directory for New Architecture support, then upgrade or replace abandoned packages. The interop layer keeps many legacy modules and views working, but test each one. Rewrite in-house native modules as TurboModules with Codegen specs. Run full regression on low-end Android and older iPhones, compare startup, memory and crash rates, and roll out in stages.

46. What is Hermes, and why does it improve startup time?

Hermes is a JavaScript engine built for React Native and the default engine since 0.70. Its key idea is compiling JavaScript to bytecode at build time, so the phone does not parse and compile the bundle at launch; it loads bytecode and starts running, which shortens time to interactive, especially on low-end Android. The docs also report lower memory use and a smaller app than JavaScriptCore for many apps. It supports CPU profiles and heap snapshots for debugging. A few rarely used JavaScript features are not supported, so check libraries that rely on them.

47. Cold start on low-end Android phones takes 5 seconds. How do you find and fix the cause?

Measure first on a real low-end phone with a release build: time from tap to the first useful screen, plus a CPU profile of startup. Then cut work before the first render. Typical wins: load heavy screens and modules lazily so their code is evaluated only when first needed, defer analytics, chat and remote config until the home screen shows, avoid reading and parsing large stored JSON at launch, initialise fewer native SDKs up front, and confirm Hermes is on. Keep the splash screen until the first screen is ready, and re-measure after each change.

Example
// Evaluated only when the user shares an invoice
let pdfModule = null;

function getPdfModule() {
  if (pdfModule === null) {
    pdfModule = require('./invoicePdf');
  }
  return pdfModule;
}

export async function shareInvoice(order) {
  const { buildPdf } = getPdfModule();
  return buildPdf(order);
}

48. Your Android app has grown to 60 MB. How do you reduce its size?

Measure before changing anything: open the APK or bundle in Android Studio's APK Analyzer to see whether native libraries, assets or the JS bundle dominate. Ship an Android App Bundle so Play delivers only the CPU architecture and screen density each phone needs. Turn on R8 with minifyEnabled and shrinkResources for release builds, then test well, because missing keep rules break libraries. Remove unused dependencies, since each native library adds .so files, convert big images to WebP and drop unused fonts. On iOS, read Xcode's App Thinning Size Report.

Example
// android/app/build.gradle
def enableProguardInReleaseBuilds = true

android {
    buildTypes {
        release {
            minifyEnabled enableProguardInReleaseBuilds
            shrinkResources enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }
}

49. How would you build a swipe-down-to-close bottom sheet that stays smooth at 60 fps?

Keep the whole gesture on the UI thread. With react-native-gesture-handler and Reanimated installed, gesture callbacks run as worklets: onUpdate writes the drag distance into a shared value and useAnimatedStyle maps it to translateY, so no frame waits for JavaScript or a React render. On release, decide using the distance or velocity, animate with withSpring or withTiming, and use runOnJS only to call the JavaScript close handler. Animate transform, not height. This is Reanimated 3 syntax; Reanimated 4 needs the New Architecture.

Example
const translateY = useSharedValue(0);

const pan = Gesture.Pan()
  .onUpdate(e => {
    translateY.value = Math.max(0, e.translationY);
  })
  .onEnd(e => {
    if (e.translationY > 150 || e.velocityY > 1000) {
      translateY.value = withTiming(SHEET_HEIGHT);
      runOnJS(onClose)();
    } else {
      translateY.value = withSpring(0);
    }
  });

const sheetStyle = useAnimatedStyle(() => ({
  transform: [{ translateY: translateY.value }],
}));

<GestureDetector gesture={pan}>
  <Animated.View style={[styles.sheet, sheetStyle]} />
</GestureDetector>

50. In a large app, one store update re-renders half the screens. How do you design state to prevent this?

Make each component subscribe to the smallest piece it needs. In React Redux, useSelector re-renders when the selected value is not strictly equal to the last one, so a selector that builds a new object re-renders on every dispatched action; select primitives, pass shallowEqual, or use memoised selectors from createSelector. Keep server data in a query cache, keep fast-changing values such as form text out of the global store, and normalise entities by id so updating one order does not change every list. Verify with the React Profiler.

Example
// Re-renders after every action: a new object each time
const { name, city } = useSelector(state => ({
  name: state.user.name,
  city: state.user.city,
}));

// Re-renders only when these values change
const name = useSelector(state => state.user.name);
const city = useSelector(state => state.user.city);

51. How is FlashList different from FlatList, and what bug can recycling cause?

FlatList virtualises by mounting rows as they come into view and unmounting them later, which costs JavaScript work during fast scrolls and can show blank areas. FlashList, from Shopify, recycles: rows that scroll off are reused for new data, like RecyclerView on Android and UICollectionView on iOS, so far fewer components are created. The catch is that local state inside a row survives recycling, so an expanded row or a liked heart can appear on another item; keep such state outside the row, keyed by id. Use getItemType for mixed layouts.

Example
<FlashList
  data={feed}
  keyExtractor={item => item.id}
  getItemType={item => item.kind}
  extraData={likedIds}
  renderItem={({ item }) =>
    item.kind === 'banner'
      ? <BannerCard banner={item} />
      : <PostCard post={item} liked={likedIds.has(item.id)} />
  }
/>

52. How do you tell whether jank comes from the JavaScript thread or the UI thread, and what do you do in each case?

Turn on the Perf Monitor from the Dev Menu; it shows JS and UI frame rates separately. If the JS rate drops while the UI rate holds, JavaScript is busy: use the React Profiler to find wasted re-renders and a Hermes CPU profile to find heavy functions, then memoise, split or defer work. If the UI rate drops, the native side struggles: too many views, large images, heavy shadows or overdraw, so use Android Studio's profiler or Xcode Instruments. Dev numbers are only a guide; confirm on a release build, and strip console.log from release.

53. Users say the app gets slower and finally crashes after browsing products for a while. How do you hunt the memory leak?

Reproduce it with a repeatable path, such as opening and closing a product screen 20 times, and watch memory in Android Studio's Memory Profiler or Xcode Instruments. For JavaScript, take Hermes heap snapshots before and after and compare what keeps growing. Usual causes are listeners or intervals never removed, a module-level cache that only grows, closures held by native callbacks, and full-size images decoded for thumbnails. Fix each cause, repeat the same path and confirm memory settles at a steady level.

Example
// Leak 1: module-level array that only grows
const viewedProducts = [];

function ProductScreen({ product }) {
  viewedProducts.push(product); // whole objects, never removed

  useEffect(() => {
    const unsubscribe = priceFeed.subscribe(product.id, setPrice);
    // Leak 2: missing return unsubscribe;
  }, [product.id]);
  // ...
}

54. When should you write your own native module, and how do you design its API?

Only when no maintained library covers the need, for example a bank's payment SDK or a Bluetooth billing printer used by a Jaipur retail chain. Keep the API coarse: one call such as printReceipt(order) instead of many small calls, because every crossing has a cost. Return Promises for anything slow and do the work on a background thread, never the main thread. Use events for streams such as scan results, release resources when the app goes to the background, and write the interface as a typed Codegen spec so both sides share one contract.

55. Design offline support for a field sales app used by distributor staff in areas with a patchy network.

Make a local database the source of truth: screens read from SQLite or a layer such as WatermelonDB, so they work with no signal. Each change, such as a new order, is saved locally and added to an outbox with a client-generated id, and a sync job sends the outbox when the network returns, retrying with backoff. The client id makes retries idempotent on the server, so an order is never created twice. Pull changes since the last sync cursor, pick a conflict rule per entity, show sync status, and test in airplane mode.

56. An app with deep stacks and many tabs feels slow and uses a lot of memory. How do you optimise navigation?

Use the native stack, which uses UINavigationController on iOS and Fragments on Android through react-native-screens, so transitions and gestures run natively. Screens below the top of a stack stay mounted and keep re-rendering when shared state changes; the freezeOnBlur option stops inactive screens from rendering. Keep tabs lazy, which is the default for bottom tabs, and avoid heavy work in focus listeners. Do not let users build endless stacks by pushing the same screens again and again; pop back or reset the stack instead.

57. What is Continuous Native Generation in Expo, and when would a team still prefer a bare project?

With Continuous Native Generation, the android and ios folders are generated by npx expo prebuild from app.json or app.config.js plus config plugins, instead of being edited by hand. Native settings such as permissions, URL schemes and SDK keys live in config, so an upgrade means regenerating instead of merging native diffs. A team may prefer a bare project when it has a lot of hand-written native code, an unusual build setup, or is adding React Native to an existing native app. Each Expo SDK release targets one React Native version.

Example
// app.config.js
export default {
  name: 'Kirana Ledger',
  slug: 'kirana-ledger',
  ios: { bundleIdentifier: 'in.example.kiranaledger' },
  android: { package: 'in.example.kiranaledger' },
  plugins: [
    ['expo-camera', {
      cameraPermission: 'Allow Kirana Ledger to scan bills.',
    }],
  ],
};

58. How do over-the-air updates work in React Native, and what are their limits?

An OTA service such as EAS Update ships a new JavaScript bundle and assets; the app checks for an update and applies it, typically on the next launch, so a JS bug fix reaches users without waiting for store review. It cannot change native code, so a new native module or permission needs a store build. Each update must target a matching runtime version, or the bundle may call native code that is not there and crash. Use staged rollouts and quick rollbacks, and follow store rules. App Center, which hosted CodePush, was retired in March 2025.

59. Design a CI/CD pipeline for a React Native app that releases every week.

On every pull request run lint, type-check and unit tests and build the JS bundle, so errors show up early. For a release, the pipeline bumps versionCode and the iOS build number and builds a signed AAB and IPA with Fastlane or EAS Build, with keystores and certificates kept as CI secrets, never in git. It uploads source maps to the crash reporter, ships to the Play internal track and TestFlight, and after QA promotes to a staged rollout watched against crash-free rates. Tag every release in git so each build can be traced.

Example
# fastlane/Fastfile
platform :ios do
  lane :beta do
    increment_build_number(xcodeproj: "ios/App.xcodeproj")
    build_app(workspace: "ios/App.xcworkspace", scheme: "App")
    upload_to_testflight
  end
end

60. What do you check in a security review of a React Native fintech app?

Assume the app will be unpacked: the JavaScript bundle and Hermes bytecode can be extracted and read, so no API secrets or hidden business rules belong in it. Tokens go in the Keychain or Keystore, never AsyncStorage, and logs with personal data are stripped from release builds. Use HTTPS only, consider certificate pinning, and validate every amount and permission on the server, not in the app. Hide sensitive screens from screenshots and the app switcher, for example with FLAG_SECURE on Android. Root and jailbreak checks only slow attackers down.

Practice questions for November 2026

New every month

3 questions from the React Native mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.

  1. FresherPlatform-specific Code

    Which of these APIs works only on Android?

    • Alert
    • ToastAndroid
    • AccessibilityInfo
    • Linking
    Show the answer

    Answer: ToastAndroid. ToastAndroid shows a native Android toast and has no iOS version, as its name says. Alert, Linking and AccessibilityInfo work on both platforms. For a toast-like message on iOS you need your own component or a library.

  2. Intermediate (1-5 years)Styling and Flexbox

    How tall is the last child?

    <View style={{ height: 600, padding: 20 }}>
        <View style={{ height: 80 }} />
        <View style={{ flex: 1 }} />
        <View style={{ flex: 3 }} />
    </View>
    • 360
    • 450
    • 420
    • 390
    Show the answer

    Answer: 360. height includes padding, so the content area is 600 - 40 = 560. The fixed child takes 80, leaving 480 to share in the ratio 1:3, so the last child gets 480 x 3 / 4 = 360.

  3. Experienced (5+ years)Build, Release and Expo

    Which gradle.properties setting limits the CPU architectures built in a React Native Android app, often to speed up local builds?

    • hermesEnabled
    • newArchEnabled
    • reactNativeArchitectures
    • abiFiltersEnabled
    Show the answer

    Answer: reactNativeArchitectures. reactNativeArchitectures lists ABIs such as arm64-v8a and x86_64, and building only your test device ABI cuts native build time. hermesEnabled and newArchEnabled switch the engine and the New Architecture.

Take the full React Native mock test

Ready to apply?

There are 69 open jobs asking for React Native on CoreHRx right now. Create a free profile once and apply with it.