React Native · 13 min
Senior React Native interview questions and follow-ups
Ten senior React Native interview questions on migration, startup, retries, old app versions, feature flags and native rewrites, with the follow-up to each.
React Native · 14 min read
Twelve React Native interview questions on rendering, lists, effects, the New Architecture, Hermes and storage, with what is scored, the trap and the follow-up.
This page is for React Native engineers with a technical round coming up, at mid level or reaching for senior. You get twelve React Native interview questions I would ask at that level, what each one scores, the answer that falls short, the one that holds, and the follow-up that decides it. The pattern is the same one I see in every mobile loop from the hiring side: the definition is the entry fee. The score comes from the second question, where you have to say what it costs and how you would know.
Most candidates can define useEffect, FlatList and the New Architecture. The round separates on three things:
| Signal | What it sounds like |
|---|---|
| Cause and effect | “It re-renders because the parent passes a new object every time.” |
| The boundary | “That work is on the JavaScript thread; this part is native.” |
| Evidence | “I would check it in the profiler on a release build before changing anything.” |
An answer that stops at the definition is a junior answer, however many years are on the CV. The questions below are grouped the way a React Native round is usually run: rendering, lists and data, effects, the native boundary, and storage.
What is scored: whether you separate running the function from changing the screen.
Weak answer: “The screen redraws, which is why you should avoid re-renders.”
Strong answer: “React calls the component function again and gets a new description of the tree. It compares that with the previous one and commits only what changed to the native views. So a re-render is JavaScript work first. It only turns into native work when the output differs, or when the render itself does something heavy, like formatting a large list inline.”
The follow-up: “So is a parent re-rendering its children a problem?” Not by itself. Children run again by default, and that is usually cheap. It becomes a problem when the profiler shows an expensive child running for no reason, and then the fix is specific: move state down, stop creating a new object prop every render, or memoise that one child.
useMemo and useCallback?”Strong answer: “useMemo keeps a computed value between renders and recomputes it when a dependency changes. useCallback keeps the same function reference. Neither changes what the code does; they exist for cost and for reference stability. A stable function only matters if something compares it: a child wrapped in React.memo, or an effect that lists it as a dependency.”
The trap: wrapping every handler in useCallback. If nothing downstream compares the reference, you have added bookkeeping and a dependency list to maintain, and gained nothing. Say you would measure first, then memoise the one thing the profiler points at.
What is scored: more than one concrete cause.
Strong answer: “Four come up again and again. State kept higher than it needs to be, so one keystroke re-renders a whole screen. Object or array literals created inline and passed as props. New function identities passed into memoised children. And a Context provider whose value is a new object on every render, which wakes every consumer. I find which one it is with the React DevTools profiler, not by guessing.”
The follow-up: “The team says React Native is just slow.” That is usually a state placement or prop stability problem in the app. Put the profile on the table before anyone concludes the framework is the bottleneck.
FlatList stutters when you scroll. What do you do?”Weak answer: “Tune windowSize and maxToRenderPerBatch.”
Strong answer: “First I make each row cheap, because the props only decide how many rows are rendered, not what each one costs. Rows are memoised with stable props, the keyExtractor returns a real id, heavy work like image decoding or date formatting is not done during render, and if every row has a fixed height I give getItemLayout so the list can skip measuring. Only then do I tune the windowing props, and I check each change on a real, mid-range Android device in a release build.”
The follow-up: “It only stutters on Android.” Then compare the JavaScript and UI thread frame rates in the performance monitor. A drop on the JS side is your render work; a drop on the UI side is layout, images or native views. They have different fixes, and saying so is the senior signal.
What is scored: that you know cancellation alone does not fix ordering.
Strong answer: “Two parts. I cancel the request that is no longer wanted with an AbortController, when the term changes or the screen leaves. But a response can already be on its way, so I also tag each request with its query or a sequence number and drop any result that no longer matches the current one. A server-state library does both for you, as long as the query key really contains every input.”
The trap: treating unmount cleanup as the whole answer. The bug users report is the slow response for ‘reac’ landing after the fast one for ‘react’, and it happens while the screen is still mounted.
Strong answer: “The server cursor is the page’s identity, not the length of my array. I allow one request per cursor, remember which cursors have completed, and merge results by item id. onEndReached can fire more than once, so I treat it as a hint to ask for the next cursor, not as proof that a new page exists. Refresh and load-more keep separate state, and I stop when the server returns no next cursor.”
The follow-up: “An item is deleted while the user scrolls.” With an offset derived from the array length, every later page shifts by one and you either skip or repeat a row. With cursors and id-based merging it does not matter.
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);What is scored: closure semantics, and not silencing the linter.
Strong answer: “The effect ran once, in the first render, so the callback captured count as 0 and keeps computing 0 plus 1. The fix here is the functional update, setCount(c => c + 1), which does not read the captured value. In general I fix the ownership: list the real dependencies, use a functional update, or move event logic out of the effect.”
The trap: adding // eslint-disable-next-line to stop the effect from re-running. That hides the bug and leaves the next reader a worse one. The better question to ask yourself is whether this code synchronises with something outside React, reacts to an event, or derives a value. Only the first belongs in an effect.
What is scored: that you know the current architecture, not the one from old blog posts.
Weak answer: “JavaScript sends JSON messages over the bridge to native.”
Strong answer: “That was the old model: asynchronous, batched, serialised messages, with no shared memory. It explains a lot of older advice, like batching calls across the bridge. Current React Native runs on the New Architecture. Since 0.82 it is the only architecture you can run, and later releases keep removing the legacy code. JavaScript talks to native through JSI, a C++ interface that lets it hold references to native objects and call them directly, synchronously where that makes sense.”
The follow-up: “So is crossing to native free now?” No. JSI removes the mandatory serialisation hop, not the cost of the work. Large buffers can still be copied, a synchronous native call blocks the JavaScript thread while it runs, and native work on the main thread still drops frames. A chatty API with hundreds of tiny calls is still slow as a TurboModule. Design one coarse, typed call around what the screen needs.
Strong answer: “They are the two pillars of the New Architecture, both built on JSI. Fabric is the renderer: a mostly shared C++ core that builds and commits the native view tree, with synchronous layout measurement when it is needed. TurboModules replace the old native modules: typed, generated from a spec by codegen, and loaded lazily when first used instead of all at startup. The typed spec is a real benefit on a team, because an interface mismatch fails at build time, not in a user’s session.”
The trap: “The New Architecture makes the app fast.” It removes overhead and raises the ceiling. A screen that renders too much, or blocks the main thread in native code, is exactly as slow as before.
Strong answer: “Hermes is the default JavaScript engine, built for mobile. The main idea is that JavaScript is compiled to bytecode at build time, so the app does not parse and compile source on every launch, and its garbage collector is tuned for phone memory. Since 0.84 the default is Hermes V1. The effect is largest on low-end devices and large bundles, where parsing was a big share of startup.”
The follow-up: “Startup is still slow with Hermes.” Then Hermes has done its one job, removing parse and compile time, and the rest is what the app does before the first screen: eager SDK setup, a large synchronous config read, a first screen that waits for three requests. Instrument the startup milestones and find out which.
What is scored: matching the store to the data, one decision per kind of data.
Strong answer: “Tokens go into the platform’s secure storage, the Keychain on iOS and the Keystore on Android, through a wrapper such as Expo SecureStore. Never AsyncStorage, which is not built to protect secrets. Access tokens stay short-lived and refresh tokens rotate if the backend supports it, and neither ever goes into logs or crash reports. AsyncStorage is right for small, unstructured things: preferences, a flag, a bit of config. The order history is relational, queried, filtered and growing, so it goes into SQLite or a proper embedded database, where I can also keep a sync state column per row.”
The trap: keeping a large list in AsyncStorage and filtering it in JavaScript after loading all of it. It works in the demo and gets slower with every order.
Strong answer: “Everything in memory is gone: stores, refs, timers, pending promises. And I cannot rely on a last callback, because neither platform promises that JavaScript gets to run before the process disappears. So I do not save on an AppState change or on unmount. I persist the draft as it changes, or at clear checkpoints: a draft id, the committed fields, where the user was. On cold start I load it, check it still belongs to this user and this schema version, and fetch the server-owned data again.”
The follow-up: “How do you test it?” As its own case, separate from a warm resume from the background. Background the app, kill the process from the developer tools or the command line, reopen it, and check what the user sees. Most apps that pass the resume test fail this one.
Prompt: “Our order list screen feels slow on Android. What would you do?”
Strong answer: “First I pin down what slow means: the time to first rows, the scroll, or a tap that responds late. Then I reproduce it on a release build on a mid-range Android device, because a debug build is slower in ways that mislead. I watch the JS and UI frame rates while scrolling. If JS is dropping, I profile renders and usually find rows re-rendering because of an inline object prop, or a date formatted in every row. If the UI thread is dropping, I look at image sizes and view depth.”
Push: “The rows are memoised and it still drops frames.” Then check whether the props are stable. An onPress={() => open(item.id)} defined inline makes every memoised row re-render. Pass the id and a stable handler.
Push: “It is fine on iPhone.” That points at the device class, not at React Native. Test on the phones your users have, and put the frame numbers in the ticket.
Push: “Should we rewrite this screen in native?” Not on an impression. Only after the known fixes are in and the measurements still show a ceiling React Native cannot reach. That decision, with the evidence it needs, is on senior React Native interview questions.
| Push | Example | A strong answer |
|---|---|---|
| Make it absolute | “So JSI makes every call synchronous?” | Narrows it: synchronous where it helps, async work stays async |
| Change the device | “Now on a three-year-old Android phone.” | Names the release build, the frame monitor and the device |
| Break it | “The response arrives after the user left.” | Cancels, then ignores stale results by key |
| Kill it | “The process dies mid-form.” | Persists as it goes, restores on cold start |
| Measure it | “How do you know it is faster?” | Before and after on the same device and build |
Answer each question out loud in under a minute and a half, then answer the follow-up without reading it. Where your answer defines the API but does not say what it costs or how you would check, that is the one to rework. If your loop is at senior level or above, continue with senior React Native interview questions. The platform overview is on React Native interview prep.
newArchEnabled=false on Android and RCT_NEW_ARCH_ENABLED=0 on iOS are ignored (React Native 0.82).
React Native · 13 min
Ten senior React Native interview questions on migration, startup, retries, old app versions, feature flags and native rewrites, with the follow-up to each.

Senior and Staff loops · 11 min
Architecture interview questions that do not depend on the platform: modules, shared code, server-driven UI, flags, old app versions and who owns what.

Mobile system design · 14 min
How to design offline-first sync in a mobile system design interview: local source of truth, the outbox, conflict rules, clocks and migration on device.