React Native Developer Interview Questions
Core Overview
Prepare for React Native Developer interviews covering React Native fundamentals, components, props, state, rendering, hooks, navigation, networking, native platform integration, the New Architecture, testing, performance, debugging, app lifecycle, and production mobile architecture.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What are components in React Native, and how does JSX ultimately become native mobile UI?
Direct Answer
React Native applications describe UI with React components and JSX, while React Native maps supported component trees to native platform views rather than rendering browser DOM elements.
Detailed Explanation
React Native uses the React component model to describe mobile user interfaces.
A component is a reusable unit that returns a description of UI.
For example:
`tsx
function Greeting() {
return (
<View>
<Text>Hello</Text>
</View>
);
}
View and Text are React Native components rather than browser elements such as div or span.
JSX
JSX is syntax used to describe the component tree. It is transformed into JavaScript representations consumed by React.
JSX itself is not a native Android or iOS view.
Core components
React Native provides components representing common mobile UI capabilities, including concepts such as:
ViewTextImageTextInputPressableScrollViewThese participate in React rendering while being backed by React Native platform implementations.
React Native is not browser React
React Native does not normally render HTML into a browser DOM.
Although React concepts such as components, props, state, hooks, and reconciliation are shared, the host environment and supported components differ.
Declarative rendering
The component describes what the UI should look like for the current props and state.
When those inputs change, React computes the required updates and React Native applies the corresponding host-platform changes.
Developers should avoid manually trying to keep every native element synchronized with application state when React can derive the interface declaratively.
The key interview distinction is that React Native shares React's programming model but targets native mobile platforms rather than the browser DOM.
Code Example
import React from 'react';
import {
Pressable,
Text,
View,
} from 'react-native';
type GreetingProps = {
name: string;
};
export function Greeting({
name,
}: GreetingProps) {
return (
<View>
<Text>
Hello {name}
</Text>
<Pressable
onPress={() => {
console.log(
'Pressed',
);
}}
>
<Text>
Continue
</Text>
</Pressable>
</View>
);
}Common Interview Pitfalls
- Assuming React Native renders normal browser DOM elements.
- Using div or span as though they were standard React Native components.
- Treating JSX itself as a native view hierarchy.
- Putting application logic into one enormous screen component.
- Manually synchronizing UI that could be derived declaratively from props and state.
- Assuming every React web component library automatically works in React Native.
What is the difference between props and state in React Native, and how does one-way data flow affect component design?
Direct Answer
Props are inputs supplied by a parent, while state represents data owned by a component or surrounding state owner; UI is derived from these inputs and events request state changes.
Detailed Explanation
React Native uses the same core React model for props and state.
Props
Props are inputs passed from a parent component.
`tsx
<UserCard name="Alex" />
The child receives name as a prop.
Components should treat props as read-only inputs rather than mutating them.
State
State represents information whose changes should affect rendering.
For example:
`tsx
const [count, setCount] =
useState(0);
Calling the state setter requests a render using the updated state.
One-way data flow
Data normally flows down the component tree through props.
When a child needs to request a change, the parent can provide an event callback:
`tsx
<SaveButton
onSave={handleSave}
/>
The child emits the event, while the appropriate owner decides how application state changes.
Do not duplicate derived state unnecessarily
If a value can be calculated from existing props or state during rendering, storing another synchronized copy can create inconsistencies.
For example, storing both firstName, lastName, and another mutable fullName value may require unnecessary synchronization when fullName can simply be derived.
Place state at the appropriate owner
State used by only one component can often remain local.
If multiple sibling components need to coordinate around the same value, the state may need to move to their closest meaningful shared owner.
Do not move every piece of local UI state into a global store simply because a global store exists.
Good React Native design keeps state ownership understandable and derives UI from authoritative state.
Code Example
import React, {
useState,
} from 'react';
import {
Pressable,
Text,
View,
} from 'react-native';
export function Counter() {
const [count, setCount] =
useState(0);
return (
<View>
<Text>
Count: {count}
</Text>
<Pressable
onPress={() => {
setCount(
current =>
current + 1,
);
}}
>
<Text>
Increment
</Text>
</Pressable>
</View>
);
}Common Interview Pitfalls
- Mutating props received from a parent component.
- Duplicating values in state that can be calculated directly from existing state or props.
- Moving every local UI value into global application state.
- Letting multiple unrelated components become independent owners of the same logical state.
- Expecting a state setter to mutate the current render variable synchronously.
- Using module-level mutable variables as a replacement for component state.
How do component identity and keys affect rendering and state preservation in React Native?
Direct Answer
React associates state with a component position and identity in the rendered tree; keys help distinguish sibling elements so React can preserve or reset state according to the intended identity.
Detailed Explanation
React Native uses React's reconciliation and component identity model.
State is associated with where a component appears in the rendered tree rather than being permanently attached to a particular JSX string.
Preserving state
If React sees the same component identity in the same logical position across renders, its state can be preserved.
If that identity changes, React may remove the previous component instance and create another one with fresh state.
Keys
Keys provide identity among siblings, especially when rendering collections.
`tsx
{users.map(user => (
<UserRow
key={user.id}
user={user}
/>
))}
A stable domain identifier is usually preferable to an array index when elements can be inserted, removed, or reordered.
Why index keys can cause problems
Suppose rows contain local state and the first item is removed.
If array indexes are used as identities, a component previously associated with one logical item can become associated with another position.
This may lead to unexpected state preservation or UI behavior.
Indexes are not inherently forbidden, but they are inappropriate when position does not represent stable identity.
Keys can intentionally reset state
Changing a key can tell React that something should be treated as a different component identity.
That can be useful when a form should reset for a completely different record.
Do not generate a new random key on every render; doing so destroys stable identity and can cause unnecessary remounting and lost state.
The important concept is that keys are about identity, not merely eliminating a warning.
Code Example
import React from 'react';
import {
Text,
View,
} from 'react-native';
type User = {
id: string;
name: string;
};
export function UserList({
users,
}: {
users: User[];
}) {
return (
<View>
{users.map(user => (
<Text
key={user.id}
>
{user.name}
</Text>
))}
</View>
);
}Common Interview Pitfalls
- Thinking keys exist only to remove a React warning.
- Using array indexes as keys for reorderable collections with meaningful item identity.
- Generating random keys on every render.
- Changing keys unintentionally and causing child state to reset.
- Assuming component state belongs permanently to the JSX source declaration.
- Reusing unstable identifiers that do not represent the logical item.
When should a React Native application use ScrollView versus FlatList for displaying collections?
Direct Answer
ScrollView renders its child content eagerly, while FlatList builds on virtualized list behavior intended to efficiently render large or changing collections without mounting every item simultaneously.
Detailed Explanation
ScrollView and FlatList can both display scrollable content, but they are designed for different collection characteristics.
ScrollView
A ScrollView renders its child component tree rather than virtualizing a potentially large dataset.
It is convenient for relatively small amounts of content where rendering all children is acceptable.
For a very large collection, eagerly creating every item can increase rendering and memory costs.
FlatList
FlatList provides a higher-level interface for efficiently rendering lists and builds on React Native's virtualized list infrastructure.
Typical usage includes:
`tsx
<FlatList
data={items}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<ItemRow item={item} />
)}
/>
List rendering can be limited to a window around content needed by the user rather than requiring every item to remain rendered simultaneously.
Stable identities still matter
Use meaningful keys through keyExtractor where appropriate.
Unstable identities can produce incorrect component reuse and unnecessary rendering behavior.
Pure rendering considerations
FlatList performance depends not only on the list itself but also on the work done by each row.
Expensive calculations, unstable props, large images, or unnecessary state changes can still create poor scrolling behavior.
Do not optimize list parameters blindly
React Native exposes configuration affecting list rendering windows and batching.
Changing those values involves tradeoffs among:
Tune using actual device behavior and representative datasets rather than copying arbitrary configuration values.
Choose ScrollView for modest bounded content and virtualized list components when collection size or rendering cost makes virtualization valuable.
Code Example
import React from 'react';
import {
FlatList,
Text,
} from 'react-native';
type Job = {
id: string;
title: string;
};
export function JobList({
jobs,
}: {
jobs: Job[];
}) {
return (
<FlatList
data={jobs}
keyExtractor={
job => job.id
}
renderItem={({
item,
}) => (
<Text>
{item.title}
</Text>
)}
/>
);
}Common Interview Pitfalls
- Rendering thousands of items inside a ScrollView without considering memory and rendering cost.
- Assuming FlatList automatically fixes expensive row components.
- Using unstable list keys.
- Changing virtualization parameters without measuring on representative devices.
- Performing expensive data transformations repeatedly inside renderItem.
- Assuming every tiny list needs virtualization.
How do styling, Flexbox layout, dimensions, and platform-specific behavior work in React Native?
Direct Answer
React Native uses JavaScript style objects and a Flexbox-based layout model, while platform-specific files and Platform APIs allow intentional Android and iOS differences where shared UI is insufficient.
Detailed Explanation
React Native styles are JavaScript objects rather than browser CSS stylesheets.
For example:
`tsx
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
});
Flexbox
React Native uses Flexbox concepts for layout, including:
flexDirectionjustifyContentalignItemsflexDevelopers coming from web development should not assume every default or CSS property behaves identically to the browser.
Avoid fixed-screen assumptions
Mobile interfaces run across different:
Hardcoding a layout for one device screenshot usually produces fragile behavior.
Prefer layouts based on available space and content requirements.
Platform-specific behavior
React Native allows conditional behavior with platform APIs and platform-specific source files such as conceptual .ios and .android variants where appropriate.
Use platform-specific implementations when the operating systems genuinely require different behavior or interaction patterns.
Do not fork the entire application unnecessarily for small differences.
Shared does not mean identical
A cross-platform product may share most application logic while intentionally respecting platform conventions for navigation, permissions, controls, or system integrations.
The goal is not forcing every pixel and interaction to behave identically across Android and iOS.
Measure layout assumptions on real devices
Emulators and simulators are useful, but meaningful UI verification should include representative physical devices and accessibility settings before production release.
Code Example
import React from 'react';
import {
Platform,
StyleSheet,
Text,
View,
} from 'react-native';
export function Header() {
return (
<View
style={
styles.container
}
>
<Text>
Jobs
</Text>
<Text>
{Platform.OS}
</Text>
</View>
);
}
const styles =
StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent:
'space-between',
padding: 16,
},
});Common Interview Pitfalls
- Assuming React Native styling is identical to browser CSS.
- Hardcoding layouts for one screen size.
- Ignoring dynamic text and accessibility sizing.
- Forcing identical platform behavior when Android and iOS conventions legitimately differ.
- Duplicating entire screens for tiny platform differences.
- Testing responsive layout only on one simulator configuration.
How would you design React Native component and rendering architecture for a large production mobile feature without creating excessive state coupling or unnecessary renders?
Direct Answer
Keep feature boundaries cohesive, colocate state with its real owner, derive rather than duplicate data, use stable identities, virtualize large collections, and optimize rendering only after profiling demonstrates meaningful cost.
Detailed Explanation
Production React Native rendering architecture starts with ownership and data flow rather than memoization everywhere.
1. Split components by meaningful responsibility
A large screen can contain separate responsibilities such as:
Do not create components solely to reduce line count, but avoid one screen owning every behavior and UI detail.
2. Keep state close to its true owner
Local presentation state should generally remain local.
State needed by multiple cooperating components can move to their nearest meaningful shared owner or an appropriate external state boundary.
Do not make global state the default solution for communication between two nearby components.
3. Avoid duplicate sources of truth
If filtered jobs can be calculated from jobs plus a filter, storing another independently mutable filtered-jobs array can create synchronization bugs.
Derive values when practical.
4. Understand render versus remount
A render does not necessarily mean every corresponding native element is destroyed and recreated.
React compares the resulting tree and applies required changes.
A remount caused by changed identity is a different lifecycle event and may reset local state.
5. Preserve stable list identity
Use IDs representing domain identity rather than random or position-derived keys when rows can move.
6. Virtualize large collections
Use FlatList or another appropriate virtualized list abstraction when dataset size and row cost justify it.
Do not render thousands of expensive rows in a plain ScrollView merely because implementation is simpler.
7. Optimize only the expensive path
Memoization tools can help when repeated work is genuinely expensive or stable identity matters for optimized children.
Do not add memoization automatically to every component, callback, and calculated value.
Memoization itself adds complexity and has a cost.
8. Avoid unnecessary state-driven fan-out
If frequently changing state is owned at the root of a very large subtree, many descendants may participate in rendering even when only a small area cares about that state.
Moving ownership closer to the consuming feature can reduce coupling and simplify reasoning.
9. Keep transport data separate from view requirements when useful
API response objects do not have to become permanent screen models.
Normalize or derive view-oriented data where doing so clarifies ownership, but do not create layers mechanically.
10. Respect mobile memory constraints
Large images, huge in-memory collections, and unbounded caches can damage application responsiveness even if component code itself is correct.
Rendering architecture and data lifecycle are connected.
11. Preserve platform behavior intentionally
Share behavior that is truly common while isolating platform-specific differences at clear boundaries.
Avoid spreading Platform.OS conditions throughout every feature when a dedicated platform implementation is clearer.
12. Profile before changing architecture for performance
If scrolling or interaction is slow, determine whether the actual problem is:
Do not infer the bottleneck from component size alone.
A strong production design keeps state ownership, identity, platform boundaries, and data lifetime understandable first, then applies targeted optimization to measured bottlenecks.
Code Example
import React, {
useMemo,
useState,
} from 'react';
import {
FlatList,
} from 'react-native';
type Job = {
id: string;
title: string;
};
export function JobsScreen({
jobs,
}: {
jobs: Job[];
}) {
const [query, setQuery] =
useState('');
const visibleJobs =
useMemo(
() => {
const normalized =
query
.trim()
.toLowerCase();
if (!normalized) {
return jobs;
}
return jobs.filter(
job =>
job.title
.toLowerCase()
.includes(
normalized,
),
);
},
[jobs, query],
);
return (
<FlatList
data={visibleJobs}
keyExtractor={
job => job.id
}
renderItem={({
item,
}) => (
<JobRow
job={item}
/>
)}
/>
);
}Common Interview Pitfalls
- Moving all feature state into a global store regardless of ownership.
- Duplicating derived data into independently mutable state.
- Treating every render as a complete native-view reconstruction.
- Using unstable list keys that cause incorrect identity or remounting.
- Adding memoization to every component without measuring a rendering problem.
- Owning rapidly changing local state at an unnecessarily high component boundary.
- Rendering large datasets eagerly without considering virtualization.
- Scattering platform conditionals throughout the feature instead of creating meaningful platform boundaries.
- Assuming JavaScript rendering is responsible for every mobile performance issue.
- Redesigning component architecture for performance without profiling representative devices.
How do useState and useEffect work in React Native, and when should an Effect be used?
Direct Answer
useState stores render-affecting component state, while useEffect synchronizes a component with external systems and can return cleanup logic for subscriptions or other external resources.
Detailed Explanation
useState and useEffect are React Hooks commonly used by React Native components.
useState
useState lets a component retain state between renders:
`tsx
const [query, setQuery] = useState("");
Calling the setter requests an update. Code should not mutate state objects or arrays directly and then expect React to detect that mutation reliably.
When the next state depends on the previous state, an updater function can make that dependency explicit:
`tsx
setCount(current => current + 1);
useEffect
An Effect is primarily for synchronizing a component with something outside React.
Examples include:
An Effect is generally not necessary merely to calculate a value from existing props or state.
For example, this usually does not require an Effect:
`tsx
const fullName = ${firstName} ${lastName};
Storing fullName separately and synchronizing it through an Effect creates an unnecessary second source of truth.
Dependencies
The dependency list describes reactive values used by the Effect whose changes require the synchronization to be performed again.
Do not remove dependencies merely to silence lint rules. Instead, restructure the code if the Effect is doing too many unrelated jobs.
Cleanup
An Effect can return a cleanup function:
`tsx
useEffect(() => {
const subscription = subscribe();
return () => {
subscription.remove();
};
}, []);
Cleanup is important for subscriptions, timers, listeners, and other resources whose lifetime should follow the Effect.
A good rule is: if there is no external system to synchronize with, first ask whether an Effect is needed at all.
Code Example
import React, {
useEffect,
useState,
} from 'react';
import {
AppState,
Text,
} from 'react-native';
export function Status() {
const [state, setState] =
useState(
AppState.currentState,
);
useEffect(() => {
const subscription =
AppState.addEventListener(
'change',
nextState => {
setState(nextState);
},
);
return () => {
subscription.remove();
};
}, []);
return (
<Text>
{state}
</Text>
);
}Common Interview Pitfalls
- Using an Effect to calculate values that could be derived directly during rendering.
- Mutating objects or arrays stored in React state directly.
- Removing Effect dependencies merely to silence lint warnings.
- Creating subscriptions without cleaning them up.
- Putting several unrelated synchronization responsibilities into one large Effect.
- Assuming useEffect is required whenever state changes.
- Expecting the current render state variable to change immediately after calling its setter.
How should controlled inputs, form state, and derived values be modeled in React Native?
Direct Answer
Controlled inputs derive their displayed value from React state and send changes back through callbacks; keep one authoritative source of truth and derive values instead of storing synchronized duplicates.
Detailed Explanation
React Native form components such as TextInput can be controlled by React state.
For example:
`tsx
const [name, setName] = useState("");
<TextInput
value={name}
onChangeText={setName}
/>
The state owns the current value, while user input requests updates through the callback.
Single source of truth
Avoid maintaining multiple independently mutable versions of the same information.
If a value can be calculated from authoritative state, derive it during rendering when practical.
For example:
`tsx
const normalizedEmail =
email.trim().toLowerCase();
usually does not need another state variable plus an Effect to keep it synchronized.
Local versus lifted state
If one component owns and uses a form field, local state can be appropriate.
If several components need to coordinate the same value, move that state to their nearest meaningful shared owner.
Do not lift state higher than necessary because doing so increases coupling and can expand the rendering scope affected by every update.
Draft state versus committed state
A form often needs temporary editable state before the user submits it.
The editable draft does not have to mutate the application's canonical stored object on every keystroke.
This distinction can make cancellation, validation, and reset behavior easier to reason about.
Validation
Validation can occur at different moments depending on product requirements, such as while editing, on blur, or on submission.
Do not confuse the UI representation of a validation error with the underlying validation rule itself.
Keep the authoritative state owner and transition rules explicit.
Code Example
import React, {
useState,
} from 'react';
import {
Button,
Text,
TextInput,
View,
} from 'react-native';
export function ProfileForm() {
const [name, setName] =
useState('');
const trimmedName =
name.trim();
const valid =
trimmedName.length > 0;
return (
<View>
<TextInput
value={name}
onChangeText={setName}
/>
{!valid && (
<Text>
Name is required
</Text>
)}
<Button
title="Save"
disabled={!valid}
onPress={() => {
saveProfile(
trimmedName,
);
}}
/>
</View>
);
}Common Interview Pitfalls
- Maintaining several mutable copies of the same logical form value.
- Using an Effect to synchronize simple derived form values.
- Lifting every TextInput value to the root application component.
- Mutating a canonical domain object directly while the user is still editing a cancelable draft.
- Mixing validation rules tightly with presentation-specific error rendering.
- Assuming every form field needs global state.
- Resetting form state unintentionally because component identity changes.
When should a React Native feature use Context and useReducer, and what problems do they solve?
Direct Answer
Context distributes shared values through a component subtree, while useReducer centralizes related state transitions; together they can manage complex feature state without making all application state global.
Detailed Explanation
useReducer and Context address different concerns and can be combined when a feature becomes difficult to manage with scattered state updates.
useReducer
A reducer receives the current state and an action and returns the next state.
`tsx
function reducer(
state: State,
action: Action,
): State {
switch (action.type) {
case "queryChanged":
return {
...state,
query: action.query,
};
}
}
Reducers are useful when several transitions affect related state and the transition rules benefit from being centralized.
The reducer should remain pure: do not perform network requests, navigation commands, timers, or other side effects directly inside the reducer.
Context
Context allows components below a provider to read shared values without forwarding the same prop manually through every intermediate component.
Context does not automatically define the correct state architecture.
Putting an object into Context does not mean every part of the application should depend on it.
Reducer + Context
A feature can provide reducer state and dispatch through Context when many related descendants require them.
This can be appropriate for a cohesive feature such as an onboarding flow or editor.
It does not imply that one enormous application-wide Context should contain every unrelated piece of state.
Rendering considerations
Components consuming a context value can participate in updates when that provider value changes.
Large frequently changing context values can therefore create unnecessary coupling.
Split providers according to meaningful ownership rather than creating arbitrary fragmentation purely for optimization.
Use reducer and Context when they clarify transition and ownership semantics, not simply because the application has multiple screens.
Code Example
import React, {
createContext,
useContext,
useReducer,
} from 'react';
type State = {
query: string;
};
type Action = {
type: 'queryChanged';
query: string;
};
function reducer(
state: State,
action: Action,
): State {
switch (action.type) {
case 'queryChanged':
return {
...state,
query: action.query,
};
}
}
const SearchContext =
createContext<
| {
state: State;
dispatch:
React.Dispatch<Action>;
}
| undefined
>(undefined);
export function SearchProvider({
children,
}: {
children:
React.ReactNode;
}) {
const [state, dispatch] =
useReducer(
reducer,
{ query: '' },
);
return (
<SearchContext.Provider
value={{
state,
dispatch,
}}
>
{children}
</SearchContext.Provider>
);
}Common Interview Pitfalls
- Performing network requests or other side effects directly inside a reducer.
- Creating one global Context containing every unrelated piece of application state.
- Using Context when ordinary props through one or two levels would remain clearer.
- Mutating reducer state instead of returning the intended next state.
- Treating dispatch actions as arbitrary commands with hidden side effects.
- Ignoring how frequently changing Context values can broaden update coupling.
- Adding reducers to trivial independent boolean state with no meaningful transition complexity.
How should state and data ownership be designed across navigated screens in a React Native application?
Direct Answer
Navigation should identify where the user is and carry minimal navigation-relevant parameters, while authoritative domain data should live at the appropriate application or feature owner rather than being duplicated across screens.
Detailed Explanation
Navigation and application state are related but should not be treated as the same thing.
A navigation layer commonly needs enough information to identify concepts such as:
Pass identity rather than duplicated mutable objects when practical
Suppose a job details screen needs to display job job-123.
Passing the job identifier lets the destination read the authoritative record from the appropriate feature/data owner.
Copying a large mutable job object into navigation parameters and then separately mutating the canonical store can create stale duplicated state.
This is a design principle rather than a rule that route parameters may contain only IDs.
Small immutable values directly describing navigation intent can be appropriate.
Screen-local state
A screen can own temporary UI state such as:
That state should not become application-global merely because the screen participates in navigation.
State preservation depends on lifecycle and identity
Do not assume every screen necessarily remounts whenever the user moves elsewhere and returns.
Different navigation architectures can preserve mounted screens or recreate them.
Therefore critical data should not rely on undocumented assumptions about screen mount/unmount timing.
App lifecycle is different from screen navigation
React Native AppState describes whether the application is foregrounded/backgrounded at the application level.
Navigating from one screen to another is not necessarily equivalent to the entire application entering the background.
Do not use AppState as a substitute for screen navigation/focus semantics.
Persistence is another boundary
If state must survive process termination or application restart, keeping it only in a screen component is insufficient.
Choose state lifetime deliberately:
`text
render
→ component
→ screen/feature
→ application session
→ durable persistence
Use the narrowest lifetime that satisfies the product requirement.
Code Example
type JobRoute = {
jobId: string;
};
// Navigation-specific data:
//
// {
// jobId: 'job-123'
// }
//
// The details feature can use
// jobId to resolve authoritative
// job information from its data
// owner instead of maintaining a
// second mutable copy of the job.
Common Interview Pitfalls
- Treating navigation state as the storage location for all application data.
- Passing large mutable domain objects between screens and creating stale duplicate state.
- Moving temporary screen UI state into a global store without a lifecycle requirement.
- Assuming a screen always unmounts immediately when navigating away.
- Using AppState changes as though they represented screen focus changes.
- Keeping critical durable information only in component memory.
- Passing sensitive information through navigation state without considering exposure and lifecycle.
How should React Native components manage asynchronous data fetching without applying stale results or mixing server data with unrelated UI state?
Direct Answer
Model loading, success, empty, and failure states explicitly, prevent obsolete requests from committing stale results, and distinguish remote authoritative data from temporary local UI state.
Detailed Explanation
Asynchronous data introduces lifecycle and ordering problems that do not exist with simple synchronous component state.
Requests can finish out of order
Suppose a user searches for react, then immediately searches for swift.
If the first request finishes after the second request, blindly storing every response can overwrite the newer result with stale data.
The component needs a strategy ensuring obsolete work cannot commit an outdated result.
Depending on the API and architecture, this can involve:
Effect cleanup
When fetching directly from an Effect, cleanup can mark an Effect execution as obsolete so a late response is not applied.
Cleanup and actual network cancellation are related but not identical concepts.
Ignoring a stale result prevents incorrect UI state; canceling underlying work can additionally reduce unnecessary resource use when supported.
Model async state explicitly
A screen may need to distinguish:
A single data === undefined check often cannot represent every useful state clearly.
Server state versus UI state
Remote data has different semantics from temporary presentation state.
Remote data may involve:
while UI state might simply represent whether a modal is open.
Do not put both into one undifferentiated state object merely because they appear on the same screen.
Do not fetch through Effects mechanically
Effects can perform fetching, but larger applications may benefit from a dedicated framework/data layer that handles caching, deduplication, request ordering, and lifecycle consistently.
The key interview skill is recognizing the lifecycle problems, not memorizing one particular third-party library.
Code Example
import React, {
useEffect,
useState,
} from 'react';
export function useJob(
jobId: string,
) {
const [job, setJob] =
useState<Job | null>(
null,
);
useEffect(() => {
let ignore = false;
async function load() {
const result =
await fetchJob(
jobId,
);
if (!ignore) {
setJob(result);
}
}
void load();
return () => {
ignore = true;
};
}, [jobId]);
return job;
}Common Interview Pitfalls
- Allowing an older request response to overwrite newer screen data.
- Assuming Effect cleanup automatically cancels every underlying network request.
- Representing loading, refreshing, empty, and failure as one ambiguous state.
- Mixing remote cached data with unrelated temporary UI state in one large object.
- Starting duplicate requests from several components without considering ownership.
- Fetching data from Effects everywhere without considering caching or deduplication needs.
- Updating state after work becomes obsolete without checking request lifetime.
- Treating every remote response as permanently fresh.
How would you design state and data flow for a production React Native feature containing multiple screens, forms, remote data, navigation, and asynchronous operations?
Direct Answer
Classify state by owner and lifetime, keep one authoritative source for each value, separate server data from temporary UI state, centralize complex transitions where useful, and make async and navigation lifecycles explicit.
Detailed Explanation
Large React Native features become difficult when every piece of data is treated as the same kind of state.
A stronger design starts by classifying state according to ownership and lifetime.
1. Identify the authoritative owner
For every value ask:
Do not keep several mutable copies simply because several screens display the same information.
2. Keep ephemeral UI state local
Examples include:
These usually do not need application-global storage.
3. Distinguish form drafts from persisted models
A multi-screen editor may need a feature-owned draft that survives movement between its screens.
That does not mean every keystroke should immediately mutate the canonical persisted record.
The feature can define explicit transitions such as:
`text
load
→ edit draft
→ validate
→ submit
→ commit result
4. Separate remote data semantics
Server-owned data introduces caching, freshness, invalidation, refetching, and request ordering.
Do not duplicate a remote entity into several screen states and then attempt to synchronize them manually.
5. Use reducers when transitions become related
If state has meaningful transitions such as:
`text
idle
→ loading
→ loaded
→ submitting
→ completed
or several fields must change consistently for one action, a reducer can make those transitions explicit.
Do not use a reducer merely because the feature has many lines of code.
6. Use Context within meaningful ownership boundaries
A multi-screen feature may provide shared draft state to its descendants.
Avoid turning the application root into a universal store for every feature.
7. Navigation carries navigation intent
Prefer parameters identifying destinations or immutable navigation intent rather than using navigation as another mutable database.
Resolve authoritative entities through the appropriate data owner.
8. Do not equate navigation with lifecycle termination
A screen leaving the foreground of a navigation stack may remain mounted depending on the navigation architecture.
Do not depend on unmounting as the only way to save critical state or cancel important work.
9. Make async ownership explicit
When a query changes or a screen becomes irrelevant, determine what should happen to outstanding work.
Possible semantics include:
The correct choice depends on ownership.
10. Effects synchronize external systems
Do not build the entire application transition model as chains of Effects reacting to one another.
Event handlers and reducers should express direct state transitions where possible.
Effects should remain focused on synchronization with things outside React.
11. Keep durable state separate from memory-only state
Anything that must survive process termination needs an explicit persistence strategy.
Component state, Context, and reducers alone are memory-resident abstractions.
12. Avoid architecture by library name
The correct design is not automatically Redux, Context, Zustand, reducer, or local state.
First determine:
Then choose the smallest mechanism satisfying those constraints.
13. Control update scope
Frequently changing values placed too high in the tree can create unnecessary coupling.
Keep providers and shared state aligned with the feature that actually needs them.
14. Test transitions independently where possible
Pure reducer and domain transition rules can be tested without rendering complete navigation flows.
Use broader component tests for behavior that genuinely depends on multiple UI boundaries.
A production state architecture succeeds when developers can explain exactly where a value lives, who may change it, how long it survives, and what happens when asynchronous work completes late.
Code Example
type EditorState = {
status:
| 'idle'
| 'submitting'
| 'error';
draft: ProfileDraft;
errorMessage:
string | null;
};
type EditorAction =
| {
type: 'fieldChanged';
field:
keyof ProfileDraft;
value: string;
}
| {
type: 'submitStarted';
}
| {
type: 'submitFailed';
message: string;
};
function reducer(
state: EditorState,
action: EditorAction,
): EditorState {
switch (action.type) {
case 'fieldChanged':
return {
...state,
draft: {
...state.draft,
[action.field]:
action.value,
},
};
case 'submitStarted':
return {
...state,
status:
'submitting',
errorMessage: null,
};
case 'submitFailed':
return {
...state,
status: 'error',
errorMessage:
action.message,
};
}
}Common Interview Pitfalls
- Putting every screen and form value into one global state container.
- Keeping multiple mutable copies of the same remote entity across screens.
- Persisting every keystroke directly into the canonical model when the workflow requires a cancelable draft.
- Using chains of Effects to implement state transitions that should be explicit events.
- Using navigation parameters as a general mutable application store.
- Assuming navigation away always unmounts a screen and cancels its work.
- Allowing stale async results to overwrite newer feature state.
- Treating Context, reducers, and component state as durable persistence.
- Choosing a state-management library before defining ownership and lifetime requirements.
- Placing rapidly changing feature state at the application root without a cross-application requirement.
When does a React Native application need native platform integration, and what role do Native Modules play?
Direct Answer
Native Modules expose platform capabilities to React Native code when JavaScript alone cannot provide the required functionality, while keeping the native boundary explicit and narrowly scoped.
Detailed Explanation
React Native provides many platform capabilities directly through its built-in APIs and ecosystem, so application code does not need custom native code for every feature.
Custom native integration becomes useful when an application needs functionality that is available through Android, iOS, C++, or another native SDK but is not already exposed through an appropriate React Native API.
Examples can include:
Native Modules
A Native Module exposes non-visual native functionality to React Native code.
Conceptually, JavaScript or TypeScript calls a typed React Native-facing API, while the corresponding native implementation performs platform work.
The public React Native API should remain focused on what the application actually needs rather than exposing a native SDK wholesale.
For example, an application may need:
`ts
await DeviceScanner.scan();
rather than exposing dozens of platform-specific scanner classes directly to feature code.
Prefer existing React Native APIs when they satisfy the requirement
Writing native code creates additional responsibilities:
Do not introduce a custom module simply because a platform API exists natively.
Keep platform details behind the boundary
React feature code should ideally depend on a stable capability-oriented contract.
Android-specific classes and iOS-specific implementation details should remain behind the native integration boundary unless exposing them is intentionally part of the product API.
Native integration is not inherently faster
Moving arbitrary logic into native code should not be treated as a default performance optimization.
Use profiling and product requirements to determine whether native implementation provides actual value.
A strong native boundary minimizes unnecessary surface area while making ownership, errors, lifecycle, and platform differences clear.
Code Example
export interface Scanner {
scan(): Promise<
ScanResult
>;
}
export async function
scanDocument(
scanner: Scanner,
): Promise<ScanResult> {
return scanner.scan();
}
// Feature code depends on
// the capability contract,
// not Android/iOS SDK types.
Common Interview Pitfalls
- Writing custom native modules for capabilities already adequately provided by React Native.
- Exposing an entire native SDK directly to feature components instead of defining a focused application contract.
- Moving ordinary business logic into native code without a platform or performance requirement.
- Assuming native code is automatically faster than JavaScript code.
- Ignoring Android and iOS lifecycle differences when wrapping platform APIs.
- Allowing platform-specific exception or error representations to leak unpredictably into feature code.
- Adding native dependencies without considering React Native upgrade and build compatibility.
How should a React Native application organize platform-specific Android and iOS behavior and native UI components?
Direct Answer
Keep shared behavior shared, isolate genuine platform differences with Platform APIs or platform-specific files, and introduce native UI components only when the required platform view is not adequately represented by existing components.
Detailed Explanation
React Native applications often share most feature logic while still requiring some Android- or iOS-specific behavior.
React Native provides several ways to isolate those differences.
Platform API
For small conditional differences, code can inspect the current platform:
`tsx
Platform.OS
or use platform selection where appropriate.
This is useful for focused differences such as a small platform-dependent value or behavior.
Platform-specific files
For larger implementation differences, React Native can resolve platform-specific files such as conceptual variants:
`text
PaymentSheet.ios.tsx
PaymentSheet.android.tsx
Both files can expose the same feature-facing API while their implementation differs by platform.
This is often cleaner than scattering many platform conditions throughout a large component.
Shared does not mean identical
Android and iOS have different APIs, permissions, system behaviors, lifecycle details, and UI conventions.
A cross-platform application should share behavior where the product semantics are common while intentionally isolating differences where the operating systems require them.
Native UI components
Sometimes an application needs a platform-native view that React Native does not already expose.
A custom Native Component can provide a React-facing component whose implementation is backed by native platform UI.
Conceptually, feature code should be able to use an API such as:
`tsx
<MapPreview
latitude={latitude}
longitude={longitude}
/>
without needing to know how the corresponding Android or iOS view class is implemented.
Avoid unnecessary platform forks
Do not duplicate an entire feature into Android and iOS versions when only one small behavior differs.
Likewise, do not force one shared implementation when platform behavior genuinely needs different native integration.
Choose the boundary according to the size and ownership of the platform-specific behavior.
Code Example
import {
Platform,
} from 'react-native';
export const
topSpacing =
Platform.select({
ios: 12,
android: 8,
default: 8,
});
// Larger differences can use:
//
// Checkout.ios.tsx
// Checkout.android.tsx
//
// while preserving the same
// feature-facing API.
Common Interview Pitfalls
- Scattering Platform.OS checks throughout every component in a large feature.
- Duplicating an entire Android and iOS feature for one minor platform difference.
- Forcing identical implementations when the platforms require genuinely different behavior.
- Exposing native Android or iOS view classes directly to ordinary React feature code.
- Creating custom native UI components when standard React Native components already satisfy the requirement.
- Allowing platform-specific implementations to expose incompatible feature contracts accidentally.
- Assuming platform-specific files remove the need to test both platforms.
What are Fabric, Turbo Native Modules, JSI, and Codegen in the React Native New Architecture?
Direct Answer
Fabric is the New Architecture rendering system, Turbo Native Modules provide the modern native-module system, JSI enables JavaScript/native interfacing, and Codegen generates glue code from typed specifications.
Detailed Explanation
The React Native New Architecture changes several foundational systems used for rendering and native integration.
Interview answers should focus on the architectural responsibilities rather than memorizing unstable internal implementation details.
Fabric
Fabric is React Native's New Architecture rendering system.
It supports the React Native component tree and its coordination with native host views under the newer architecture.
For application developers, the useful concept is that Fabric is primarily associated with the native component/rendering side of the architecture.
Turbo Native Modules
Turbo Native Modules are the modern Native Module system used for exposing non-visual native functionality through the New Architecture.
Examples of module capabilities can include storage, hardware integrations, native SDK operations, or platform services.
Do not confuse a Turbo Native Module with a Native Component:
JSI
The JavaScript Interface, or JSI, is part of the native interfacing foundation used by the New Architecture.
The important interview-level distinction is that the New Architecture does not depend on the same JSON-serialized asynchronous bridge model traditionally associated with older React Native architecture.
Avoid making stronger claims such as every native interaction being synchronous or zero-cost.
Individual APIs can still be asynchronous by design, and platform work can still require thread scheduling and asynchronous completion.
Codegen
Codegen works from typed specifications describing Native Modules or Native Components and generates supporting native/React Native glue code.
This reduces repetitive manual interop code and helps keep the boundary contract explicit.
These pieces solve different problems
A useful mental model is:
`text
React/React Native application
|
+-- Fabric
| native component/rendering boundary
|
+-- Turbo Native Modules
non-visual native capability boundary
Typed specs
|
Codegen
|
generated integration code
JSI participates in the underlying
JavaScript/native interfacing architecture.
Do not describe Fabric, TurboModules, JSI, and Codegen as synonyms.
They are related parts of the architecture with different responsibilities.
Code Example
// Conceptual architecture:
//
// React Native Feature
// |
// +--> Fabric Native Component
// |
// +--> Turbo Native Module
//
// TypeScript / Flow specifications
// |
// Codegen
// |
// generated native integration
//
// JSI participates in the
// underlying native interface.
//
// These concepts have different
// responsibilities.
Common Interview Pitfalls
- Treating Fabric, TurboModules, JSI, and Codegen as interchangeable names for the same feature.
- Describing Fabric primarily as the Native Module system.
- Describing Turbo Native Modules as the native UI renderer.
- Claiming the New Architecture means every JavaScript-to-native operation is synchronous.
- Claiming JSI eliminates all native-call overhead.
- Memorizing internal implementation classes instead of understanding architectural boundaries.
- Teaching the old serialized bridge model as though it were the architecture of new Turbo Native Modules.
- Assuming application developers must directly manipulate JSI for ordinary React Native features.
How does React Native Codegen help define typed contracts for Turbo Native Modules and Fabric Native Components?
Direct Answer
Codegen consumes supported TypeScript or Flow specifications and generates integration scaffolding, making the React Native-to-native API contract explicit and reducing repetitive manually maintained glue code.
Detailed Explanation
Native integration crosses language and platform boundaries, so the contract between React Native and native implementations needs to remain synchronized.
React Native Codegen supports this by working from a typed specification.
Specification
A specification describes the React Native-facing API for a custom Turbo Native Module or Fabric Native Component using supported TypeScript or Flow forms.
Conceptually, a Turbo Native Module specification might describe operations such as:
`ts
getValue(key: string): string | null;
setValue(key: string, value: string): void;
The specification is not the native implementation itself.
It defines the contract that native implementations need to satisfy.
Generated integration code
Codegen can generate repetitive platform integration/scaffolding required to connect the specification to native implementation code.
This reduces the need for teams to manually keep several representations of the boundary synchronized.
Use supported boundary types
The React Native/native contract is not equivalent to arbitrary TypeScript application typing.
Specifications must use forms supported by the Codegen/native boundary.
Do not assume every advanced TypeScript construct can automatically be translated into native interfaces.
Keep contracts intentional
A native boundary is expensive to evolve compared with an ordinary internal helper function because it can affect:
Expose only the operations the React Native layer genuinely requires.
Regenerate when the specification changes
When the spec evolves, generated integration should be regenerated according to the project workflow and both platform implementations must remain compatible with the new contract.
Do not manually modify generated artifacts as the primary source of truth.
Type safety does not replace runtime failure handling
A method can be correctly typed while the underlying native operation still fails because of:
The API contract should represent expected failures appropriately rather than assuming generated typing guarantees operational success.
Codegen improves boundary consistency; it does not eliminate the need for lifecycle, error, and compatibility design.
Code Example
import type {
TurboModule,
} from 'react-native';
import {
TurboModuleRegistry,
} from 'react-native';
export interface Spec
extends TurboModule {
getValue(
key: string,
): string | null;
setValue(
key: string,
value: string,
): void;
}
export default
TurboModuleRegistry
.getEnforcing<Spec>(
'NativeSettings',
);
Common Interview Pitfalls
- Treating the Codegen specification as the native implementation itself.
- Assuming every arbitrary TypeScript type can cross the native boundary.
- Editing generated integration code as the primary source instead of updating the specification.
- Changing the specification without updating both Android and iOS implementations.
- Exposing a very large native API surface simply because Codegen can generate bindings for it.
- Assuming compile-time typing prevents runtime permission or platform failures.
- Allowing Android and iOS implementations to implement different semantics behind the same typed contract.
- Committing generated output without understanding the repository-specific Codegen workflow.
How should a React Native application handle AppState and native foreground/background lifecycle changes?
Direct Answer
AppState reports application-level foreground and background transitions; features should subscribe only when needed, clean up listeners, and design work so interruption or process loss does not corrupt important state.
Detailed Explanation
Mobile application lifetime is not equivalent to a continuously running desktop process.
The operating system can move an application between foreground and background states and may eventually terminate its process according to platform behavior and resource conditions.
React Native AppState
AppState exposes application-level state and change notifications.
A feature can observe these transitions when its behavior genuinely depends on whether the app is active or backgrounded.
For example:
`tsx
const subscription =
AppState.addEventListener(
"change",
handleAppStateChange,
);
The listener should be removed when its owner no longer needs it.
AppState is not navigation focus
Moving from one screen to another usually does not mean that the whole application entered the background.
Do not use AppState to approximate whether a particular screen is visible.
Navigation lifecycle and application lifecycle solve different problems.
Background does not mean unlimited execution
Do not assume React Native code can continue arbitrary long-running work merely because the application entered the background.
Android and iOS impose platform-specific background execution rules.
Work that must continue independently needs an appropriate supported platform mechanism and architecture.
Persist durable state before it is too late
If information must survive application/process termination, do not rely solely on an eventual termination callback.
Persist important durable state at meaningful points in the workflow.
Memory-only React state can disappear when the process is terminated.
Resume safely
When the application becomes active again, ask whether state may now be stale.
Examples include:
Refresh only what the product semantics require rather than refetching the entire application indiscriminately.
Clean up lifecycle subscriptions
A feature repeatedly mounting listeners without removing them can generate duplicate callbacks and memory/lifecycle bugs.
Application lifecycle handling should therefore have an explicit owner.
Code Example
import {
AppState,
type AppStateStatus,
} from 'react-native';
import {
useEffect,
} from 'react';
export function
useApplicationState(
onChange:
(
state:
AppStateStatus,
) => void,
) {
useEffect(() => {
const subscription =
AppState.addEventListener(
'change',
onChange,
);
return () => {
subscription.remove();
};
}, [onChange]);
}Common Interview Pitfalls
- Treating AppState as a screen-focus or navigation API.
- Assuming React Native receives unlimited background execution time.
- Keeping durable user work only in React component memory.
- Waiting for a final process-termination callback before persisting all important state.
- Registering AppState listeners repeatedly without cleanup.
- Refetching every application resource whenever the app becomes active regardless of freshness requirements.
- Assuming Android and iOS lifecycle and background-execution behavior are identical.
- Treating an application background transition as proof that every screen component unmounted.
How would you design production React Native native-integration architecture across Android and iOS while keeping New Architecture boundaries, lifecycle, errors, compatibility, and performance maintainable?
Direct Answer
Expose small typed native contracts, isolate platform implementations, use Turbo Native Modules or Fabric Components according to capability type, make lifecycle and errors explicit, and optimize native boundaries only from measured evidence.
Detailed Explanation
Production native integration should be treated as a long-lived platform boundary rather than a collection of convenience calls.
1. Start with the capability requirement
Ask why native code is necessary.
Possible reasons include:
Do not move ordinary application logic native merely because the application is React Native.
2. Choose module versus component deliberately
A non-visual platform capability normally belongs behind a Native Module-style API.
A custom native view belongs behind a Native Component boundary.
Do not create a visual component merely to invoke a service, or a service module merely to model a native view.
3. Keep the React Native-facing contract small
Avoid mirroring an entire Android/iOS SDK.
Expose application-oriented operations and data structures that the React Native feature actually needs.
This reduces:
4. Treat the typed specification as a cross-platform contract
If Android and iOS implement the same React Native API, their externally observable behavior should be intentionally aligned.
Where platforms genuinely differ, represent the difference explicitly rather than hiding incompatible behavior behind the same method name.
5. Avoid leaking native types
Feature code should not need Android Activity, Fragment, Intent, UIKit view-controller, or vendor SDK types merely to call a capability.
Translate native data into boundary types appropriate for React Native.
6. Make asynchronous semantics explicit
Some operations naturally complete asynchronously because they involve:
Do not redesign such operations as synchronous simply because newer native interfacing mechanisms exist.
Architecture capabilities do not erase real-world asynchronous behavior.
7. Understand thread requirements
Native SDKs and UI APIs can have platform-specific threading constraints.
Do not assume a native method always executes on the thread required by the underlying SDK.
Keep thread scheduling inside the native implementation boundary where possible rather than exposing threading assumptions to feature components.
8. Design event ownership
If a Native Module emits events, define:
Subscriptions need lifecycle ownership just like ordinary React listeners.
9. Map errors deliberately
Native exceptions, NSError values, Android error types, SDK statuses, and platform error codes should not leak arbitrarily through the application.
Define stable error semantics useful to React Native callers.
Preserve diagnostic information internally where appropriate.
10. Handle permission and lifecycle races
A native operation can span application state changes.
Examples include camera capture, biometric prompts, document pickers, and external-app flows.
When the operation resumes, verify that the owning feature still expects its result.
11. Separate app lifecycle from screen lifecycle
Backgrounding the application, losing navigation focus, unmounting a screen, and process termination are different events.
Do not use one as a universal proxy for another.
12. Persist what must survive process loss
If an operation represents durable user work, define persistence/recovery semantics instead of assuming the React Native runtime remains alive until completion.
13. Keep New Architecture migration intentional
Libraries and applications can have compatibility considerations when moving native integration between architectural generations.
Do not mix legacy and New Architecture concepts casually or claim they expose identical implementation contracts.
Use the compatibility approach required by the actual supported React Native versions and library strategy.
14. Keep generated code generated
Codegen output is integration scaffolding, not the place to establish the business contract manually.
Change the specification and implementation sources, then regenerate according to the project workflow.
15. Test both sides of the boundary
Useful verification can include:
A JavaScript unit test alone cannot prove that the actual native SDK behaves correctly.
16. Measure performance rather than assuming the boundary is expensive
If a feature is slow, determine whether the bottleneck is:
Do not rewrite architecture because of generic beliefs about JavaScript/native overhead.
17. Minimize chatty native APIs
If profiling demonstrates real boundary overhead, consider whether a capability-oriented operation can replace excessive tiny calls.
But do not batch unrelated operations merely to optimize an unmeasured concern.
18. Plan upgrades and vendor compatibility
Native integrations depend on more than TypeScript packages.
They can involve:
Review these dependencies during React Native upgrades.
19. Keep ownership proportional
A feature needing one device capability does not require a massive generic native-platform framework.
Create boundaries around real ownership and reuse requirements.
20. Preserve one product contract
The strongest cross-platform architecture gives feature developers a predictable application-level capability while allowing Android and iOS implementations to satisfy that contract in platform-appropriate ways.
Code Example
export type
ScanFailure =
| 'permission-denied'
| 'cancelled'
| 'unavailable'
| 'failed';
export type ScanResult = {
uri: string;
};
export interface
DocumentScanner {
scan():
Promise<ScanResult>;
}
// React Native feature:
//
// DocumentScanner
// |
// typed native boundary
// |
// +---+---+
// | |
// Android iOS
//
// Platform SDK details,
// threads, lifecycle,
// permissions, and error
// translation remain behind
// the boundary.
Common Interview Pitfalls
- Exposing complete vendor SDK APIs directly to React Native feature components.
- Using a Native Component for non-visual service functionality without a UI requirement.
- Assuming newer native interfaces make naturally asynchronous platform operations synchronous.
- Leaking Android or iOS framework types across the React Native-facing contract.
- Ignoring native API thread requirements.
- Keeping native event listeners active without lifecycle ownership.
- Allowing Android and iOS implementations of the same contract to have undocumented incompatible semantics.
- Treating screen navigation, AppState, component unmount, and process termination as the same lifecycle event.
- Editing generated Codegen output instead of updating specification and native sources.
- Testing only the TypeScript wrapper without exercising actual native implementations.
- Rewriting native architecture based on performance assumptions without profiling.
- Upgrading React Native without checking native SDK, build-tool, and architecture compatibility.
How should a React Native application use fetch and distinguish HTTP errors from network failures?
Direct Answer
React Native supports the Fetch API for HTTP requests; applications should inspect HTTP response status explicitly and separately handle transport failures, invalid payloads, cancellation, and domain errors.
Detailed Explanation
React Native applications can use the Fetch API for HTTP networking.
For example:
`tsx
const response = await fetch(
"https://api.example.com/jobs",
);
A completed HTTP response is not automatically application success
The application should inspect the response according to the API contract.
For example:
`tsx
if (!response.ok) {
// Map the HTTP failure.
}
A server returning an error status is different from a request failing before a usable HTTP response is obtained.
Different failure categories matter
A production application may need to distinguish among:
Do not convert every failure into a generic Something went wrong state inside the networking utility if callers need different recovery behavior.
Parse responses intentionally
A successful status does not guarantee that the payload matches the application's expected runtime shape.
TypeScript types describe compile-time expectations but do not validate untrusted JSON automatically at runtime.
Do not expose internal server errors directly
Display stable user-facing messages while preserving useful diagnostic information through appropriate internal logging or telemetry.
HTTPS matters
Production authentication and application traffic should use secure network transport according to the application and platform security model.
Networking code should provide a clear boundary between raw HTTP mechanics and feature-level outcomes.
Code Example
type Job = {
id: string;
title: string;
};
export async function
loadJobs(): Promise<Job[]> {
const response =
await fetch(
'https://api.example.com/jobs',
);
if (!response.ok) {
throw new Error(
`request failed: ${response.status}`,
);
}
const data =
await response.json();
return data as Job[];
}Common Interview Pitfalls
- Assuming every resolved fetch Promise represents a successful application request.
- Treating HTTP error responses and transport failures as identical.
- Assuming TypeScript automatically validates untrusted JSON at runtime.
- Displaying raw backend or infrastructure error details directly to users.
- Putting domain-specific response handling into one generic networking helper.
- Logging authentication tokens or sensitive request headers while debugging.
- Ignoring secure transport requirements for production API traffic.
Why should React Native applications cancel or ignore obsolete network work, and how should request timeouts be designed?
Direct Answer
Requests can outlive screens or newer user actions, so obsolete work should be canceled where supported or prevented from committing stale results, while timeout policy should reflect useful operation lifetime.
Detailed Explanation
Mobile networking is asynchronous, and request lifetime can easily become longer than the UI state that originally initiated the operation.
For example, a user can:
1. Search for React.
2. Immediately search for Go.
3. Receive the Go result first.
4. Receive the older React response afterward.
If every response is applied blindly, the older request can overwrite newer state.
Cancellation and stale-result prevention are related but different
When supported, an obsolete request can be canceled using an appropriate request cancellation mechanism such as AbortController with Fetch.
Even when cancellation is attempted, application state ownership should still ensure that an obsolete result cannot unexpectedly replace current data.
Timeouts
A timeout represents how long an operation remains useful to the product.
Do not select one universal timeout for every request.
A lightweight autocomplete request and a large upload may have very different useful lifetimes.
Screen lifecycle
Do not assume navigation away always means an operation must be canceled.
Some operations belong to the screen and should stop.
Others may belong to a feature-level cache or durable workflow and can continue independently.
The correct behavior follows ownership rather than component location alone.
Avoid abandoned result updates
When a component or feature no longer owns a request result, late completion should not mutate obsolete UI state.
Request lifecycle should therefore be part of feature architecture rather than an afterthought.
Code Example
import {
useEffect,
useState,
} from 'react';
export function useJobs(
query: string,
) {
const [jobs, setJobs] =
useState<Job[]>([]);
useEffect(() => {
const controller =
new AbortController();
async function load() {
try {
const response =
await fetch(
buildJobsUrl(query),
{
signal:
controller.signal,
},
);
if (!response.ok) {
throw new Error(
'request failed',
);
}
const result =
await response.json();
setJobs(result);
} catch (error) {
if (
controller
.signal
.aborted
) {
return;
}
throw error;
}
}
void load();
return () => {
controller.abort();
};
}, [query]);
return jobs;
}Common Interview Pitfalls
- Allowing older requests to overwrite newer state.
- Assuming navigation away always means every request should be canceled.
- Assuming calling abort guarantees every possible downstream operation has already stopped.
- Using one hardcoded timeout for every network operation.
- Starting duplicate requests without defining which component or data layer owns them.
- Treating cancellation as an application error that must always be shown to the user.
- Allowing late results to update state whose owner no longer cares about the operation.
How should a React Native application separate ordinary persistent state from sensitive credentials and other security-critical data?
Direct Answer
Choose persistence according to data lifetime and sensitivity: ordinary application data and caches differ from secrets such as credentials or tokens, which require platform-appropriate secure storage.
Detailed Explanation
Mobile applications persist different kinds of information for different reasons.
The first architectural question should not be Which storage library should I use?
Instead ask:
Memory state versus persistent state
React component state, reducers, and Context normally live in process memory.
If the operating system terminates the application process, that memory should not be treated as durable persistence.
Information that must survive restart needs an explicit persistence mechanism.
Ordinary application persistence
Examples can include:
The actual persistence technology depends on application requirements and chosen libraries/native capabilities.
Do not teach one storage mechanism as universally appropriate for every React Native application.
Sensitive information
Credentials, authentication secrets, private keys, and similarly sensitive information require stronger handling than generic unencrypted application persistence.
Platform secure-storage facilities or appropriately designed secure abstractions should be used according to the security requirement.
Do not embed secrets in application source
Values shipped inside a mobile application package should not be considered secret merely because they are stored in JavaScript code or environment configuration during the build.
A determined user controls the installed application binary and device environment.
Server-side secrets belong on trusted server infrastructure.
Cache versus source of truth
If persisted data is merely a cache of server-owned information, the application should define how stale or invalid cache entries are refreshed.
Do not silently treat yesterday's cached server data as permanently authoritative.
Logout and account transitions
Data ownership matters when a user signs out or switches accounts.
Account-scoped caches, drafts, and credentials should not accidentally become visible to the next authenticated account.
Strong storage architecture defines lifetime, sensitivity, ownership, and invalidation explicitly.
Code Example
type StorageClass =
| 'memory'
| 'persistent'
| 'secure';
type DataPolicy = {
lifetime:
| 'screen'
| 'session'
| 'restart';
sensitive: boolean;
accountScoped: boolean;
cache: boolean;
};
// Choose the storage mechanism
// from the data policy.
//
// Do not choose storage merely
// because one key-value API is
// convenient.
Common Interview Pitfalls
- Treating React component state as durable persistence.
- Putting authentication secrets into ordinary unencrypted application storage.
- Assuming values bundled into a mobile application binary remain secret.
- Treating cached server data as permanently authoritative.
- Choosing a storage library before defining data lifetime and sensitivity.
- Keeping account-scoped persisted data after logout without an intentional policy.
- Persisting unnecessary personal information merely because local storage is available.
- Using one storage mechanism for every category of application data.
How should a production React Native application divide testing across unit, component, integration, end-to-end, and native-boundary tests?
Direct Answer
Test pure logic directly, component behavior through user-visible outcomes, integration where collaborators matter, native code at its platform boundary, and reserve end-to-end tests for critical complete flows.
Detailed Explanation
A React Native application contains several different kinds of behavior, so no single test level provides complete confidence.
Unit tests
Pure business rules, reducers, formatters, validators, and data transformations can often be tested directly without rendering a React Native component.
These tests should generally be fast and deterministic.
Component tests
Component tests should focus on behavior users can observe.
Examples include:
Prefer testing observable behavior over implementation details such as private state variables.
Integration tests
Integration testing is useful when confidence depends on several components collaborating, such as:
`text
screen
→ feature state
→ repository/API boundary
Do not mock so aggressively that the integration being tested no longer exists.
Native boundaries
Custom Android/iOS modules and Fabric components need verification beyond a TypeScript wrapper test.
Depending on the integration, useful coverage can include:
End-to-end tests
E2E tests provide confidence across complete application flows but are slower and more operationally expensive.
Use them for high-value flows rather than moving every validation permutation into a full device-level test.
Avoid implementation-detail tests
A test tightly coupled to internal component structure can fail during harmless refactoring even though user behavior remains correct.
Prefer assertions matching the product contract.
Platform coverage matters
A React Native feature passing on iOS does not prove its Android implementation behaves identically, particularly around native modules, permissions, lifecycle, layout, or system APIs.
Code Example
// Example testing boundaries:
//
// Pure reducer
// -> direct unit test
//
// Screen behavior
// -> component test
//
// Screen + API adapter
// -> integration test
//
// Native scanner module
// -> Android/iOS native tests
// + RN integration test
//
// Login / critical checkout
// -> selected E2E flow
Common Interview Pitfalls
- Testing every business rule through a full end-to-end mobile flow.
- Mocking so many collaborators that an integration test no longer exercises integration.
- Testing private component implementation details instead of observable behavior.
- Testing only the TypeScript wrapper around custom native code.
- Assuming an iOS test pass proves Android behavior.
- Depending on production APIs or accounts in routine automated tests.
- Using fixed sleeps as the main synchronization mechanism for asynchronous tests.
- Creating a large E2E suite when cheaper test levels already cover most behavior.
How should a React Native application behave when connectivity is unreliable or unavailable, and when are retries appropriate?
Direct Answer
Design degraded and recovery states explicitly, preserve user work when required, retry only operations that are safe to repeat, and never treat connectivity status alone as proof that a remote service is reachable.
Detailed Explanation
Mobile connectivity changes frequently.
A device can move among Wi-Fi, cellular networks, captive portals, weak connections, and complete disconnection while the application remains open.
Reliable mobile architecture should expect network operations to fail.
Connectivity is not the same as service reachability
Knowing that a device appears to have network connectivity does not guarantee that:
The real request outcome remains authoritative.
Define offline product behavior
Different features have different requirements.
Examples include:
Do not claim every React Native application needs a full offline-first architecture.
Retries need semantic safety
A transient read request can often be retried more safely than a side-effecting operation.
For writes, ask whether retrying can duplicate the effect.
Examples requiring special care include:
Use operation-specific idempotency or deduplication where the backend contract supports retriable writes.
Timeout does not prove failure
A server can perform a write successfully while the response is lost before the mobile client receives it.
Blindly repeating the operation can therefore create duplicate effects.
Backoff
Repeated immediate retries can consume battery, bandwidth, and server capacity.
When automatic retries are appropriate, they should normally be bounded and spaced according to the application/dependency policy.
Preserve user work
If a form or workflow represents meaningful user effort, decide whether it should survive temporary connectivity loss or process interruption.
Reliability should be based on product semantics rather than merely displaying an offline banner.
Code Example
type RetryDecision = {
retry: boolean;
reason: string;
};
function canRetry(
operation: {
idempotent: boolean;
transientFailure: boolean;
attempts: number;
},
): RetryDecision {
if (
!operation
.transientFailure
) {
return {
retry: false,
reason:
'permanent failure',
};
}
if (
!operation.idempotent
) {
return {
retry: false,
reason:
'unsafe duplicate effect',
};
}
return {
retry:
operation.attempts < 3,
reason:
'bounded transient retry',
};
}Common Interview Pitfalls
- Treating network-connectivity status as proof that an API is reachable.
- Automatically retrying every failed request.
- Retrying non-idempotent writes without considering duplicate effects.
- Assuming a client timeout proves that the server did not process the request.
- Retrying immediately in a tight loop during an outage.
- Discarding meaningful user work after a temporary network failure.
- Building a complex offline queue for features that do not require offline execution.
- Showing cached information without defining its freshness semantics.
How would you design production React Native networking, persistence, testing, and reliability so the app remains correct across poor connectivity, process loss, retries, and native platform failures?
Direct Answer
Classify data by authority, lifetime and sensitivity; make network operations cancellable and retry-safe, persist durable work deliberately, test failure paths across JS and native boundaries, and design recovery before failures occur.
Detailed Explanation
Production mobile reliability requires assuming that networks, processes, devices, and external services can disappear at inconvenient times.
1. Classify data before choosing storage
For each value identify:
A temporary screen filter, authentication credential, cached job result, and unsent application draft should not automatically use the same persistence policy.
2. Separate server authority from local cache
A local copy of remote data should have explicit freshness and invalidation semantics.
Do not let cached data silently become an independent conflicting source of truth unless the product intentionally supports local-first mutation and reconciliation.
3. Preserve meaningful user work
For important drafts, define when data is persisted rather than waiting for final submission or process termination.
The operating system may terminate the process without giving the feature a convenient final callback.
4. Protect sensitive information
Authentication credentials and security-sensitive material require stronger handling than generic persistence.
Do not embed service secrets in the application package and assume they remain private.
5. Give requests explicit owners
A request can belong to:
Cancellation and late-result handling should follow that ownership.
6. Prevent stale-result corruption
When multiple requests target the same state, define which response remains authoritative.
Request identity, cancellation, versioning, or a centralized data layer can prevent older results from replacing newer information.
7. Define timeout policy from usefulness
Do not use one arbitrary timeout everywhere.
Uploads, lightweight reads, authentication, and background synchronization have different useful lifetimes.
8. Retry from semantics, not convenience
Retry only when the failure is plausibly transient and the operation is safe to repeat.
For write operations, coordinate with backend idempotency/deduplication where duplicate side effects matter.
9. Design degraded behavior
When the network is unavailable, determine which features should:
There is no universal offline policy.
10. Avoid unlimited retry queues
An application that indefinitely accumulates failed operations can consume storage and create a burst of outdated work when connectivity returns.
Durable queues need limits, ownership, expiry, retry policy, and reconciliation semantics.
11. Treat authentication state carefully
When authentication expires, do not allow several concurrent requests to independently trigger uncontrolled refresh behavior.
Define coordination so credentials, pending requests, logout, and account transitions remain consistent.
Do not persist credentials into ordinary logs or diagnostics.
12. Handle process loss
Anything required after restart needs durable representation.
React state, Context, reducers, promises, and in-memory queues disappear with the process.
Design restoration from persisted state rather than assuming memory continuity.
13. Respect application lifecycle without overfitting to it
AppState can inform foreground/background behavior, but do not rely on background callbacks as a guaranteed opportunity to finish critical work.
Persist durable state earlier.
14. Test failure paths
Important tests should include conditions such as:
Success-only testing creates false confidence.
15. Test native integrations on actual platform boundaries
A mocked TypeScript Native Module cannot prove the underlying Android or iOS SDK behaves correctly.
Keep native-level and device-level verification proportional to risk.
16. Protect diagnostics
Network inspection and logs can expose headers, payloads, identifiers, and credentials.
Collect only what is needed and avoid retaining sensitive information unnecessarily.
17. Measure reliability
Useful product/operational signals may include:
Metrics should answer specific reliability questions instead of creating telemetry for its own sake.
18. Make platform differences explicit
Android and iOS can differ in networking behavior, background policies, process lifecycle, secure storage capabilities, and native SDK behavior.
Keep those differences behind intentional boundaries while preserving one understandable feature contract.
19. Coordinate mobile and backend contracts
Reliable client behavior often depends on backend support for:
Mobile reliability cannot always be solved entirely inside React Native.
20. Optimize for recoverability
A robust mobile feature should be able to explain:
`text
What happens if the request fails?
What happens if the result arrives late?
What happens if the user retries?
What happens if the process disappears?
What happens when the app starts again?
If those answers are explicit, the architecture is much more likely to behave predictably in production.
Code Example
type OperationPolicy = {
owner:
| 'screen'
| 'feature'
| 'durable';
retry:
| 'never'
| 'safe-transient';
persist:
boolean;
sensitive:
boolean;
idempotencyKey?:
string;
};
// Reliability policy should
// follow product semantics,
// not a generic networking
// helper applied everywhere.
Common Interview Pitfalls
- Using one storage policy for caches, drafts, preferences, and credentials.
- Treating local cached server data as permanently authoritative.
- Waiting until process termination to persist important user work.
- Allowing obsolete network responses to replace newer data.
- Retrying side-effecting requests without backend idempotency semantics.
- Accumulating an unlimited offline retry queue.
- Assuming React memory state survives operating-system process termination.
- Testing only successful network and authentication paths.
- Mocking custom native modules without testing their actual platform implementations.
- Logging sensitive request headers or credentials during production diagnosis.
- Assuming Android and iOS lifecycle and recovery behavior are identical.
- Trying to solve backend consistency guarantees entirely in mobile client code.
How should a React Native developer investigate slow or unresponsive UI performance?
Direct Answer
Start by reproducing and measuring the problem, identify whether JavaScript, React rendering, native work, layout, images, lists, or I/O is responsible, and optimize the measured bottleneck.
Detailed Explanation
React Native performance work should begin with evidence rather than assumptions.
A user-visible symptom such as a slow screen, delayed interaction, or stuttering animation does not identify its root cause by itself.
Possible sources include:
Responsiveness depends on completing important work within the available interaction/frame budget
If too much work blocks progress during an interaction or animation, users can observe dropped frames, delayed presses, or visual stutter.
Do not reduce performance debugging to one universal claim such as the JavaScript thread is always the bottleneck.
Measure representative builds
Development tooling and development-mode checks add overhead and can distort measurements.
Performance conclusions should therefore be validated in an appropriate non-development/release-like build on representative hardware.
Reproduce first
Define the exact scenario:
`text
open jobs screen
→ scroll 500 rows
→ open item
→ return
Then collect measurements for that scenario.
Without reproducibility, before/after comparisons are unreliable.
Change one relevant cause at a time
After locating the bottleneck, make the smallest reasonable change and measure again.
An optimization is successful only if it improves the relevant user experience without unacceptable correctness, memory, or maintainability tradeoffs.
The strongest performance workflow is:
`text
observe
→ reproduce
→ measure
→ locate bottleneck
→ change
→ measure again
not add memoization everywhere.
Code Example
type PerfInvestigation = {
scenario: string;
device: string;
buildType: 'release-like';
suspectedArea:
| 'react-render'
| 'javascript'
| 'native'
| 'list'
| 'image'
| 'io'
| 'unknown';
};
// Measure a repeatable scenario
// before selecting an
// optimization.
Common Interview Pitfalls
- Optimizing code before reproducing and measuring the performance problem.
- Assuming every slow interaction is caused by JavaScript.
- Using development-mode performance as the only production performance measurement.
- Adding memoization to every component without measuring whether rendering is the bottleneck.
- Changing several architectural variables at once and losing the ability to attribute improvement.
- Testing only on a high-end development device.
- Improving one metric while introducing excessive memory usage or incorrect behavior.
How should React Native DevTools and platform profilers be used when debugging performance problems?
Direct Answer
Use React Native DevTools to inspect React and JavaScript behavior, then use Android or iOS profiling tools when evidence points to native CPU, memory, rendering, or platform-specific work.
Detailed Explanation
React Native performance debugging can cross several layers, so the tool should match the suspected bottleneck.
React Native DevTools
React Native DevTools can help inspect areas such as:
This makes it useful when investigating unnecessary React commits, expensive JavaScript work, memory retention, or application-level behavior.
React Profiler
The React Profiler can show component render/commit behavior.
If a user interaction causes a large subtree to render repeatedly, profiling can help locate where the work originates.
Do not interpret every render as a bug. Rendering is a normal part of React; the question is whether the work is unnecessary or expensive enough to matter.
Memory investigation
Memory tooling can help identify retained JavaScript objects or growth over repeated workflows.
A single large snapshot does not automatically prove a leak. Compare controlled scenarios and determine whether objects remain reachable when they should have become collectible.
Native profiling
If evidence points outside JavaScript or React, use platform tools.
Examples include Android Studio profiling tools or Apple Instruments for native CPU, memory, rendering, and system-level behavior.
Use release-like conditions for performance conclusions
Developer tooling is excellent for diagnosis, but final performance should be validated under representative build conditions.
The tool chain should follow the evidence:
`text
React render problem
→ React Profiler
JavaScript behavior/memory
→ React Native DevTools
Native CPU/memory/system issue
→ platform profiler
Do not force every problem through one profiler.
Code Example
// Example investigation:
//
// Symptom:
// Search screen stutters.
//
// 1. Reproduce.
// 2. Record React profile.
// 3. Inspect expensive commits.
// 4. Check JS execution.
// 5. If JS looks healthy,
// inspect native/platform work.
// 6. Apply one change.
// 7. Measure again.
Common Interview Pitfalls
- Treating every React render shown by the profiler as a performance defect.
- Using only console logs to diagnose complex rendering or memory problems.
- Using a JavaScript profiler to make unsupported conclusions about native CPU work.
- Using native profiling first when the problem is clearly caused by repeated React renders.
- Calling normal memory allocation a leak without showing unexpected retention over time.
- Leaving diagnostic instrumentation enabled without considering production overhead or sensitive data.
- Skipping before-and-after measurement after making a profiling-driven change.
How would you optimize a React Native screen containing a large scrolling list, images, and frequently updating items?
Direct Answer
Use virtualization, stable item identity, lightweight rows, appropriately sized images, controlled update scope, and measured list configuration rather than rendering the entire dataset or applying blanket memoization.
Detailed Explanation
Large scrolling screens combine several sources of performance cost:
Virtualize large collections
A large dataset should generally use an appropriate virtualized list abstraction such as FlatList rather than eagerly rendering every row in a ScrollView.
Virtualization limits the amount of UI work associated with content far outside the visible region.
Keep item identity stable
Use meaningful stable keys based on domain identity when available.
Random keys recreate component identity and defeat state preservation and useful rendering optimizations.
Keep rows lightweight
A list row rendered dozens or hundreds of times magnifies small costs.
Avoid unnecessary component nesting, expensive synchronous calculations, and oversized visual assets inside repeated cells.
Memoization is conditional
memo, useMemo, or useCallback can be useful when profiling shows repeated expensive work caused by unstable props or recalculation.
They also add complexity and can provide little benefit when inputs change every render.
Do not memoize every row automatically.
Image dimensions matter
A tiny visual thumbnail should not necessarily require decoding or retaining an unnecessarily huge source asset.
Use assets and loading behavior appropriate to the displayed size and product requirements.
List configuration is a tradeoff
Virtualization settings influence competing concerns such as:
There is no universal configuration that is correct for every device and list.
Tune with representative rows, datasets, devices, and user behavior.
Known layout can help
When item dimensions are predictable, providing layout information can avoid repeated measurement work in appropriate scenarios.
Do not force fixed layouts onto genuinely dynamic content solely for an optimization.
Update only what changed
If one row changes frequently, avoid unnecessarily rebuilding unrelated data or causing the whole feature tree to perform expensive work.
The objective is not zero renders; it is bounded, predictable work proportional to the visible interaction.
Code Example
import {
FlatList,
} from 'react-native';
export function JobList({
jobs,
}: {
jobs: Job[];
}) {
return (
<FlatList
data={jobs}
keyExtractor={
item => item.id
}
renderItem={({
item,
}) => (
<JobRow
job={item}
/>
)}
/>
);
}
// Tune virtualization only
// after measuring actual
// list behavior.
Common Interview Pitfalls
- Rendering thousands of large rows eagerly inside a ScrollView.
- Generating random list keys during every render.
- Using array indexes as identity for reorderable domain entities.
- Memoizing every component without checking whether unstable or expensive renders are actually occurring.
- Displaying small thumbnails from unnecessarily large image assets without considering memory and decoding cost.
- Copying list optimization settings from another application without profiling.
- Making every row fixed-size even when the content genuinely requires dynamic layout.
- Recreating large data structures unnecessarily for a small item-level change.
How should a React Native team investigate a problem that happens only in a production or release build?
Direct Answer
Reproduce the issue under release-like conditions, determine whether it originates in JavaScript, React Native, native code, build configuration, or external dependencies, and preserve useful symbolicated diagnostics without exposing sensitive data.
Detailed Explanation
A problem appearing only in production should not be assumed to be impossible to reproduce locally.
The goal is to make the local/staging environment resemble the failing conditions closely enough to isolate the difference.
Reproduce with the relevant build configuration
Development and release builds differ in important ways such as optimization, bundling, developer tooling, logging behavior, native configuration, and environment values.
A bug that disappears in development may still reproduce in a locally generated release build.
Classify the failure
Determine whether evidence points toward:
Do not assume every React Native crash originates in JavaScript.
Capture diagnostics with enough context
Useful crash or error reporting can include:
Avoid logging credentials, tokens, private form data, or complete request payloads merely to improve debugging convenience.
Preserve symbolication/source mapping workflows
Minified JavaScript and optimized native binaries are harder to diagnose without the appropriate build artifacts and mappings.
Production release processes should preserve the artifacts required by the team's crash-analysis workflow.
Compare release-specific dependencies/configuration
Investigate differences such as:
Do not debug production by exposing developer functionality to users
Diagnostic controls should remain appropriately restricted.
Production diagnostics should provide enough evidence to reproduce and correct failures without turning the production application into an unrestricted debugging environment.
Finally, convert significant production failures into regression tests at the cheapest level that can reliably reproduce the issue.
Code Example
type DiagnosticContext = {
appVersion: string;
platform: 'ios' | 'android';
osVersion: string;
feature: string;
operation: string;
};
// Keep diagnostics useful,
// but never attach credentials,
// auth tokens, or sensitive
// user content automatically.
Common Interview Pitfalls
- Trying to reproduce a release-only problem exclusively in development mode.
- Assuming every React Native production crash is a JavaScript exception.
- Shipping unrestricted developer debugging functionality in production.
- Failing to retain build artifacts needed to interpret optimized production crashes.
- Logging authentication tokens or private user data during crash diagnosis.
- Ignoring platform, OS, native dependency, or build-variant differences.
- Fixing a production failure without adding an appropriate regression test.
- Changing multiple release configuration variables simultaneously before isolating the cause.
How would you investigate and improve React Native application startup performance?
Direct Answer
Measure startup phases first, reduce unnecessary work and JavaScript loaded before first useful interaction, use Hermes and supported loading optimizations appropriately, and inspect native initialization when JavaScript is not the bottleneck.
Detailed Explanation
Startup performance is the result of several stages rather than one number produced entirely by JavaScript.
Potential startup work can include:
Measure the startup path
Define a meaningful milestone such as first useful screen or first interactive state rather than optimizing an arbitrary internal timestamp.
Then determine where the time is actually spent.
Hermes
Hermes is the JavaScript engine used by modern React Native applications by default in standard new-project configurations.
In release builds, Hermes can execute ahead-of-time compiled bytecode rather than parsing ordinary JavaScript source at application startup.
This is one part of startup architecture, not proof that startup is automatically fast.
Delay unnecessary JavaScript work
Code that is not required for the initial experience does not always need to be eagerly evaluated before the first useful interaction.
React Native provides JavaScript-loading optimizations and supports patterns that delay loading work until required.
Do not lazily load code blindly: module initialization ordering and side effects can matter.
Avoid expensive module-scope initialization
A large amount of synchronous work executed when modules are imported can lengthen startup even if the associated feature is not immediately visible.
Move expensive initialization closer to when it is actually needed where product semantics permit.
Inspect native initialization
A slow startup may occur before substantial application JavaScript runs.
Examples include native analytics SDKs, databases, advertising libraries, security SDKs, or other platform initialization.
Do not keep optimizing React components if measurement shows native initialization dominates startup.
Avoid loading everything before showing anything
Not every network request, preference, cache, and SDK must necessarily finish before displaying the first useful UI.
Separate truly blocking prerequisites from work that can happen later.
Measure application size and dependency cost when relevant
Large dependencies can affect more than package size. Some also add initialization, native binaries, JavaScript evaluation, or build complexity.
Review whether expensive dependencies provide enough product value.
Validate across device classes
Startup that appears instant on a flagship development phone can be materially slower on lower-end production hardware.
A successful startup strategy optimizes the user-perceived critical path rather than simply moving work into less visible places.
Code Example
type StartupPhase = {
name:
| 'native-init'
| 'runtime'
| 'js-load'
| 'first-render'
| 'critical-data';
durationMs: number;
blocking:
boolean;
};
// Measure startup phases,
// then remove or defer work
// from the actual critical
// path.
Common Interview Pitfalls
- Assuming all startup latency comes from React component rendering.
- Adding lazy loading everywhere without considering module initialization side effects.
- Performing expensive synchronous work at module scope for features not needed at startup.
- Blocking the first useful screen on every application network request.
- Ignoring native SDK initialization when profiling startup.
- Assuming Hermes by itself guarantees fast startup.
- Adding large native or JavaScript dependencies without considering startup and binary costs.
- Validating startup performance only on a high-end developer device.
How would you design a production React Native architecture that remains performant, diagnosable, maintainable, and upgradeable as the application grows?
Direct Answer
Design clear feature and native boundaries, keep state and rendering scoped to their owners, measure critical user flows, virtualize large data, preserve diagnostics, control dependencies, and make architecture changes from evidence.
Detailed Explanation
Production React Native architecture should optimize for predictable ownership and measurable user experience rather than maximum abstraction.
1. Define product-critical flows
Performance work needs concrete targets.
Examples include:
Measure the workflows users actually care about.
2. Keep feature boundaries explicit
A feature should have clear ownership for:
Avoid one application-level module becoming the owner of every concern.
3. Keep state close to its real owner
Frequently changing state placed unnecessarily high in the tree can broaden update scope and coupling.
Do not move state globally only because several components exist.
Likewise, do not duplicate authoritative data into every screen just to keep it local.
4. Separate remote, UI, and durable state
Remote server data has cache/freshness semantics.
UI state has component or feature lifetime.
Durable drafts have process-recovery requirements.
These categories should not accidentally share one lifecycle merely because one screen displays all three.
5. Bound rendering work
For large collections:
Do not attempt to render an entire unbounded dataset because React Native can technically construct the elements.
6. Optimize React renders from evidence
Use profiling to identify expensive or unnecessarily repeated commits.
Then consider techniques such as:
Do not make memoization the architecture itself.
7. Treat images as performance resources
Large or repeated imagery affects memory, decoding, networking, and rendering.
Use appropriately sized assets and define caching/loading behavior according to product requirements.
8. Keep JavaScript startup intentional
Avoid loading and executing expensive feature code before it is necessary.
Preserve only truly critical startup work on the first-interaction path.
9. Keep native integrations narrow
Native modules and components should expose product-oriented contracts rather than mirroring vendor SDKs throughout the React layer.
This reduces upgrade coupling and makes failures easier to isolate.
10. Respect native threading and lifecycle
Do not assume a JavaScript call implies the underlying native work runs on a particular thread or survives process/background transitions.
Hide platform-specific thread/lifecycle details behind the native boundary.
11. Make network work lifecycle-aware
A request should have a defined owner and stale-result policy.
Cancellation, caching, durable work, and retries should follow operation semantics rather than screen structure alone.
12. Design for process loss
Anything required after restart needs durable representation.
Do not rely on React state, unresolved Promises, or in-memory queues surviving operating-system process termination.
13. Preserve diagnostics
Production builds need enough information to answer:
while still protecting credentials and private user data.
14. Use multiple profiling layers
React/JavaScript tools answer different questions from Android/iOS native profilers.
Follow evidence between layers rather than assuming one tool observes the whole application.
15. Test release-like behavior
Development mode adds overhead and tooling that do not match production exactly.
Performance and release-only failures should therefore be validated under representative build conditions.
16. Control dependencies
Every dependency can add some combination of:
Do not add foundational dependencies merely because they are popular.
17. Keep upgrades continuous
Large gaps between React Native upgrades can make accumulated native, build-tool, and library incompatibilities harder to isolate.
Treat upgrade compatibility as ongoing engineering work rather than an emergency rewrite.
18. Establish regression budgets
For important flows, track relevant indicators such as startup time, memory growth, scrolling behavior, crash-free operation, or request latency where those metrics correspond to actual product requirements.
Avoid arbitrary metrics that do not map to user experience.
19. Optimize the bottleneck, not the architecture diagram
A perfectly layered codebase can still be slow, and a slow interaction does not prove the entire architecture must be replaced.
Profile the failing path before choosing a structural change.
20. Preserve correctness first
Performance optimizations that create stale data, race conditions, lost user work, broken accessibility, or platform inconsistencies are not successful optimizations.
Production architecture should make the application easier to reason about under both normal operation and failure.
Code Example
type ProductionFlow = {
name: string;
owner:
| 'screen'
| 'feature'
| 'application';
metrics: {
startupMs?: number;
memoryMb?: number;
failureRate?: number;
};
persistence:
| 'memory'
| 'durable';
nativeDependency:
boolean;
};
// Architecture decisions should
// map to real ownership,
// lifecycle, and measured
// product requirements.
Common Interview Pitfalls
- Moving all state to a global store to solve unrelated rendering problems.
- Using memoization everywhere instead of fixing incorrect state ownership.
- Rendering unbounded large datasets without virtualization.
- Optimizing only JavaScript when native profiling shows the actual bottleneck.
- Treating development-mode performance as representative of production.
- Loading every application feature and SDK before showing useful UI.
- Allowing native SDK details to spread throughout React feature code.
- Assuming in-memory state or pending operations survive process termination.
- Collecting production diagnostics that expose credentials or sensitive user content.
- Adding major dependencies without reviewing startup, native, binary, and upgrade costs.
- Replacing architecture based on anecdotal performance assumptions instead of profiling.
- Accepting an optimization that improves speed while breaking correctness or recovery behavior.
Want to tailer your resume for React Native Developer roles?
Import your resume, scan it for critical React Native Developer keywords, and compare it against ATS standards instantly.