Skip to content

Latest commit

 

History

History
1629 lines (1349 loc) · 46.6 KB

File metadata and controls

1629 lines (1349 loc) · 46.6 KB

Live Coding Challenges

Navigation: ← Advanced Topics | Introduction →


Table of Contents


Overview

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.


Theory

What Interviewers Evaluate

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)

Common Patterns and Libraries

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

Time Management Tips

  1. Clarify requirements first (2 min) — Ask about edge cases, data shape, loading states
  2. Scaffold before details (5 min) — Component structure, types, imports
  3. Make it work, then optimize (20 min) — Functional first, performance second
  4. Verbalize trade-offs (ongoing) — "I'm using FlatList here; FlashList would be better for 1000+ items"
  5. Leave time for questions (5 min) — Discuss what you'd add with more time

Challenge 1: Searchable List

Challenge

Build a screen with a search input and a filtered list of items. Typing in the search box filters the list in real time.

What the Interviewer Checks

  • useState for query and derived filtered data
  • FlatList with proper keyExtractor and renderItem
  • Case-insensitive filtering
  • Empty state when no results match
  • Clean component separation

Solution Approach

  1. Define a typed data array
  2. Store search query in useState
  3. Derive filtered list with useMemo (avoid re-filtering on unrelated renders)
  4. Render TextInput + FlatList
  5. Show empty state component when filtered list is empty

TypeScript Code

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 },
});

Challenge 2: Counter with useReducer

Challenge

Build a counter with increment, decrement, and reset buttons. Add a step selector (step by 1, 5, or 10). Use useReducer instead of useState.

What the Interviewer Checks

  • Correct useReducer pattern with typed actions
  • State includes both count and step
  • Dispatching typed actions
  • Understanding when reducer is preferred over useState
  • Immutable state updates

Solution Approach

  1. Define state type and action union type
  2. Write reducer function with switch on action type
  3. Connect buttons to dispatch calls
  4. Step selector updates step via dispatch

TypeScript Code

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' },
});

Challenge 3: API Fetch with Loading/Error

Challenge

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.

What the Interviewer Checks

  • useEffect with proper cleanup (AbortController)
  • Three UI states: loading, error, success
  • Typed API response
  • Not calling setState on unmounted component
  • Dependency array correctness

Solution Approach

  1. Define User type and API function with abort signal
  2. Track loading, error, and data in state
  3. Fetch in useEffect with cleanup
  4. Conditional rendering for each state

TypeScript Code

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 },
});

Challenge 4: Optimized List (1000 Items)

Challenge

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.

What the Interviewer Checks

  • Understanding of why FlatList struggles at 1000+ items
  • React.memo on list item component
  • FlashList with estimatedItemSize
  • Stable keyExtractor and memoized renderItem
  • Knowledge of virtualization concepts

Solution Approach

  1. Generate 1000 items with stable IDs
  2. Extract list item into memoized component
  3. Use FlashList with estimated item size
  4. Memoize renderItem callback with useCallback

TypeScript Code

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 },
});

Challenge 5: Custom useNetworkStatus Hook

Challenge

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.

What the Interviewer Checks

  • Custom hook extraction and reusability
  • NetInfo subscription with cleanup
  • Typed return value
  • Initial state handling
  • Real-world usage in a component

Solution Approach

  1. Create hook with NetInfo listener
  2. Return typed status object
  3. Subscribe in useEffect, unsubscribe on cleanup
  4. Build banner component consuming the hook

TypeScript Code

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 },
});

Challenge 6: Login Form Validation

Challenge

Build a login form with email and password fields. Validate email format and minimum password length. Show inline error messages. Use React Hook Form.

What the Interviewer Checks

  • React Hook Form setup with TypeScript
  • Validation rules (required, pattern, minLength)
  • Error message display per field
  • Form submission handling
  • Loading state during submit

Solution Approach

  1. Define form data type
  2. Configure useForm with validation rules
  3. Use Controller for TextInput binding
  4. Handle submit with async simulation

TypeScript Code

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' },
});

Challenge 7: Dark Mode Toggle

Challenge

Implement a dark/light mode toggle using React Context. The theme should persist across the app and affect background, text, and component colors.

What the Interviewer Checks

  • 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)

Solution Approach

  1. Define Theme type with color tokens
  2. Create ThemeContext with light/dark palettes
  3. Build provider with toggle function
  4. Consume in components via useTheme hook

TypeScript Code

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 },
});

Challenge 8: Debounced Search API

Challenge

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.

What the Interviewer Checks

  • 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)

Solution Approach

  1. Create useDebounce custom hook
  2. Store debounced query separately from input
  3. Fetch in useEffect triggered by debounced value
  4. Abort previous request when new query arrives

TypeScript Code

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 },
});

Challenge 9: Navigation List → Details

Challenge

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.

What the Interviewer Checks

  • React Navigation stack setup with typed params
  • Passing params via navigation.navigate
  • Reading params with route.params on detail screen
  • Type-safe navigation with ParamList
  • Loading details based on param

Solution Approach

  1. Define RootStackParamList with typed params
  2. Build list screen with navigate on press
  3. Build detail screen reading route params
  4. Fetch detail data based on ID param

TypeScript Code

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 },
});

Challenge 10: Reanimated Box Animation

Challenge

Animate a box that moves horizontally when a button is pressed. Use react-native-reanimated for 60fps UI-thread animation with spring physics.

What the Interviewer Checks

  • 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

Solution Approach

  1. Create shared value for translateX
  2. Define animated style mapping shared value to transform
  3. Toggle position on button press with withSpring
  4. Render Animated.View with animated style

TypeScript Code

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 },
});

System Design Mock

Challenge

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.

What the Interviewer Checks

  • 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

Solution Approach

Structure the answer in layers: Architecture → State → Offline → CI/CD → Scalability.

Consolidated Q&A

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)    │
└─────────────────┘  └─────────────────┘

Best Practices Summary

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 →