Navigation: ← Advanced Topics | Introduction →
- Overview
- Theory
- Challenge 1: Searchable List
- Challenge 2: Counter with useReducer
- Challenge 3: API Fetch with Loading/Error
- Challenge 4: Optimized List (1000 Items)
- Challenge 5: Custom useNetworkStatus Hook
- Challenge 6: Login Form Validation
- Challenge 7: Dark Mode Toggle
- Challenge 8: Debounced Search API
- Challenge 9: Navigation List → Details
- Challenge 10: Reanimated Box Animation
- System Design Mock
- Best Practices Summary
Live coding rounds are a staple of React Native interviews at mid-level and above. Unlike algorithmic challenges, these focus on practical UI patterns, state management, performance awareness, and familiarity with the React Native ecosystem. You typically have 30-45 minutes to build a working feature while explaining your decisions.
This guide provides 10 common live coding challenges with solution approaches, TypeScript code, and a system design mock — the patterns you'll encounter at companies ranging from startups to enterprise.
| Dimension | What They Look For |
|---|---|
| Correctness | Feature works as described, handles edge cases |
| Code quality | Clean naming, proper TypeScript types, no unnecessary complexity |
| React patterns | Correct hook usage, no stale closures, proper cleanup |
| Performance awareness | Mentions memoization, list optimization, debouncing |
| Communication | Explains approach before coding, asks clarifying questions |
| Trade-offs | Discusses alternatives (FlatList vs FlashList, Context vs Zustand) |
| Pattern | Hook/Library | When to Use |
|---|---|---|
| Local state | useState |
Simple UI state |
| Complex state | useReducer |
Multi-action state machines |
| Side effects | useEffect |
API calls, subscriptions |
| Memoization | useMemo, useCallback, React.memo |
Expensive computations, list items |
| Global state | Context, Zustand | Theme, auth, shared data |
| Forms | React Hook Form | Validation, error display |
| Lists | FlatList, FlashList | Scrollable data |
| Animation | Reanimated 3 | 60fps UI thread animations |
| Navigation | React Navigation | Screen transitions, params |
| Network status | NetInfo | Offline detection |
- Clarify requirements first (2 min) — Ask about edge cases, data shape, loading states
- Scaffold before details (5 min) — Component structure, types, imports
- Make it work, then optimize (20 min) — Functional first, performance second
- Verbalize trade-offs (ongoing) — "I'm using FlatList here; FlashList would be better for 1000+ items"
- Leave time for questions (5 min) — Discuss what you'd add with more time
Build a screen with a search input and a filtered list of items. Typing in the search box filters the list in real time.
useStatefor query and derived filtered dataFlatListwith properkeyExtractorandrenderItem- Case-insensitive filtering
- Empty state when no results match
- Clean component separation
- Define a typed data array
- Store search query in
useState - Derive filtered list with
useMemo(avoid re-filtering on unrelated renders) - Render
TextInput+FlatList - Show empty state component when filtered list is empty
import React, { useMemo, useState } from 'react';
import {
View,
Text,
TextInput,
FlatList,
StyleSheet,
ListRenderItem,
} from 'react-native';
interface Item {
id: string;
name: string;
category: string;
}
const DATA: Item[] = [
{ id: '1', name: 'Apple', category: 'Fruit' },
{ id: '2', name: 'Banana', category: 'Fruit' },
{ id: '3', name: 'Carrot', category: 'Vegetable' },
{ id: '4', name: 'Broccoli', category: 'Vegetable' },
{ id: '5', name: 'Mango', category: 'Fruit' },
{ id: '6', name: 'Spinach', category: 'Vegetable' },
];
export function SearchableListScreen() {
const [query, setQuery] = useState('');
const filteredItems = useMemo(() => {
const normalized = query.trim().toLowerCase();
if (!normalized) return DATA;
return DATA.filter(
(item) =>
item.name.toLowerCase().includes(normalized) ||
item.category.toLowerCase().includes(normalized),
);
}, [query]);
const renderItem: ListRenderItem<Item> = ({ item }) => (
<View style={styles.item}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemCategory}>{item.category}</Text>
</View>
);
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Search items..."
value={query}
onChangeText={setQuery}
autoCorrect={false}
clearButtonMode="while-editing"
/>
<FlatList
data={filteredItems}
keyExtractor={(item) => item.id}
renderItem={renderItem}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<Text style={styles.empty}>No items match "{query}"</Text>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 16,
fontSize: 16,
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
itemName: { fontSize: 16, fontWeight: '600' },
itemCategory: { fontSize: 14, color: '#666', marginTop: 4 },
empty: { textAlign: 'center', color: '#999', marginTop: 32 },
});Build a counter with increment, decrement, and reset buttons. Add a step selector (step by 1, 5, or 10). Use useReducer instead of useState.
- Correct
useReducerpattern with typed actions - State includes both count and step
- Dispatching typed actions
- Understanding when reducer is preferred over useState
- Immutable state updates
- Define state type and action union type
- Write reducer function with switch on action type
- Connect buttons to dispatch calls
- Step selector updates step via dispatch
import React, { useReducer } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
interface CounterState {
count: number;
step: number;
}
type CounterAction =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' }
| { type: 'SET_STEP'; payload: number };
const initialState: CounterState = { count: 0, step: 1 };
function counterReducer(
state: CounterState,
action: CounterAction,
): CounterState {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step };
case 'DECREMENT':
return { ...state, count: state.count - state.step };
case 'RESET':
return { ...state, count: 0 };
case 'SET_STEP':
return { ...state, step: action.payload };
default:
return state;
}
}
const STEP_OPTIONS = [1, 5, 10];
export function CounterScreen() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<View style={styles.container}>
<Text style={styles.count}>{state.count}</Text>
<Text style={styles.stepLabel}>Step: {state.step}</Text>
<View style={styles.stepSelector}>
{STEP_OPTIONS.map((step) => (
<Pressable
key={step}
style={[
styles.stepButton,
state.step === step && styles.stepButtonActive,
]}
onPress={() => dispatch({ type: 'SET_STEP', payload: step })}
>
<Text style={styles.stepButtonText}>{step}</Text>
</Pressable>
))}
</View>
<View style={styles.controls}>
<Pressable
style={styles.button}
onPress={() => dispatch({ type: 'DECREMENT' })}
>
<Text style={styles.buttonText}>-</Text>
</Pressable>
<Pressable
style={[styles.button, styles.resetButton]}
onPress={() => dispatch({ type: 'RESET' })}
>
<Text style={styles.buttonText}>Reset</Text>
</Pressable>
<Pressable
style={styles.button}
onPress={() => dispatch({ type: 'INCREMENT' })}
>
<Text style={styles.buttonText}>+</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
count: { fontSize: 64, fontWeight: 'bold' },
stepLabel: { fontSize: 16, color: '#666', marginTop: 8 },
stepSelector: { flexDirection: 'row', gap: 8, marginTop: 24 },
stepButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ccc',
},
stepButtonActive: { backgroundColor: '#007AFF', borderColor: '#007AFF' },
stepButtonText: { fontSize: 14 },
controls: { flexDirection: 'row', gap: 16, marginTop: 32 },
button: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
},
resetButton: { width: 80, borderRadius: 8 },
buttonText: { color: '#fff', fontSize: 20, fontWeight: '600' },
});Fetch a list of users from an API and display them. Show a loading indicator while fetching, an error message on failure, and the list on success.
useEffectwith proper cleanup (AbortController)- Three UI states: loading, error, success
- Typed API response
- Not calling setState on unmounted component
- Dependency array correctness
- Define User type and API function with abort signal
- Track loading, error, and data in state
- Fetch in useEffect with cleanup
- Conditional rendering for each state
import React, { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
ActivityIndicator,
Pressable,
StyleSheet,
} from 'react-native';
interface User {
id: number;
name: string;
email: string;
}
const API_URL = 'https://jsonplaceholder.typicode.com/users';
async function fetchUsers(signal: AbortSignal): Promise<User[]> {
const response = await fetch(API_URL, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: Failed to fetch users`);
}
return response.json();
}
export function UserListScreen() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadUsers = () => {
setLoading(true);
setError(null);
const controller = new AbortController();
fetchUsers(controller.signal)
.then(setUsers)
.catch((err: Error) => {
if (err.name !== 'AbortError') {
setError(err.message);
}
})
.finally(() => setLoading(false));
return controller;
};
useEffect(() => {
const controller = loadUsers();
return () => controller.abort();
}, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading users...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>{error}</Text>
<Pressable style={styles.retryButton} onPress={loadUsers}>
<Text style={styles.retryText}>Retry</Text>
</Pressable>
</View>
);
}
return (
<FlatList
data={users}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<View style={styles.userItem}>
<Text style={styles.userName}>{item.name}</Text>
<Text style={styles.userEmail}>{item.email}</Text>
</View>
)}
contentContainerStyle={styles.list}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
loadingText: { marginTop: 12, color: '#666' },
errorText: { color: '#FF3B30', textAlign: 'center', marginBottom: 16 },
retryButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
retryText: { color: '#fff', fontWeight: '600' },
list: { padding: 16 },
userItem: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
userName: { fontSize: 16, fontWeight: '600' },
userEmail: { fontSize: 14, color: '#666', marginTop: 4 },
});Render a list of 1000 items smoothly at 60fps. Each item displays an index and random color. Optimize with React.memo and @shopify/flash-list.
- Understanding of why FlatList struggles at 1000+ items
React.memoon list item componentFlashListwithestimatedItemSize- Stable
keyExtractorand memoizedrenderItem - Knowledge of virtualization concepts
- Generate 1000 items with stable IDs
- Extract list item into memoized component
- Use FlashList with estimated item size
- Memoize renderItem callback with useCallback
import React, { useCallback, useMemo } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { FlashList, ListRenderItem } from '@shopify/flash-list';
interface ListItem {
id: string;
index: number;
color: string;
}
const COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'];
function generateItems(count: number): ListItem[] {
return Array.from({ length: count }, (_, i) => ({
id: `item-${i}`,
index: i,
color: COLORS[i % COLORS.length],
}));
}
interface ItemProps {
index: number;
color: string;
}
const ListItemComponent = React.memo(function ListItemComponent({
index,
color,
}: ItemProps) {
return (
<View style={[styles.item, { borderLeftColor: color }]}>
<View style={[styles.colorDot, { backgroundColor: color }]} />
<Text style={styles.itemText}>Item #{index + 1}</Text>
</View>
);
});
export function OptimizedListScreen() {
const data = useMemo(() => generateItems(1000), []);
const renderItem: ListRenderItem<ListItem> = useCallback(
({ item }) => (
<ListItemComponent index={item.index} color={item.color} />
),
[],
);
const keyExtractor = useCallback((item: ListItem) => item.id, []);
return (
<View style={styles.container}>
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={56}
removeClippedSubviews
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
item: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
borderLeftWidth: 4,
height: 56,
},
colorDot: { width: 12, height: 12, borderRadius: 6, marginRight: 12 },
itemText: { fontSize: 16 },
});Create a reusable useNetworkStatus hook that returns the current connectivity state (online/offline) and connection type (wifi, cellular, none). Show an offline banner when disconnected.
- Custom hook extraction and reusability
- NetInfo subscription with cleanup
- Typed return value
- Initial state handling
- Real-world usage in a component
- Create hook with NetInfo listener
- Return typed status object
- Subscribe in useEffect, unsubscribe on cleanup
- Build banner component consuming the hook
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import NetInfo, { NetInfoState, NetInfoStateType } from '@react-native-community/netinfo';
interface NetworkStatus {
isConnected: boolean;
isInternetReachable: boolean | null;
type: NetInfoStateType;
}
export function useNetworkStatus(): NetworkStatus {
const [status, setStatus] = useState<NetworkStatus>({
isConnected: true,
isInternetReachable: true,
type: NetInfoStateType.unknown,
});
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => {
setStatus({
isConnected: state.isConnected ?? false,
isInternetReachable: state.isInternetReachable,
type: state.type,
});
});
return unsubscribe;
}, []);
return status;
}
function OfflineBanner() {
const { isConnected, type } = useNetworkStatus();
if (isConnected) return null;
return (
<View style={bannerStyles.container}>
<Text style={bannerStyles.text}>
No internet connection ({type})
</Text>
</View>
);
}
export function NetworkAwareScreen() {
const { isConnected, isInternetReachable, type } = useNetworkStatus();
return (
<View style={styles.container}>
<OfflineBanner />
<View style={styles.content}>
<Text style={styles.label}>Connection Type</Text>
<Text style={styles.value}>{type}</Text>
<Text style={styles.label}>Connected</Text>
<Text style={styles.value}>{isConnected ? 'Yes' : 'No'}</Text>
<Text style={styles.label}>Internet Reachable</Text>
<Text style={styles.value}>
{isInternetReachable === null ? 'Checking...' : isInternetReachable ? 'Yes' : 'No'}
</Text>
</View>
</View>
);
}
const bannerStyles = StyleSheet.create({
container: {
backgroundColor: '#FF3B30',
padding: 12,
alignItems: 'center',
},
text: { color: '#fff', fontWeight: '600' },
});
const styles = StyleSheet.create({
container: { flex: 1 },
content: { flex: 1, padding: 24 },
label: { fontSize: 14, color: '#666', marginTop: 16 },
value: { fontSize: 20, fontWeight: '600', marginTop: 4 },
});Build a login form with email and password fields. Validate email format and minimum password length. Show inline error messages. Use React Hook Form.
- React Hook Form setup with TypeScript
- Validation rules (required, pattern, minLength)
- Error message display per field
- Form submission handling
- Loading state during submit
- Define form data type
- Configure useForm with validation rules
- Use Controller for TextInput binding
- Handle submit with async simulation
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
Pressable,
ActivityIndicator,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { useForm, Controller } from 'react-hook-form';
interface LoginFormData {
email: string;
password: string;
}
interface LoginFormProps {
onSubmit: (data: LoginFormData) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [submitting, setSubmitting] = useState(false);
const {
control,
handleSubmit,
formState: { errors },
} = useForm<LoginFormData>({
defaultValues: { email: '', password: '' },
});
const onFormSubmit = async (data: LoginFormData) => {
setSubmitting(true);
try {
await onSubmit(data);
} finally {
setSubmitting(false);
}
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<Text style={styles.title}>Sign In</Text>
<Controller
control={control}
name="email"
rules={{
required: 'Email is required',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Enter a valid email address',
},
}}
render={({ field: { onChange, onBlur, value } }) => (
<View>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
{errors.email && (
<Text style={styles.error}>{errors.email.message}</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="password"
rules={{
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters',
},
}}
render={({ field: { onChange, onBlur, value } }) => (
<View>
<TextInput
style={[styles.input, errors.password && styles.inputError]}
placeholder="Password"
secureTextEntry
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
{errors.password && (
<Text style={styles.error}>{errors.password.message}</Text>
)}
</View>
)}
/>
<Pressable
style={[styles.button, submitting && styles.buttonDisabled]}
onPress={handleSubmit(onFormSubmit)}
disabled={submitting}
>
{submitting ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Sign In</Text>
)}
</Pressable>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, justifyContent: 'center' },
title: { fontSize: 28, fontWeight: 'bold', marginBottom: 32 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 14,
fontSize: 16,
marginBottom: 4,
},
inputError: { borderColor: '#FF3B30' },
error: { color: '#FF3B30', fontSize: 13, marginBottom: 12 },
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 16,
},
buttonDisabled: { opacity: 0.6 },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});Implement a dark/light mode toggle using React Context. The theme should persist across the app and affect background, text, and component colors.
- Context creation with typed provider
- Custom hook for consuming theme (
useTheme) - Toggle mechanism
- Style application based on theme
- Avoiding unnecessary re-renders (memoize context value)
- Define Theme type with color tokens
- Create ThemeContext with light/dark palettes
- Build provider with toggle function
- Consume in components via useTheme hook
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { View, Text, Pressable, StyleSheet, Switch } from 'react-native';
interface ThemeColors {
background: string;
surface: string;
text: string;
textSecondary: string;
primary: string;
border: string;
}
interface Theme {
dark: boolean;
colors: ThemeColors;
}
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const lightColors: ThemeColors = {
background: '#FFFFFF',
surface: '#F2F2F7',
text: '#000000',
textSecondary: '#666666',
primary: '#007AFF',
border: '#E5E5EA',
};
const darkColors: ThemeColors = {
background: '#000000',
surface: '#1C1C1E',
text: '#FFFFFF',
textSecondary: '#AEAEB2',
primary: '#0A84FF',
border: '#38383A',
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [dark, setDark] = useState(false);
const toggleTheme = useCallback(() => {
setDark((prev) => !prev);
}, []);
const value = useMemo<ThemeContextValue>(
() => ({
theme: { dark, colors: dark ? darkColors : lightColors },
toggleTheme,
}),
[dark, toggleTheme],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function ThemedCard({ title, subtitle }: { title: string; subtitle: string }) {
const { theme } = useTheme();
const { colors } = theme;
return (
<View
style={[
cardStyles.card,
{ backgroundColor: colors.surface, borderColor: colors.border },
]}
>
<Text style={[cardStyles.title, { color: colors.text }]}>{title}</Text>
<Text style={[cardStyles.subtitle, { color: colors.textSecondary }]}>
{subtitle}
</Text>
</View>
);
}
export function DarkModeScreen() {
const { theme, toggleTheme } = useTheme();
const { colors } = theme;
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.header}>
<Text style={[styles.headerText, { color: colors.text }]}>
{theme.dark ? 'Dark Mode' : 'Light Mode'}
</Text>
<Switch
value={theme.dark}
onValueChange={toggleTheme}
trackColor={{ true: colors.primary }}
/>
</View>
<ThemedCard title="Welcome" subtitle="Theme persists across components" />
<ThemedCard title="Performance" subtitle="Context value is memoized" />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24 },
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
headerText: { fontSize: 24, fontWeight: 'bold' },
});
const cardStyles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,
borderWidth: 1,
marginBottom: 12,
},
title: { fontSize: 18, fontWeight: '600' },
subtitle: { fontSize: 14, marginTop: 4 },
});Build a search input that calls an API after the user stops typing for 300ms. Show loading indicator during fetch, handle errors, and cancel stale requests.
- Debounce implementation (custom or lodash)
- Cleanup of pending debounce and in-flight requests
- AbortController for request cancellation
- Loading state during debounced fetch
- Empty query handling (don't fetch)
- Create useDebounce custom hook
- Store debounced query separately from input
- Fetch in useEffect triggered by debounced value
- Abort previous request when new query arrives
import React, { useEffect, useState, useRef } from 'react';
import {
View,
Text,
TextInput,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
interface SearchResult {
id: number;
title: string;
}
async function searchAPI(
query: string,
signal: AbortSignal,
): Promise<SearchResult[]> {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?q=${encodeURIComponent(query)}`,
{ signal },
);
if (!response.ok) throw new Error('Search failed');
const posts: Array<{ id: number; title: string }> = await response.json();
return posts
.filter((p) => p.title.toLowerCase().includes(query.toLowerCase()))
.slice(0, 20);
}
export function DebouncedSearchScreen() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debouncedQuery = useDebounce(query, 300);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (!debouncedQuery.trim()) {
setResults([]);
setError(null);
return;
}
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
searchAPI(debouncedQuery, controller.signal)
.then(setResults)
.catch((err: Error) => {
if (err.name !== 'AbortError') {
setError(err.message);
}
})
.finally(() => {
if (!controller.signal.aborted) {
setLoading(false);
}
});
return () => controller.abort();
}, [debouncedQuery]);
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Search posts..."
value={query}
onChangeText={setQuery}
autoCorrect={false}
/>
{loading && <ActivityIndicator style={styles.loader} color="#007AFF" />}
{error && <Text style={styles.error}>{error}</Text>}
<FlatList
data={results}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<View style={styles.resultItem}>
<Text style={styles.resultTitle}>{item.title}</Text>
</View>
)}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
debouncedQuery && !loading ? (
<Text style={styles.empty}>No results found</Text>
) : null
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
},
loader: { marginVertical: 12 },
error: { color: '#FF3B30', marginVertical: 8 },
resultItem: { padding: 14, borderBottomWidth: 1, borderBottomColor: '#eee' },
resultTitle: { fontSize: 15 },
empty: { textAlign: 'center', color: '#999', marginTop: 24 },
});Create a list screen that navigates to a detail screen when an item is tapped. Pass the item ID as a navigation param and fetch/display details on the detail screen.
- React Navigation stack setup with typed params
- Passing params via
navigation.navigate - Reading params with
route.paramson detail screen - Type-safe navigation with ParamList
- Loading details based on param
- Define RootStackParamList with typed params
- Build list screen with navigate on press
- Build detail screen reading route params
- Fetch detail data based on ID param
import React from 'react';
import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
type RootStackParamList = {
ProductList: undefined;
ProductDetail: { productId: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const PRODUCTS: Product[] = [
{ id: '1', name: 'Wireless Headphones', price: 99.99, description: 'Premium noise-cancelling headphones with 30h battery life.' },
{ id: '2', name: 'Smart Watch', price: 249.99, description: 'Fitness tracking, heart rate monitor, GPS enabled.' },
{ id: '3', name: 'Mechanical Keyboard', price: 149.99, description: 'Cherry MX switches, RGB backlight, wireless.' },
{ id: '4', name: 'USB-C Hub', price: 49.99, description: '7-in-1 adapter with HDMI, SD card, and USB 3.0.' },
];
type ListProps = NativeStackScreenProps<RootStackParamList, 'ProductList'>;
type DetailProps = NativeStackScreenProps<RootStackParamList, 'ProductDetail'>;
function ProductListScreen({ navigation }: ListProps) {
return (
<FlatList
data={PRODUCTS}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<Pressable
style={styles.productItem}
onPress={() =>
navigation.navigate('ProductDetail', { productId: item.id })
}
>
<Text style={styles.productName}>{item.name}</Text>
<Text style={styles.productPrice}>${item.price.toFixed(2)}</Text>
</Pressable>
)}
/>
);
}
function ProductDetailScreen({ route }: DetailProps) {
const { productId } = route.params;
const product = PRODUCTS.find((p) => p.id === productId);
if (!product) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>Product not found</Text>
</View>
);
}
return (
<View style={styles.detail}>
<Text style={styles.detailName}>{product.name}</Text>
<Text style={styles.detailPrice}>${product.price.toFixed(2)}</Text>
<Text style={styles.detailDescription}>{product.description}</Text>
</View>
);
}
export function ProductNavigator() {
return (
<Stack.Navigator>
<Stack.Screen
name="ProductList"
component={ProductListScreen}
options={{ title: 'Products' }}
/>
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={{ title: 'Product Detail' }}
/>
</Stack.Navigator>
);
}
const styles = StyleSheet.create({
list: { padding: 16 },
productItem: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
productName: { fontSize: 16, fontWeight: '600' },
productPrice: { fontSize: 16, color: '#007AFF' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#FF3B30', fontSize: 16 },
detail: { flex: 1, padding: 24 },
detailName: { fontSize: 28, fontWeight: 'bold' },
detailPrice: { fontSize: 24, color: '#007AFF', marginTop: 8 },
detailDescription: { fontSize: 16, color: '#666', marginTop: 16, lineHeight: 24 },
});Animate a box that moves horizontally when a button is pressed. Use react-native-reanimated for 60fps UI-thread animation with spring physics.
- Reanimated 3 API (
useSharedValue,useAnimatedStyle,withSpring) - Animated.View from reanimated (not React Native Animated)
- Understanding UI thread vs JS thread animation
- Pressable trigger for animation
- Spring configuration awareness
- Create shared value for translateX
- Define animated style mapping shared value to transform
- Toggle position on button press with withSpring
- Render Animated.View with animated style
import React from 'react';
import { View, Pressable, Text, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolateColor,
} from 'react-native-reanimated';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const BOX_SIZE = 80;
const TRAVEL_DISTANCE = SCREEN_WIDTH - BOX_SIZE - 48;
export function ReanimatedBoxScreen() {
const translateX = useSharedValue(0);
const isMoved = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
backgroundColor: interpolateColor(
isMoved.value,
[0, 1],
['#007AFF', '#FF3B30'],
),
}));
const togglePosition = () => {
const moving = isMoved.value === 0;
translateX.value = withSpring(moving ? TRAVEL_DISTANCE : 0, {
damping: 15,
stiffness: 120,
mass: 1,
});
isMoved.value = withTiming(moving ? 1 : 0, { duration: 300 });
};
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Pressable style={styles.button} onPress={togglePosition}>
<Text style={styles.buttonText}>Toggle Position</Text>
</Pressable>
<Text style={styles.hint}>
Animation runs on the UI thread via Reanimated worklets
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
},
box: {
width: BOX_SIZE,
height: BOX_SIZE,
borderRadius: 16,
backgroundColor: '#007AFF',
},
button: {
backgroundColor: '#333',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 48,
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
hint: { textAlign: 'center', color: '#999', marginTop: 16, fontSize: 13 },
});Design the architecture for an Instagram-like or e-commerce React Native app. Cover module structure, state management, offline support, and CI/CD pipeline. You have 15-20 minutes to whiteboard the architecture.
- Feature-based folder structure
- Appropriate state management choices (server vs client state)
- Offline-first strategy
- Navigation architecture for deep screen hierarchies
- CI/CD pipeline awareness
- Scalability for team growth
- Image/media handling strategy
- Authentication and security patterns
Structure the answer in layers: Architecture → State → Offline → CI/CD → Scalability.
Q: Design the architecture for a large-scale Instagram/e-commerce React Native app. How do you structure modules, manage state, handle offline, and set up CI/CD?
Answer:
1. Project Architecture — Feature-based modules:
src/
├── app/ # App entry, providers, navigation shell
├── features/
│ ├── auth/ # Login, register, biometrics
│ ├── feed/ # Timeline, stories, posts
│ ├── search/ # Search, filters, suggestions
│ ├── profile/ # User profile, settings, edit
│ ├── cart/ # Shopping cart, checkout (e-commerce)
│ ├── orders/ # Order history, tracking
│ ├── chat/ # Messaging, notifications
│ └── camera/ # Media capture, editing
├── shared/
│ ├── components/ # Button, Input, Avatar, Card
│ ├── hooks/ # useNetworkStatus, useDebounce
│ ├── utils/ # Formatters, validators
│ └── types/ # Shared TypeScript interfaces
├── services/
│ ├── api/ # Axios client, interceptors
│ ├── storage/ # MMKV, SecureStore wrappers
│ └── analytics/ # Event tracking abstraction
└── navigation/
├── RootNavigator.tsx
├── AuthNavigator.tsx
└── MainTabNavigator.tsx
Each feature is self-contained with its own components, hooks, services, and types. Teams own features independently.
2. State Management — Right tool for each concern:
| State Type | Tool | Example |
|---|---|---|
| Server state | TanStack Query | Feed posts, product catalog, user profiles |
| Global client state | Zustand | Auth session, cart items, app settings |
| Local UI state | useState/useReducer | Form inputs, modal visibility, tab index |
| Theme/preferences | Context | Dark mode, locale, accessibility |
// Server state — TanStack Query
const { data: feed } = useInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetchFeed(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
// Client state — Zustand
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
}));Why not Redux everywhere? TanStack Query eliminates boilerplate for server data (caching, refetch, pagination). Zustand handles lightweight global state without the ceremony of actions/reducers for simple cases.
3. Offline Support — Layered strategy:
| Layer | Strategy | Implementation |
|---|---|---|
| API cache | Stale-while-revalidate | TanStack Query with gcTime, networkMode: 'offlineFirst' |
| Persistent cache | Survive app restart | MMKV-backed query persister |
| Optimistic updates | Instant UI feedback | useMutation with rollback on failure |
| Media cache | Image/video offline | FastImage with disk cache, expo-file-system |
| Action queue | Write offline, sync later | Queue mutations in MMKV, replay on reconnect |
| Network detection | Pause/resume | NetInfo + React Query onlineManager |
import { onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';
onlineManager.setEventListener((setOnline) =>
NetInfo.addEventListener((state) => {
setOnline(state.isConnected ?? false);
}),
);4. Navigation Architecture:
RootNavigator
├── AuthStack (unauthenticated)
│ ├── Login
│ ├── Register
│ └── ForgotPassword
└── MainTabs (authenticated)
├── FeedStack
│ ├── Feed
│ ├── PostDetail
│ └── Comments
├── SearchStack
├── CartStack (e-commerce)
├── OrdersStack
└── ProfileStack
├── Profile
├── Settings
└── EditProfile
- Deep linking configured for push notifications and marketing campaigns
- Modal screens for create post, camera, checkout
- Shared element transitions for image → detail (Reanimated)
5. CI/CD Pipeline:
Developer push → GitHub Actions
├── Lint + TypeScript check
├── Unit tests (Jest)
├── Integration tests (Detox/Maestro)
├── Build Android (Gradle) + iOS (xcodebuild)
├── Deploy to Firebase App Distribution (internal)
├── E2E tests on real devices (Maestro Cloud)
└── Release via Fastlane / EAS
├── Play Store (staged rollout 10→50→100%)
└── App Store (TestFlight → Production)
Key CI/CD practices:
- Feature flags (LaunchDarkly/ConfigCat) for gradual feature rollout
- Code signing managed via Fastlane Match or EAS credentials
- Automated version bumping on merge to main
- Crash monitoring (Sentry) gate — block release if crash rate spikes
- OTA updates (Expo Updates/CodePush) for JS-only hotfixes
6. Scalability Considerations:
| Concern | Solution |
|---|---|
| Team growth (5→20 devs) | Feature modules with CODEOWNERS |
| Image-heavy feed | FlashList + FastImage + thumbnail prefetch |
| Real-time (chat, live) | WebSocket with reconnection + optimistic UI |
| Search at scale | Debounced API + local recent search cache |
| Multi-environment | .env files + build flavors (dev/staging/prod) |
| Analytics | Event abstraction layer (Segment/Amplitude) |
| A/B testing | Feature flags with analytics integration |
| Security | Certificate pinning, Keychain tokens, jailbreak detection |
Architecture diagram:
┌─────────────────────────────────────────────────────┐
│ React Native App │
├──────────┬──────────┬──────────┬────────────────────┤
│ Features │ Shared │ Services │ Navigation │
│ (modules)│ (UI/hooks│ (API, │ (Stacks, Tabs, │
│ │ /types) │ storage)│ Deep Links) │
├──────────┴──────────┴──────────┴────────────────────┤
│ State Layer │
│ TanStack Query (server) + Zustand (client) │
├─────────────────────────────────────────────────────┤
│ Offline Layer │
│ MMKV cache + Action queue + NetInfo │
├─────────────────────────────────────────────────────┤
│ Native Layer │
│ Push (FCM/APNs) + Camera + Biometrics + Maps │
└─────────────────────────────────────────────────────┘
↕ HTTPS ↕ WebSocket
┌─────────────────┐ ┌─────────────────┐
│ REST/GraphQL │ │ Real-time API │
│ Backend │ │ (Chat/Live) │
└─────────────────┘ └─────────────────┘
| Practice | Reason |
|---|---|
| Clarify requirements before coding | Avoid building the wrong thing |
| Use TypeScript types for all props and state | Demonstrates production-ready habits |
| Extract reusable logic into custom hooks | Shows abstraction skills |
| Mention performance trade-offs verbally | FlatList vs FlashList, memoization |
| Handle loading, error, and empty states | Complete feature, not just happy path |
| Clean up subscriptions and abort requests | Prevent memory leaks |
| Use React Hook Form for form challenges | Industry standard, less boilerplate |
| Prefer Reanimated over Animated API | Expected in modern RN interviews |
| Structure system design answers in layers | Architecture → State → Offline → CI/CD |
| Ask about edge cases during live coding | Shows senior-level thinking |
Navigation: ← Advanced Topics | Introduction →