Skip to content

Latest commit

 

History

History
1361 lines (1064 loc) · 41.9 KB

File metadata and controls

1361 lines (1064 loc) · 41.9 KB

JavaScript & TypeScript for React Native

Table of Contents

Closures

Simple Explanation:

A closure is when a function "remembers" the variables from the scope where it was created, even after that outer scope has finished executing. The inner function carries a backpack of variables from its birthplace.

Real-World Analogy:

Think of a closure like a restaurant receipt. The chef (outer function) finishes cooking and leaves the kitchen, but the receipt (inner function) still remembers exactly what ingredients (variables) were used. Anyone who reads the receipt later knows the full order details.

// Basic closure example
function createCounter(initialValue: number = 0) {
  // This variable lives in the outer scope
  // Think of it as a private vault only inner functions can access
  let count = initialValue;

  // The returned object contains functions that "close over" count
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}

const counter = createCounter(10);
console.log(counter.getCount()); // 10
counter.increment();
console.log(counter.getCount()); // 11
// count is NOT accessible directly — it's encapsulated
// console.log(counter.count); // undefined

Closures in React Native:

import React, { useState, useCallback, useRef } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';

// Closure in event handlers — common pattern in RN
export function TimerScreen(): React.JSX.Element {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const startTimer = useCallback(() => {
    // This closure captures intervalRef and setSeconds
    if (intervalRef.current) return;

    intervalRef.current = setInterval(() => {
      // Inner closure captures setSeconds from outer scope
      setSeconds((prev) => prev + 1);
    }, 1000);
  }, []);

  const stopTimer = useCallback(() => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.timer}>{seconds}s</Text>
      <Pressable style={styles.button} onPress={startTimer}>
        <Text>Start</Text>
      </Pressable>
      <Pressable style={styles.button} onPress={stopTimer}>
        <Text>Stop</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  timer: { fontSize: 48, marginBottom: 24 },
  button: { padding: 12, marginVertical: 4 },
});

Common Closure Pitfall — Stale Closures:

// ❌ Stale closure: effect captures old count value
function StaleClosureExample(): React.JSX.Element {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // Always logs 0 — closure captured initial count
      console.log('Count:', count);
    }, 1000);
    return () => clearInterval(id);
  }, []); // Missing count in deps!

  return <Text>{count}</Text>;
}

// ✅ Fix: use functional update or include count in deps
function FixedClosureExample(): React.JSX.Element {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // Functional update always gets latest value
      setCount((prev) => {
        console.log('Count:', prev);
        return prev;
      });
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <Text>{count}</Text>;
}

Async/Await vs Promises

Simple Explanation:

Promises represent a value that will be available in the future. Async/await is syntactic sugar on top of Promises that makes asynchronous code look and read like synchronous code.

Real-World Analogy:

  • Promise = A restaurant pager. You get a pager (Promise), do other things, and when your table is ready, the pager buzzes (.then()).
  • Async/await = A waiter who stands at your table until the food arrives. You wait (await) at that spot, but other tables (other async functions) are still being served.
// Promise-based approach
interface User {
  id: string;
  name: string;
  email: string;
}

function fetchUserPromise(userId: string): Promise<User> {
  return fetch(`https://api.example.com/users/${userId}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    })
    .then((data: User) => data)
    .catch((error: Error) => {
      console.error('Failed to fetch user:', error.message);
      throw error;
    });
}

// Async/await approach — same logic, cleaner syntax
async function fetchUserAsync(userId: string): Promise<User> {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data: User = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch user:', (error as Error).message);
    throw error;
  }
}

React Native API Service Pattern:

// Production-ready API service with async/await
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

interface RequestConfig {
  method?: HttpMethod;
  body?: unknown;
  headers?: Record<string, string>;
  timeout?: number;
}

class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public responseBody?: unknown,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function apiRequest<T>(
  url: string,
  config: RequestConfig = {},
): Promise<T> {
  const { method = 'GET', body, headers = {}, timeout = 10_000 } = config;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      method,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        ...headers,
      },
      body: body ? JSON.stringify(body) : undefined,
      signal: controller.signal,
    });

    if (!response.ok) {
      const errorBody = await response.json().catch(() => null);
      throw new ApiError(
        `Request failed: ${response.statusText}`,
        response.status,
        errorBody,
      );
    }

    return (await response.json()) as T;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Parallel requests with Promise.all
async function fetchDashboardData(userId: string) {
  const [user, posts, notifications] = await Promise.all([
    apiRequest<User>(`/users/${userId}`),
    apiRequest<Post[]>(`/users/${userId}/posts`),
    apiRequest<Notification[]>(`/users/${userId}/notifications`),
  ]);

  return { user, posts, notifications };
}

// Sequential dependency with await
async function createPostWithMedia(
  userId: string,
  mediaUri: string,
  caption: string,
): Promise<Post> {
  // Step 1: Upload media first (must complete before post creation)
  const media = await apiRequest<Media>('/media/upload', {
    method: 'POST',
    body: { uri: mediaUri, userId },
  });

  // Step 2: Create post with uploaded media reference
  const post = await apiRequest<Post>('/posts', {
    method: 'POST',
    body: { userId, mediaId: media.id, caption },
  });

  return post;
}

Event Loop

Simple Explanation:

The JavaScript event loop is the mechanism that allows JavaScript (which is single-threaded) to handle asynchronous operations. It continuously checks if the call stack is empty, then processes tasks from the callback queue (macrotasks) and microtask queue.

Real-World Analogy:

Imagine a barista (call stack) making coffee. While waiting for the espresso machine (async operation), the barista doesn't stand idle — they take the next order (processes the queue). When the espresso is ready (callback), it goes to the pickup counter (callback queue) and gets handled when the barista is free.

// Event loop execution order demonstration
console.log('1: Synchronous start');

setTimeout(() => {
  console.log('2: setTimeout (macrotask)');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise.then (microtask)');
});

console.log('4: Synchronous end');

// Output order:
// 1: Synchronous start
// 4: Synchronous end
// 3: Promise.then (microtask)    ← Microtasks run BEFORE macrotasks
// 2: setTimeout (macrotask)

Event Loop in React Native:

import { InteractionManager } from 'react-native';

// React Native has an additional InteractionManager queue
// Runs tasks after animations and interactions complete

function HeavyDataScreen(): React.JSX.Element {
  useEffect(() => {
    // ❌ Blocks UI thread if data processing is heavy
    // processLargeDataset(rawData);

    // ✅ Defer heavy work until after animations complete
    const task = InteractionManager.runAfterInteractions(() => {
      processLargeDataset(rawData);
    });

    return () => task.cancel();
  }, []);

  return (/* ... */);
}

// requestAnimationFrame — runs before next frame paint
function SmoothAnimation(): void {
  let start: number | null = null;

  function animate(timestamp: number): void {
    if (!start) start = timestamp;
    const elapsed = timestamp - start;

    // Update animation based on elapsed time
    updateAnimationFrame(elapsed);

    if (elapsed < 1000) {
      requestAnimationFrame(animate);
    }
  }

  requestAnimationFrame(animate);
}

Queue Priority (highest to lowest):

  1. Call stack (synchronous code)
  2. Microtask queue (Promise.then, queueMicrotask, MutationObserver)
  3. Macrotask queue (setTimeout, setInterval, I/O, UI events)
  4. InteractionManager queue (React Native specific)
  5. requestIdleCallback (when JS thread is idle)

TypeScript Generics

Simple Explanation:

Generics let you write reusable code that works with multiple types while maintaining type safety. Instead of hardcoding a type, you use a type parameter (like a function parameter, but for types).

Real-World Analogy:

Generics are like adjustable wrenches. A fixed wrench (specific type) only fits one bolt size. An adjustable wrench (generic) fits any bolt size while still gripping firmly (type-safe).

// Without generics — must duplicate for each type
function getFirstString(arr: string[]): string {
  return arr[0];
}

function getFirstNumber(arr: number[]): number {
  return arr[0];
}

// With generics — one function, any type
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstName = getFirst(['Alice', 'Bob']);     // string | undefined
const firstAge = getFirst([25, 30, 35]);          // number | undefined
const firstUser = getFirst<User>(users);           // User | undefined

Generics in React Native:

import React, { useState, useCallback } from 'react';
import { FlatList, ListRenderItem } from 'react-native';

// Generic API hook — works with any data type
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const refetch = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      const json: T = await response.json();
      setData(json);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, [url]);

  return { data, loading, error, refetch };
}

// Generic list component
interface ListProps<T> {
  data: T[];
  renderItem: ListRenderItem<T>;
  keyExtractor: (item: T) => string;
  onItemPress?: (item: T) => void;
}

function GenericList<T>({
  data,
  renderItem,
  keyExtractor,
}: ListProps<T>): React.JSX.Element {
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
    />
  );
}

// Generic storage utility
async function getStoredValue<T>(key: string, defaultValue: T): Promise<T> {
  try {
    const raw = await AsyncStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : defaultValue;
  } catch {
    return defaultValue;
  }
}

// Constrained generics — T must have an id property
interface Identifiable {
  id: string;
}

function findById<T extends Identifiable>(items: T[], id: string): T | undefined {
  return items.find((item) => item.id === id);
}

Utility Types (Partial, Pick, Omit)

Simple Explanation:

TypeScript utility types transform existing types into new types. They're built-in type functions that save you from writing repetitive type definitions.

Real-World Analogy:

Utility types are like Photoshop filters for types. You start with a photo (original type) and apply a filter (utility type) to get a modified version — cropped (Pick), with background removed (Omit), or semi-transparent (Partial).

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: Date;
  updatedAt: Date;
}

// Partial<T> — all properties become optional
// Use case: update forms where you send only changed fields
type UserUpdate = Partial<User>;
// Equivalent to: { id?: string; name?: string; email?: string; ... }

const update: UserUpdate = { name: 'Alice' }; // ✅ Only name required

// Pick<T, Keys> — select specific properties
// Use case: API response that returns only public user data
type PublicUser = Pick<User, 'id' | 'name' | 'avatar'>;
// { id: string; name: string; avatar: string }

// Omit<T, Keys> — exclude specific properties
// Use case: creating a new user (no id or timestamps yet)
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
// { name: string; email: string; avatar: string; role: 'admin' | 'user' | 'guest' }

// Combined utilities for complex transformations
type UserFormData = Partial<Pick<User, 'name' | 'email' | 'avatar'>>;

Production Patterns in React Native:

// Navigation params with Pick
type RootStackParamList = {
  Home: undefined;
  Profile: Pick<User, 'id'>;
  EditProfile: Partial<Pick<User, 'name' | 'email' | 'avatar'>>;
  Settings: undefined;
};

// API layer with Omit for creation endpoints
interface Post {
  id: string;
  title: string;
  body: string;
  authorId: string;
  createdAt: string;
  tags: string[];
}

type CreatePostPayload = Omit<Post, 'id' | 'createdAt'>;
type UpdatePostPayload = Partial<Omit<Post, 'id' | 'createdAt' | 'authorId'>>;

async function createPost(payload: CreatePostPayload): Promise<Post> {
  return apiRequest<Post>('/posts', { method: 'POST', body: payload });
}

async function updatePost(id: string, payload: UpdatePostPayload): Promise<Post> {
  return apiRequest<Post>(`/posts/${id}`, { method: 'PATCH', body: payload });
}

// Component props with Required to make optional fields mandatory
type ButtonProps = {
  label: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
};

// Force all props to be required (useful for wrapper components)
type RequiredButtonProps = Required<ButtonProps>;

// Record for dynamic key-value types
type UserPreferences = Record<string, boolean | string | number>;
// { theme: 'dark', notifications: true, fontSize: 16, ... }

Optional Chaining

Simple Explanation:

Optional chaining (?.) lets you safely access nested properties without checking each level for null/undefined. If any part of the chain is null or undefined, the expression short-circuits and returns undefined.

Real-World Analogy:

Optional chaining is like asking "Does the house have a garage? And if so, does the garage have a car?" instead of checking each question separately and stopping if any answer is "no."

interface ApiResponse {
  data?: {
    user?: {
      profile?: {
        name?: string;
        settings?: {
          theme?: 'light' | 'dark';
          notifications?: boolean;
        };
      };
    };
  };
  error?: string;
}

// ❌ Without optional chaining — verbose and error-prone
function getThemeOld(response: ApiResponse): string {
  if (response.data && response.data.user && response.data.user.profile &&
      response.data.user.profile.settings && response.data.user.profile.settings.theme) {
    return response.data.user.profile.settings.theme;
  }
  return 'light';
}

// ✅ With optional chaining — clean and safe
function getTheme(response: ApiResponse): string {
  return response.data?.user?.profile?.settings?.theme ?? 'light';
}

// Optional chaining with function calls
user.getProfile?.(); // Only calls if getProfile exists

// Optional chaining with array access
const firstTag = post?.tags?.[0];

// Nullish coalescing (??) — default value for null/undefined only
const name = user?.profile?.name ?? 'Anonymous';
const count = data?.count ?? 0; // 0 is valid, don't replace with default

React Native Usage:

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

interface Product {
  id: string;
  name: string;
  price: number;
  images?: string[];
  seller?: {
    name?: string;
    rating?: number;
    verified?: boolean;
  };
  reviews?: Array<{ rating: number; comment: string }>;
}

function ProductCard({ product }: { product: Product }): React.JSX.Element {
  return (
    <View style={styles.card}>
      {/* Safe image access — won't crash if images is undefined */}
      {product.images?.[0] && (
        <Image source={{ uri: product.images[0] }} style={styles.image} />
      )}

      <Text style={styles.name}>{product.name}</Text>
      <Text style={styles.price}>${product.price.toFixed(2)}</Text>

      {/* Nested optional access for seller info */}
      {product.seller?.verified && (
        <Text style={styles.verified}> Verified Seller</Text>
      )}

      {/* Optional chaining + nullish coalescing */}
      <Text>
        Rating: {product.reviews?.[0]?.rating ?? 'No reviews'}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, backgroundColor: '#FFF', borderRadius: 8 },
  image: { width: '100%', height: 200, borderRadius: 8 },
  name: { fontSize: 18, fontWeight: '600', marginTop: 8 },
  price: { fontSize: 16, color: '#007AFF' },
  verified: { fontSize: 12, color: '#34C759' },
});

Destructuring

Simple Explanation:

Destructuring lets you extract values from arrays or properties from objects into distinct variables with concise syntax.

Real-World Analogy:

Destructuring is like unpacking a suitcase. Instead of reaching in and pulling out each item one by one by name, you have a checklist that automatically assigns each item to its place.

// Object destructuring
const user = { id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin' as const };

const { name, email } = user;
const { role: userRole } = user; // Rename during destructuring
const { avatar = 'default.png' } = user as typeof user & { avatar?: string }; // Default value

// Array destructuring
const colors = ['red', 'green', 'blue'];
const [primary, secondary] = colors;
const [, , tertiary] = colors; // Skip elements

// Nested destructuring
const response = {
  data: { users: [{ id: '1', name: 'Alice' }] },
  meta: { total: 1, page: 1 },
};
const { data: { users: [firstUser] }, meta: { total } } = response;

// Function parameter destructuring (very common in React Native)
function greet({ name, greeting = 'Hello' }: { name: string; greeting?: string }): string {
  return `${greeting}, ${name}!`;
}

React Native Patterns:

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';

type RootStackParamList = {
  Profile: { userId: string; showEdit?: boolean };
};

// Destructure navigation props — standard RN pattern
type ProfileScreenProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;

export function ProfileScreen({ navigation, route }: ProfileScreenProps): React.JSX.Element {
  // Destructure route params with defaults
  const { userId, showEdit = false } = route.params;

  // Destructure hook return values
  const { data: user, isLoading, error, refetch } = useUser(userId);

  // Destructure styles for conditional rendering
  const [containerStyle, textStyle] = isLoading
    ? [styles.container, styles.loadingText]
    : [styles.container, styles.normalText];

  if (error) {
    return <Text>Error: {error.message}</Text>;
  }

  return (
    <View style={containerStyle}>
      {user && (
        <>
          {/* Destructure nested user properties inline */}
          <Text style={textStyle}>{user.name}</Text>
          <Text>{user.email}</Text>
        </>
      )}
    </View>
  );
}

// Rest/spread in destructuring
function updateSettings(
  current: Record<string, unknown>,
  changes: Record<string, unknown>,
): Record<string, unknown> {
  const { theme, ...otherChanges } = changes;
  return { ...current, ...otherChanges, ...(theme && { theme }) };
}

Spread Operator

Simple Explanation:

The spread operator (...) expands iterables (arrays, objects) into individual elements. It's used for copying, merging, and passing elements.

Real-World Analogy:

The spread operator is like pouring a bag of LEGOs onto a table — each piece becomes individually accessible, and you can combine pieces from multiple bags into one pile.

// Array spread
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5] — copy + append
const arr3 = [0, ...arr1];     // [0, 1, 2, 3] — prepend

// Object spread
const defaults = { theme: 'light', fontSize: 14, lang: 'en' };
const userPrefs = { theme: 'dark', fontSize: 16 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 16, lang: 'en' } — userPrefs overrides defaults

// Function arguments
const numbers = [5, 2, 8, 1, 9];
const max = Math.max(...numbers); // 9

React Native State Updates:

import React, { useState, useCallback } from 'react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

function TodoManager(): React.JSX.Element {
  const [todos, setTodos] = useState<Todo[]>([]);

  // Add item — spread existing array + new item
  const addTodo = useCallback((text: string) => {
    setTodos((prev) => [
      ...prev,
      { id: Date.now().toString(), text, completed: false },
    ]);
  }, []);

  // Update item — spread with mapped replacement
  const toggleTodo = useCallback((id: string) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    );
  }, []);

  // Delete item — spread filtered array
  const deleteTodo = useCallback((id: string) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);

  // Merge objects in state
  const [filters, setFilters] = useState({ status: 'all', sort: 'date' });
  const updateFilter = (key: string, value: string) => {
    setFilters((prev) => ({ ...prev, [key]: value }));
  };

  return (/* ... */);
}

// Spread in StyleSheet composition
const baseCard = { padding: 16, borderRadius: 8, backgroundColor: '#FFF' };
const elevatedCard = { ...baseCard, shadowOpacity: 0.1, elevation: 4 };
const flatCard = { ...baseCard, borderWidth: 1, borderColor: '#E0E0E0' };

== vs ===

Simple Explanation:

== (loose equality) compares values with type coercion — it converts types before comparing. === (strict equality) compares both value and type without coercion. Always use === in TypeScript/React Native.

Real-World Analogy:

== is like comparing "5 apples" with "5" — it ignores that one is fruit and one is a number. === checks both the quantity AND what kind of thing it is.

// Loose equality (==) — type coercion happens
console.log(5 == '5');      // true  (string '5' coerced to number 5)
console.log(0 == false);    // true  (false coerced to 0)
console.log('' == false);   // true  (both coerced to 0)
console.log(null == undefined); // true
console.log([] == false);   // true  (WAT?!)
console.log([] == ![]);     // true  (infamous JavaScript quirk)

// Strict equality (===) — no coercion
console.log(5 === '5');     // false (number !== string)
console.log(0 === false);   // false (number !== boolean)
console.log('' === false);  // false
console.log(null === undefined); // false
console.log([] === false);  // false

Best Practices:

// ✅ Always use === and !==
function isAdmin(role: string): boolean {
  return role === 'admin';
}

// ✅ For null/undefined checks, use explicit checks or nullish coalescing
function getDisplayName(user: User | null | undefined): string {
  if (user === null || user === undefined) return 'Guest';
  return user.name;
}

// ✅ Or use optional chaining + nullish coalescing
function getDisplayNameConcise(user: User | null | undefined): string {
  return user?.name ?? 'Guest';
}

// ✅ For checking null OR undefined, use == null (rare acceptable use of ==)
function isNil(value: unknown): value is null | undefined {
  return value == null; // Checks both null and undefined
}

// ❌ Avoid
if (count == 0) { /* might match false, '', null */ }

// ✅ Prefer
if (count === 0) { /* only matches number 0 */ }

Map vs Object

Simple Explanation:

Both Map and plain objects store key-value pairs, but Map is optimized for frequent additions/deletions, accepts any type as keys, maintains insertion order, and has a built-in size property.

Real-World Analogy:

  • Object = A labeled filing cabinet where labels must be strings/numbers. Good for fixed structures (like a form).
  • Map = A dynamic warehouse with RFID tags. Any item can be a key (even other objects). Easy to add/remove items and you always know the count.
// Object — best for fixed-shape data
interface UserProfile {
  name: string;
  email: string;
  age: number;
}
const profile: UserProfile = { name: 'Alice', email: 'a@b.com', age: 30 };

// Map — best for dynamic collections with non-string keys
const userCache = new Map<string, User>();
userCache.set('user-1', { id: 'user-1', name: 'Alice' });
userCache.set('user-2', { id: 'user-2', name: 'Bob' });

console.log(userCache.size);       // 2
console.log(userCache.has('user-1')); // true
userCache.delete('user-2');

// Map with object keys
const elementHandlers = new Map<HTMLElement, EventListener>();

When to Use Each in React Native:

// ✅ Use Object for: API payloads, component props, StyleSheet, config
const apiConfig = {
  baseUrl: 'https://api.example.com',
  timeout: 10_000,
  retries: 3,
};

// ✅ Use Map for: caches, registries, lookups with dynamic keys
class EventBus {
  private listeners = new Map<string, Set<(...args: unknown[]) => void>>();

  on(event: string, callback: (...args: unknown[]) => void): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);

    // Return unsubscribe function
    return () => this.listeners.get(event)?.delete(callback);
  }

  emit(event: string, ...args: unknown[]): void {
    this.listeners.get(event)?.forEach((cb) => cb(...args));
  }
}

// ✅ Use Map for: memoization in performance-critical paths
function createMemoizer<T, R>() {
  const cache = new Map<string, R>();

  return (key: string, compute: () => R): R => {
    if (cache.has(key)) return cache.get(key)!;
    const result = compute();
    cache.set(key, result);
    return result;
  };
}

// Comparison table for interviews:
// | Feature          | Object       | Map                |
// |------------------|-------------|--------------------|
// | Key types        | string/symbol| any                |
// | Insertion order  | mostly*      | guaranteed         |
// | Size             | manual count | .size property     |
// | Iteration        | for...in     | .forEach, for...of |
// | Prototype        | has one      | clean (no inherited keys) |
// | JSON.stringify   | yes          | no (need conversion)|
// | Performance      | good for small| better for frequent add/delete |

Interview Questions & Answers

Q1: What are closures and how are they used in React Native?

Answer:

A closure is a function that retains access to variables from its lexical scope, even after the outer function has returned. In JavaScript, every function creates a closure over the variables in its surrounding scope.

How Closures Work:

  1. Outer function executes and creates local variables
  2. Inner function is defined, capturing references to outer variables
  3. Outer function returns the inner function
  4. Inner function still accesses outer variables via closure

React Native Use Cases:

  1. Event handlers — capture state and props
  2. Custom hooks — encapsulate stateful logic
  3. Callbacks — pass data to async operations
  4. Module pattern — create private variables
// Custom hook using closures for encapsulation
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Closure captures setDebouncedValue and value
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer); // Cleanup closure
  }, [value, delay]);

  return debouncedValue;
}

Stale Closure Problem: The most common closure issue in React. When a useEffect or useCallback captures an old state value because dependencies are missing. Fix with functional updates (setState(prev => ...)) or proper dependency arrays.

Q2: Explain async/await vs Promises. When would you use each?

Answer:

Promises are objects representing the eventual completion (or failure) of an async operation. They have three states: pending, fulfilled, rejected.

Async/await is syntactic sugar built on Promises that allows writing async code in a synchronous style using the async keyword on functions and await to pause execution until a Promise resolves.

When to use Promises directly:

  • Promise.all() for parallel operations
  • Promise.race() for timeouts and first-to-complete patterns
  • Chaining with .then() when you need fine-grained control
  • Creating new Promise wrappers around callback-based APIs

When to use async/await:

  • Sequential async operations (step 1 → step 2 → step 3)
  • Try/catch error handling (cleaner than .catch())
  • Most React Native API calls and data fetching
  • Async functions in useEffect and event handlers
// Promise.all — parallel (use when operations are independent)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.race — timeout pattern
const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000),
  ),
]);

// Sequential — async/await (use when steps depend on each other)
const token = await login(credentials);
const profile = await fetchProfile(token);
const settings = await fetchSettings(profile.id);

Key Interview Point: Async/await does NOT make code synchronous. It still runs on the event loop. await pauses the current async function but other code continues executing.

Q3: Explain the JavaScript event loop.

Answer:

The event loop is JavaScript's concurrency model. Since JS is single-threaded (one call stack), the event loop enables non-blocking I/O by delegating async work and processing callbacks when the stack is empty.

Components:

  1. Call Stack — executes synchronous code (LIFO)
  2. Web/API APIs — browser/RN handles async ops (setTimeout, fetch, etc.)
  3. Microtask Queue — Promise callbacks, queueMicrotask (higher priority)
  4. Macrotask Queue — setTimeout, setInterval, I/O callbacks (lower priority)

Execution Order:

  1. Execute all synchronous code on the call stack
  2. Process ALL microtasks (Promise.then callbacks)
  3. Process ONE macrotask (setTimeout callback)
  4. Repeat steps 2-3

React Native Specifics:

  • JS thread runs the event loop
  • Native modules handle I/O on separate native threads
  • InteractionManager.runAfterInteractions() adds a lower-priority queue
  • Heavy JS work blocks the UI — use requestAnimationFrame or defer with InteractionManager
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Output: A, D, C, B

Q4: What are TypeScript generics and why are they useful?

Answer:

Generics allow creating reusable components/functions that work with a variety of types rather than a single one, while maintaining full type safety. The type is specified as a parameter (in angle brackets) at the call site or definition.

Benefits:

  • Type safety without sacrificing flexibility
  • Code reuse — one implementation for many types
  • Better IDE support — autocomplete and error checking
  • Self-documenting — types express intent

Common Patterns:

// Generic function
function identity<T>(arg: T): T { return arg; }

// Generic interface
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Generic React component
function Select<T extends string | number>(props: {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  renderOption: (option: T) => string;
}) { /* ... */ }

// Multiple type parameters
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

React Native Interview Example: Explain how you'd type a generic useFetch<T> hook or a reusable List<T> component — this demonstrates both generic understanding and RN patterns.

Q5: Explain Partial, Pick, and Omit utility types with examples.

Answer:

These are built-in TypeScript utility types that transform existing types:

Partial — Makes all properties of T optional. Used for update operations where you only send changed fields.

Pick<T, K> — Creates a type with only the specified properties from T. Used for selecting subsets of data.

Omit<T, K> — Creates a type with all properties of T except the specified ones. Used for creation payloads that exclude server-generated fields.

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  category: string;
  stock: number;
  createdAt: Date;
}

// Partial — for PATCH requests (update any subset of fields)
type ProductUpdate = Partial<Pick<Product, 'name' | 'price' | 'description' | 'stock'>>;

// Pick — for list views (show only summary fields)
type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;

// Omit — for POST requests (exclude server-generated fields)
type CreateProduct = Omit<Product, 'id' | 'createdAt'>;

// Real-world API service
async function updateProduct(id: string, changes: ProductUpdate): Promise<Product> {
  return apiRequest(`/products/${id}`, { method: 'PATCH', body: changes });
}

Other Important Utility Types:

  • Required<T> — makes all properties required
  • Readonly<T> — makes all properties readonly
  • Record<K, V> — creates object type with keys K and values V
  • ReturnType<T> — extracts return type of a function
  • Parameters<T> — extracts parameter types of a function

Q6: What is optional chaining and nullish coalescing?

Answer:

Optional chaining (?.) safely accesses nested object properties. If any reference in the chain is null or undefined, the expression short-circuits and returns undefined instead of throwing an error.

Nullish coalescing (??) returns the right-hand operand when the left-hand operand is null or undefined (not for other falsy values like 0, '', or false).

// Optional chaining variants
obj?.property          // Safe property access
obj?.[expression]      // Safe bracket access
func?.(args)           // Safe function call
arr?.[0]               // Safe array access

// Nullish coalescing vs OR
const value1 = null ?? 'default';     // 'default' (null triggers ??)
const value2 = 0 ?? 'default';        // 0 (0 is NOT nullish)
const value3 = null || 'default';     // 'default'
const value4 = 0 || 'default';        // 'default' (0 is falsy, triggers ||)

// Combined — very common pattern
const theme = user?.settings?.theme ?? 'light';
const count = data?.items?.length ?? 0;

Key Difference from ||: Use ?? when 0, '', or false are valid values. Use || when you want to fallback for any falsy value.

Q7: Explain destructuring in JavaScript/TypeScript.

Answer:

Destructuring extracts values from arrays or properties from objects into standalone variables.

Object Destructuring:

const { name, age, ...rest } = user;           // Basic + rest
const { name: userName } = user;                // Rename
const { role = 'guest' } = user;                // Default value
const { address: { city } } = user;             // Nested

Array Destructuring:

const [first, second, ...remaining] = items;    // Basic + rest
const [, , third] = items;                       // Skip elements
const [a, b = 10] = [1];                         // Default value
[a, b] = [b, a];                                 // Swap variables

React Native Context:

  • Destructure props in function signatures: function Screen({ navigation, route })
  • Destructure hook returns: const { data, loading } = useFetch(url)
  • Destructure route params: const { userId } = route.params
  • Destructure StyleSheet arrays: style={[styles.base, isActive && styles.active]}

Q8: How does the spread operator work?

Answer:

The spread operator (...) expands iterables into individual elements. It creates shallow copies — nested objects/arrays are still referenced, not deeply cloned.

Use Cases:

  1. Immutable state updates (most important in React Native):
setState(prev => ({ ...prev, name: 'New Name' }));
setItems(prev => [...prev, newItem]);
  1. Merging objects:
const merged = { ...defaults, ...overrides };
  1. Copying arrays:
const copy = [...original];
  1. Function arguments:
Math.max(...numbers);

Important: Spread creates shallow copies. For deep cloning, use structuredClone() or a library like lodash's cloneDeep.

const original = { user: { name: 'Alice' } };
const shallow = { ...original };
shallow.user.name = 'Bob';
console.log(original.user.name); // 'Bob' — nested object is shared!

const deep = structuredClone(original);
deep.user.name = 'Charlie';
console.log(original.user.name); // 'Bob' — properly cloned

Q9: What is the difference between == and ===?

Answer:

Operator Name Type Coercion Use
== Loose equality Yes Avoid in production
=== Strict equality No Always use this
!= Loose inequality Yes Avoid
!== Strict inequality No Always use this

Type Coercion Rules for ==:

  • If types match, same as ===
  • null == undefined → true (only case where this is acceptable)
  • Number vs string → string converted to number
  • Boolean vs anything → boolean converted to number (true=1, false=0)
  • Object vs primitive → object converted to primitive via valueOf/toString

TypeScript Context: TypeScript's strict mode and type system largely prevent == issues at compile time. However, always use === as a best practice — it's what linters (ESLint eqeqeq rule) enforce.

Exception: value == null checks for both null and undefined in one expression. This is the one acceptable use of == in modern JavaScript.

Q10: When should you use Map instead of Object?

Answer:

Use Object when:

  • Structure is known and fixed (interfaces, API payloads, props)
  • You need JSON serialization (JSON.stringify)
  • Working with TypeScript interfaces/types
  • Key-value pairs are defined at authoring time

Use Map when:

  • Keys are unknown until runtime (dynamic caching)
  • Keys are not strings (objects, DOM elements, functions)
  • Frequent additions and deletions (better performance)
  • You need guaranteed insertion order iteration
  • You need the .size property
  • Keys should not conflict with inherited properties (Object has prototype chain)
// Map shines in React Native for:

// 1. Component instance registries
const refMap = new Map<string, React.RefObject<View>>();

// 2. Request deduplication
const inflightRequests = new Map<string, Promise<unknown>>();

async function fetchWithDedup<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
  if (inflightRequests.has(key)) {
    return inflightRequests.get(key) as Promise<T>;
  }
  const promise = fetcher();
  inflightRequests.set(key, promise);
  try {
    return await promise;
  } finally {
    inflightRequests.delete(key);
  }
}

// 3. LRU Cache implementation
class LRUCache<K, V> {
  private cache = new Map<K, V>();

  get(key: K): V | undefined {
    const value = this.cache.get(key);
    if (value !== undefined) {
      this.cache.delete(key);
      this.cache.set(key, value); // Move to end (most recent)
    }
    return value;
  }

  set(key: K, value: V): void {
    if (this.cache.size >= 100) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey !== undefined) this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
}

Performance Note: For small, static datasets (< 10 entries), Object and Map perform similarly. Map pulls ahead with frequent mutations and large datasets (100+ entries).


Navigation

Previous: Core Components

Next: Advanced TypeScript