📚 Revision Notes v2

JS & React Native
Patterns + SOLID

// Design Patterns · SOLID Principles · Quick Diagrams · React Native Deep Dive

10
Patterns
5
SOLID
3
Categories
25+
RN Examples
Quick Revision

Pattern Diagrams at a Glance

// Read these first — understand the SHAPE of each pattern before diving into code

Creational Singleton 1 instance
new Counter() ──► instance?YES ◄──┘ └──► NO │ │ return old create & store │ │ └────────┬───────────┘ ▼ SAME instance counter1 === counter2 ✓
Creational Factory dynamic create
consumer │ ▼ factory('car')switch(type) ├── 'car' ──► new Car() ├── 'truck'──► new Truck() └── 'bus' ──► new Bus() │ ▼ object with same interface
Structural Proxy intercept
caller │ ▼ PROXY ◄── wraps ── Real Object │ │ │ get trap ──► log / validate │ set trap ──► encrypt / guard │ ▼ Real Object.property (caller never touches real obj)
Behavioral Observer 1→many
Subject (has state) │ state changesnotify all ├──► Observer A.update() ├──► Observer B.update() └──► Observer C.update() Observers KNOW the Subject Synchronous · Single-app
Behavioral Pub/Sub decoupled
Publisherpublish('topic', data) │ ▼ EVENT BUS (broker) │ route by topic ├──► Subscriber A └──► Subscriber B No direct reference Async · Cross-app
React/RN Container-Presentation
Container (Screen) │ fetch data │ manage state │ handle errors │ pass props ↓ │ ▼ Presentation (UI) │ only renders │ no state │ no side-effects easy to test in isolation
React/RN Custom Hook reuse logic
useNetworkStatus() ┌──────────────────┐ │ useState │ │ useEffect │ │ NetInfo listener │ │ cleanup on exit │ └──────────────────┘ │ returns { isOnline, type } │ ├──► ComponentA ├──► ComponentB └──► ComponentC logic shared, no duplication
React/RN Provider no prop-drill
<ThemeProvider> ← holds theme │ ├── <Screen> │ ├── <Header> │ │ └── useTheme() ✓ │ └── <Card> │ └── useTheme() ✓ └── <TabBar> └── useTheme() ✓ ANY depth — no props passed
React/RN Render Prop fn as prop
<Mouse render={fn} />Mouse component: │ tracks position │ calls render({ x, y }) │ ▼ fn({ x, y }) └──► consumer renders what IT wants with the data flexibility without HOC
Structural Composition compound
<Card> ├── <Card.Header> │ <Avatar /> <Name /> ├── <Card.Body> │ <Description /> └── <Card.Footer> <Button /> build complex from small flexible · reusable · readable
📋
Master Table

All Patterns Overview

PatternCategoryCore IdeaRN Real-World UseKey Methods
SingletonCreationalOne instance, global accessAxios client, AsyncStorage wrapper, FCM managergetInstance(), constructor check
FactoryCreationalCentralised object creationPlatform buttons, storage selector, payment gatewaycreateX(), switch(type)
ProxyStructuralIntercept object accessEncrypted storage, API logger, image cacheget trap, set trap
ObserverBehavioralSubject notifies observersCart store, keyboard events, AppStatesubscribe, unsubscribe, notify
Pub/SubBehavioralLoose-coupled via brokerCross-screen events, Redux, deep linkspublish(topic), subscribe(topic)
Container-PresentationReact/RNLogic vs UI separationScreen (smart) + UI Component (dumb)props passing, no state in presentation
Custom HookReact/RNReusable stateful logicuseLocation, useBluetooth, useNetInfouseState, useEffect, cleanup
ProviderReact/RNGlobal data without prop drillingTheme, Auth, i18n, NavigationcreateContext, Provider, useContext
Render PropReact/RNShare logic via function propPermissionGate, FlatList renderItemrender={fn}, children as function
CompositionStructuralBuild from small piecesCompound components, HOCs, wrappersCard.Header, Card.Body, Card.Footer
🏗️
Category 1

Creational Patterns

Singleton Pattern
CreationalReact Native
🧠 Simple Analogy

Think of a country's President — there can only be ONE at a time. If you ask for "the president", you always get the same person. Even if 10 people ask, they all refer to the same person. That's Singleton.

Purpose

Ensures a class has only one instance ever created, and provides a global access point to it.

Core Mechanism

Constructor checks ClassName.instance — if it exists, returns it. Otherwise creates and stores it.

Private Fields

Use #field (ES2022) to prevent external mutation of the instance's internal state.

Thread Safety

JS is single-threaded so race conditions aren't an issue. In React Native, module caching handles singleton naturally via import.

Detailed Points
Module-level singleton: In Node.js / React Native, every imported module is cached — so an exported object is already a singleton by nature.
Static instance property: Stored on the class itself (MyClass.instance), not on the prototype, so it survives across new calls.
Return from constructor: Returning an object from a constructor replaces the newly created this — used to return the old instance.
When NOT to use: Avoid for things that need multiple configs (e.g., two different API bases) — use Factory or DI instead.
Testing trap: Singleton state persists between tests — always reset or mock the instance in your test setup.
📱 React Native Applications
  • Axios API Client — create once with baseURL + interceptors, reuse across all screens
  • AsyncStorage / MMKV wrapper — one controller, centralised read/write logic
  • Firebase Analytics — initialise once, log events from anywhere
  • WebSocket Manager — single persistent connection, all screens subscribe to it
  • FCM Token Manager — manage push notification token in one place
  • App Config — env variables (API_URL, feature flags) loaded once at startup
⚠ Common Pitfalls
  • Shared mutable state = hidden coupling between screens — hard to debug
  • Singleton makes unit testing harder — use dependency injection for testability
  • Don't use for UI state — use React state / Context instead
singleton.ts — Axios Client (React Native)
// ✅ Module-level singleton (simplest RN approach)
import axios, { AxiosInstance } from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

class ApiClient {
  private static instance: ApiClient;
  private http: AxiosInstance;

  private constructor() {
    // Private constructor — blocks `new ApiClient()` from outside
    this.http = axios.create({
      baseURL: 'https://api.myapp.com/v1',
      timeout: 10000,
      headers: { 'Content-Type': 'application/json' },
    });

    // Add request interceptor — attach token on every request
    this.http.interceptors.request.use(async (config) => {
      const token = await AsyncStorage.getItem('@auth_token');
      if (token) config.headers.Authorization = `Bearer ${token}`;
      return config;
    });

    // Add response interceptor — handle 401 globally
    this.http.interceptors.response.use(
      res => res,
      async err => {
        if (err.response?.status === 401) {
          await AsyncStorage.removeItem('@auth_token');
          // Redirect to login — navigate globally
        }
        return Promise.reject(err);
      }
    );
  }

  // 🔑 Only way to get the instance
  public static getInstance(): ApiClient {
    if (!ApiClient.instance) {
      ApiClient.instance = new ApiClient();
    }
    return ApiClient.instance;
  }

  get<T>(url: string)           { return this.http.get<T>(url); }
  post<T>(url: string, data: any) { return this.http.post<T>(url, data); }
  put<T>(url: string, data: any)  { return this.http.put<T>(url, data); }
  del<T>(url: string)           { return this.http.delete<T>(url); }
}

// Usage — same instance everywhere in app
const api = ApiClient.getInstance();
const users = await api.get<User[]>('/users');
const profile = await api.post('/profile', { name: 'John' });
Factory Pattern
CreationalReact Native
🧠 Simple Analogy

Think of a restaurant kitchen — you say "I want a burger" (factory call). The kitchen (factory) figures out whether to make a veggie burger or chicken burger based on your preference. You don't care HOW it's made, you just get a burger.

Purpose

Centralises object creation. The what is decided at runtime, not compile time.

Factory vs Constructor

Constructor always makes the same type. Factory decides which type based on a condition.

Factory vs Singleton

Factory returns different instances each call. Singleton always returns the same one.

Detailed Points
Two forms: Factory Function (simple function that returns object) and Factory Class (class with create() method — better for complex hierarchies).
Open/Closed benefit: Add new types to the switch/map without changing consumer code — just register the new type in the factory.
Abstract Factory: A factory of factories — creates families of related objects (e.g., iOS theme factory creates iOS-styled Button, Input, Modal).
Common pattern: Replace switch(type) with a registry Map for O(1) lookup and easy extension without changing factory code.
📱 React Native Applications
  • Platform FactoryPlatform.OS === 'ios' → IOSButton, else AndroidButton
  • Storage Factorysensitive=true → SecureStore, else AsyncStorage, or MMKV for perf
  • Notification Factory — local notification vs push vs in-app banner based on app state
  • Payment Factory — Stripe (US), Razorpay (IN), PayPal (EU) based on user region
  • Analytics Factory — Firebase (prod) vs ConsoleLogger (dev) based on __DEV__
factory.ts — Storage Factory (React Native)
// Storage interface — all implementations must follow this
interface StorageDriver {
  get(key: string): Promise<string | null>;
  set(key: string, val: string): Promise<void>;
  remove(key: string): Promise<void>;
}

// Three concrete implementations
class AsyncStorageDriver implements StorageDriver { /* ... */ }
class MMKVDriver         implements StorageDriver { /* ... */ }
class SecureStoreDriver  implements StorageDriver { /* ... */ }

// ✅ Factory with registry Map (better than switch)
type StorageType = 'async' | 'mmkv' | 'secure';

const registry: Record<StorageType, () => StorageDriver> = {
  async:  () => new AsyncStorageDriver(),
  mmkv:   () => new MMKVDriver(),
  secure: () => new SecureStoreDriver(),
};

function createStorage(type: StorageType): StorageDriver {
  const factory = registry[type];
  if (!factory) throw new Error(`Unknown storage type: ${type}`);
  return factory();
}

// Usage — decision made at call site, implementation hidden
const tokenStorage  = createStorage('secure'); // biometric protect
const cacheStorage  = createStorage('mmkv');   // speed priority
const prefStorage   = createStorage('async');  // general purpose

// All use same interface — consumer code is identical
await tokenStorage.set('jwt', token);
const jwt = await tokenStorage.get('jwt');
🔧
Category 2

Structural Patterns

Proxy Pattern
StructuralReact Native
🧠 Simple Analogy

Think of a hotel receptionist — you never talk to the hotel owner directly. The receptionist (proxy) intercepts your request, checks if you're a guest, logs your visit, and then forwards it. They can even deny access if needed.

Purpose

A surrogate that controls access to another object. Add behaviour (logging, caching, validation) without touching original.

JS Built-in

new Proxy(target, handler) — handler defines traps: get, set, has, apply, construct, etc.

get Trap

Fires when any property is READ from the proxy. Can return modified values, log access, or throw for forbidden keys.

set Trap

Fires when any property is WRITTEN. Can validate types, encrypt values, log mutations. Must return true to confirm write.

Proxy Trap Types (Important for Interview)
get(target, prop, receiver) — intercept property read
set(target, prop, value, receiver) — intercept property write, must return true
has(target, prop) — intercept in operator ('name' in obj)
deleteProperty(target, prop) — intercept delete obj.name
apply(target, thisArg, args) — intercept function calls (proxy on a function)
Reflect API: Always use Reflect.get(target, prop) inside traps to maintain default behaviour cleanly.
📱 React Native Applications
  • Encrypted AsyncStorage — proxy transparently encrypts on set, decrypts on get
  • API Logger — proxy Axios to log every request/response in dev mode
  • Image Cache Proxy — serve from in-memory cache; fetch from CDN on miss
  • Permission Proxy — check permission before allowing camera/mic access calls
  • Rate Limit Proxy — block excessive API calls within a time window
proxy.ts — API Logger + Rate Limiter (RN Dev Tools)
// Proxy that logs API calls in dev and rate-limits in prod
function createApiProxy(apiService: ApiService): ApiService {
  const callCounts = new Map<string, { count: number; resetAt: number }>();
  const RATE_LIMIT = 10; // max 10 calls per endpoint per minute

  return new Proxy(apiService, {
    get(target, prop: keyof ApiService) {
      const original = target[prop];
      if (typeof original !== 'function') return original;

      return async (url: string, ...args: any[]) => {
        // 📊 Rate limit check
        const key = `${String(prop)}:${url}`;
        const now = Date.now();
        const entry = callCounts.get(key) || { count: 0, resetAt: now + 60000 };

        if (now > entry.resetAt) { entry.count = 0; entry.resetAt = now + 60000; }
        if (entry.count >= RATE_LIMIT) throw new Error(`Rate limit: ${key}`);

        entry.count++;
        callCounts.set(key, entry);

        // 🔍 Dev logging
        if (__DEV__) {
          console.log(`[API] ${String(prop).toUpperCase()} ${url}`, args[0] || '');
          const start = Date.now();
          const result = await original.call(target, url, ...args);
          console.log(`[API] ✓ ${Date.now() - start}ms`, result);
          return result;
        }

        return original.call(target, url, ...args);
      };
    },
  });
}

// Usage — transparent to all consumers
const api = createApiProxy(ApiClient.getInstance());
// Logs in dev, rate-limited in prod — zero changes to consumer
🔄
Category 3

Behavioral Patterns

Observer Pattern
BehavioralReact Native
🧠 Simple Analogy

Think of a YouTube channel — when you subscribe, you get notified on every new video. The channel (subject) maintains a list of subscribers (observers). Any change (new video) notifies ALL subscribers automatically.

Subject Role

Holds state. Maintains observer list. Calls notify() on every state change. Also called Observable.

Observer Role

Has an update(data) method. Gets called by Subject. Can subscribe/unsubscribe at any time.

Memory Leak Warning

Always unsubscribe in useEffect cleanup. Forgetting this = components update after unmount = crash.

Vs Pub/Sub

Observer: tight coupling, synchronous, single-app. Pub/Sub: loose (via broker), async, cross-app.

Detailed Points
Push vs Pull: In push model, subject sends data to observers. In pull model, observers query subject for latest data. Push is more common.
React's useState: React's own state is an Observer implementation — components re-render (notify) when state changes.
RxJS / Zustand subscriptions: Both are Observer pattern implementations — store.subscribe(fn) is classic Observer.
EventEmitter (Node/RN): Node's built-in EventEmitter is the canonical JS Observer implementation.
Cleanup is critical in RN: AppState.addEventListener, Keyboard.addListener, NetInfo.addEventListener all return cleanup functions — always call them.
📱 React Native — Built-in Observers
  • AppState.addEventListener('change', fn) — foreground/background transitions
  • Keyboard.addListener('keyboardDidShow', fn) — keyboard height tracking
  • NetInfo.addEventListener(fn) — online/offline detection
  • navigation.addListener('focus', fn) — screen focus events
  • Dimensions.addEventListener('change', fn) — orientation changes
  • Custom: Cart Store, Auth Store — components observe for changes
observer.ts — Generic Observable Store (React Native)
// Generic Observable — can be reused for any store
class Observable<T> {
  private observers: Set<(data: T) => void> = new Set();
  protected state: T;

  constructor(initialState: T) { this.state = initialState; }

  subscribe(fn: (data: T) => void): () => void {
    this.observers.add(fn);
    fn(this.state); // immediately emit current state
    return () => this.observers.delete(fn); // returns cleanup
  }

  protected setState(updater: (prev: T) => T) {
    this.state = updater(this.state);
    this.observers.forEach(fn => fn(this.state)); // notify all
  }

  getState() { return this.state; }
}

// ✅ Concrete Cart Store extending Observable
interface CartState { items: CartItem[]; total: number; }

class CartStore extends Observable<CartState> {
  constructor() { super({ items: [], total: 0 }); }

  addItem(item: CartItem) {
    this.setState(prev => ({
      items: [...prev.items, item],
      total: prev.total + item.price,
    }));
  }

  removeItem(id: string) {
    this.setState(prev => {
      const item = prev.items.find(i => i.id === id);
      return {
        items: prev.items.filter(i => i.id !== id),
        total: prev.total - (item?.price || 0),
      };
    });
  }
}

export const cartStore = new CartStore(); // singleton instance

// ✅ React Native hook to use Observable store
function useCart() {
  const [cartState, setCartState] = useState(cartStore.getState());

  useEffect(() => {
    const unsubscribe = cartStore.subscribe(setCartState);
    return unsubscribe; // cleanup prevents memory leak
  }, []);

  return { ...cartState, addItem: cartStore.addItem.bind(cartStore) };
}

// Usage in any component — all sync automatically
function CartBadge() {
  const { items } = useCart();
  return <Badge count={items.length} />;
}
Publish-Subscribe Pattern
BehavioralReact Native
🧠 Simple Analogy

Think of a newspaper subscription — The journalist (publisher) writes articles and gives them to the newspaper office (broker/event bus). Readers (subscribers) subscribe to topics like "Sports" or "Tech". The journalist has NO IDEA who the readers are. Readers have NO IDEA who the journalist is.

Publisher

Sends message to broker with a topic key. Knows nothing about subscribers. Fully decoupled.

Broker / Event Bus

Stores topic→subscribers map. Routes published messages. The ONLY shared dependency.

Subscriber

Registers interest in a topic with the broker. Receives matching messages. Doesn't know the publisher.

Observer vs Pub/Sub — Key Differences
Awareness: In Observer, observers know the Subject. In Pub/Sub, publisher and subscriber are completely unaware of each other.
Coupling: Observer = tight. Pub/Sub = loose (only coupled to broker/bus).
Execution: Observer = synchronous. Pub/Sub = typically asynchronous via message queue.
Scope: Observer = single application. Pub/Sub = can be cross-application, distributed systems.
Redux: Redux is Pub/Sub — dispatch(action) = publish, useSelector(fn) = subscribe. Store = broker.
📱 React Native Applications
  • Cross-screen events: ProductScreen adds to cart → TabBar badge updates (no direct connection)
  • Redux / Zustand: Both are Pub/Sub — dispatch=publish, selector=subscribe, store=broker
  • Deep link routing: URL opens → event published → correct screen subscribes and handles
  • Push notifications: FCM receives message → publishes to 'notification' topic → multiple handlers
  • Socket.IO: Server emits events → clients subscribed to rooms receive them
eventBus.ts — Typed Event Bus (React Native)
// Typed EventBus for React Native — type-safe Pub/Sub
type EventMap = {
  'cart:add':    { productId: string; qty: number; price: number };
  'cart:remove': { productId: string };
  'auth:login':  { userId: string; token: string };
  'auth:logout': void;
  'nav:deeplink': { screen: string; params: Record<string, any> };
};

class TypedEventBus {
  private topics = new Map<string, Set<Function>>();

  subscribe<K extends keyof EventMap>(
    topic: K,
    fn: (payload: EventMap[K]) => void
  ): () => void {
    if (!this.topics.has(topic)) this.topics.set(topic, new Set());
    this.topics.get(topic)!.add(fn);
    return () => this.topics.get(topic)?.delete(fn); // cleanup fn
  }

  publish<K extends keyof EventMap>(topic: K, payload: EventMap[K]) {
    this.topics.get(topic)?.forEach(fn => fn(payload));
  }
}

export const bus = new TypedEventBus();

// ProductScreen.tsx — Publisher (knows nothing about TabBar)
const handleAddToCart = () => {
  bus.publish('cart:add', { productId: 'p-42', qty: 1, price: 29.99 });
};

// TabBar.tsx — Subscriber (knows nothing about ProductScreen)
useEffect(() => {
  const unsub = bus.subscribe('cart:add', ({ qty }) => {
    setBadgeCount(prev => prev + qty);
  });
  return unsub; // cleanup
}, []);
⚛️
Category 4

React / React Native Patterns

Container-Presentation Pattern
StructuralReact Native
🧠 Simple Analogy

Think of a waiter and chef — the chef (container) knows all the business: ingredients, cooking process, timing. The waiter (presentation) only knows how to serve the plate. Swap the chef without retraining the waiter.

Container (Smart)

Has useState, useEffect. Fetches data, handles errors, loading states. Passes everything down as props.

Presentation (Dumb)

Pure function of props. No internal state (or only UI state like hover). Receives callbacks — never creates them.

Benefits
Testability: Test presentation components with mock props — no API mocking needed.
Reusability: Same presentation component used by different containers (live data vs mock data).
Storybook-friendly: Presentation components render perfectly in Storybook with prop controls.
Modern approach: With hooks, container = custom hook (useUserList()). Screen consumes hook + renders presentation.
📱 React Native File Structure
  • screens/HomeScreen.tsx → Container: fetches, manages state, handles nav
  • components/UserCard.tsx → Presentation: pure UI from props
  • components/UserList.tsx → Presentation: renders FlatList from array prop
  • hooks/useUserList.ts → Modern container logic extracted to hook
container-presentation.tsx — Modern Hook Approach (RN)
// ✅ Hook = modern container (replaces class container)
function useUserList() {
  const [users,   setUsers]   = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState<string | null>(null);

  useEffect(() => {
    const load = async () => {
      try {
        const { data } = await api.get<User[]>('/users');
        setUsers(data);
      } catch (e) {
        setError('Failed to load users');
      } finally {
        setLoading(false);
      }
    };
    load();
  }, []);

  const refresh = () => { setLoading(true); };

  return { users, loading, error, refresh };
}

// ✅ Screen = thin orchestration layer
function UserListScreen() {
  const { users, loading, error, refresh } = useUserList();
  return <UserListUI users={users} loading={loading} error={error} onRefresh={refresh} />;
}

// ✅ Presentation = pure UI — no logic at all
function UserListUI({ users, loading, error, onRefresh }: Props) {
  if (loading) return <ActivityIndicator size="large" />;
  if (error)   return <ErrorView message={error} onRetry={onRefresh} />;
  return (
    <FlatList
      data={users}
      keyExtractor={u => u.id}
      renderItem={({ item }) => <UserCard user={item} />}
      refreshControl={<RefreshControl refreshing={false} onRefresh={onRefresh} />}
    />
  );
}
Custom Hook Pattern
BehavioralReact Native
🧠 Simple Analogy

Think of electrical outlets — the wiring (hook logic: useState, useEffect, cleanup) is hidden in the wall. Any device (component) just plugs in and gets power. 10 rooms can use the same wiring pattern without duplicating it.

Rules of Hooks

Must start with use. Only call at top level (no loops/conditions). Only inside function components or other hooks.

What Can Be Extracted

Any stateful logic: API calls, subscriptions, timers, device sensors, permissions, keyboard state, etc.

Hook Architecture for React Native
Data hooks: useUsers(), useProduct(id) — fetch + cache + error + loading
Device hooks: useCamera(), useLocation(), useBluetooth() — native module abstraction
UI hooks: useKeyboard(), useOrientation(), useSafeArea()
State hooks: useToggle(), useDebounce(val, ms), usePrevious(val)
Cleanup pattern: Always return cleanup from useEffect — subscriptions, timers, event listeners.
📱 React Native Hardware Hooks
  • useCamera() — permission request + camera ref + photo capture
  • useLocation() — GPS permission + current coords + watchPosition cleanup
  • useNetworkStatus() — NetInfo subscription + online/offline/type
  • useKeyboard() — keyboard height for adjusting scroll view padding
  • useBackHandler() — Android hardware back button override
  • useAppState() — foreground/background/inactive tracking
  • useDeepLink() — handle incoming URLs / universal links
useLocation.ts — GPS Hook with Full Lifecycle (RN)
import * as Location from 'expo-location';

interface LocationState {
  coords: Location.LocationObject | null;
  error: string | null;
  loading: boolean;
  permissionStatus: 'granted' | 'denied' | 'undetermined';
}

function useLocation(watchMode = false) {
  const [state, setState] = useState<LocationState>({
    coords: null, error: null, loading: true, permissionStatus: 'undetermined',
  });

  useEffect(() => {
    let subscription: Location.LocationSubscription | null = null;

    const init = async () => {
      // 1️⃣ Request permission
      const { status } = await Location.requestForegroundPermissionsAsync();
      setState(s => ({ ...s, permissionStatus: status }));

      if (status !== 'granted') {
        setState(s => ({ ...s, loading: false, error: 'Permission denied' }));
        return;
      }

      if (watchMode) {
        // 2️⃣ Watch mode — continuous updates
        subscription = await Location.watchPositionAsync(
          { accuracy: Location.Accuracy.High, distanceInterval: 10 },
          (location) => setState(s => ({ ...s, coords: location, loading: false }))
        );
      } else {
        // 2️⃣ One-shot — get once
        const location = await Location.getCurrentPositionAsync({});
        setState(s => ({ ...s, coords: location, loading: false }));
      }
    };

    init();

    // 3️⃣ Cleanup — stop watching on unmount
    return () => { subscription?..remove(); };
  }, [watchMode]);

  return state;
}

// Usage
function MapScreen() {
  const { coords, loading, error } = useLocation(true); // watch mode
  if (loading) return <ActivityIndicator />;
  if (error)   return <Text>{error}</Text>;
  return <MapView region={{ lat: coords?.coords.latitude, ... }} />;
}
Provider Pattern
BehavioralReact Native
🧠 Simple Analogy

Think of WiFi in a building — the router (Provider) is set up once at the top. Every room (component) can connect to it anywhere in the building without running a cable through every floor (prop drilling).

How It Works

createContext()<Context.Provider value={...}> → any descendant calls useContext().

Re-render Warning

All consumers re-render when Provider value changes. Split contexts by update frequency to avoid unnecessary re-renders.

Performance Optimisation Tips
Split contexts: Don't put everything in one context. AuthContext (rarely changes) vs UIContext (frequent changes).
Memoize value: Wrap Provider value in useMemo to prevent unnecessary re-renders of all consumers.
Custom hook pattern: Always export useTheme() instead of exposing raw useContext(ThemeContext) — hides implementation detail.
Default value: Always provide a sensible default to createContext(defaultValue) — important for testing components in isolation.
📱 React Native Provider Stack (App.tsx)
  • <GestureHandlerRootView> — required for react-native-gesture-handler
  • <SafeAreaProvider> — notch/island safe area context
  • <ThemeProvider> — dark/light mode, design tokens
  • <AuthProvider> — user session, login/logout, token refresh
  • <LocaleProvider> — language, RTL support
  • <NavigationContainer> — itself is a Provider!
AuthProvider.tsx — Full Auth Context (React Native)
interface AuthState {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  login(email: string, password: string): Promise<void>;
  logout(): Promise<void>;
}

const AuthContext = React.createContext<AuthState>(null!);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user,      setUser]      = useState<User | null>(null);
  const [token,     setToken]     = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Restore session on app launch
  useEffect(() => {
    const restore = async () => {
      const savedToken = await SecureStore.getItemAsync('token');
      if (savedToken) {
        const userData = await api.get<User>('/me');
        setUser(userData.data);
        setToken(savedToken);
      }
      setIsLoading(false);
    };
    restore();
  }, []);

  const login = async (email: string, password: string) => {
    const { data } = await api.post('/auth/login', { email, password });
    await SecureStore.setItemAsync('token', data.token);
    setUser(data.user);
    setToken(data.token);
  };

  const logout = async () => {
    await SecureStore.deleteItemAsync('token');
    setUser(null);
    setToken(null);
  };

  // Memoize to prevent unnecessary re-renders
  const value = useMemo(
    () => ({ user, token, isLoading, login, logout }),
    [user, token, isLoading]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook — safe & semantic
export const useAuth = () => {
  const ctx = React.useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
  return ctx;
};
💎
Category 5

SOLID Principles in React Native

S
Single Responsibility Principle (SRP)
Every module/component/function should have ONE reason to change
🧠 Analogy

A chef should only cook. Not cook + take orders + clean tables + manage billing. When you need to change the menu, only the chef is affected — not the waiter or cleaner.

In React Native
One component = one visual concern: <UserAvatar>, <UserName>, <FollowButton> — not one massive <UserProfile>
One hook = one data concern: useAuth(), useCart(), useProducts() — not one useEverything()
Service files: userService.ts (API calls only), userStore.ts (state only), UserScreen.tsx (UI only)
Utility functions: formatPrice(n), validateEmail(s), parseDate(d) — pure, single-purpose functions
Signal of violation: If your component has 300+ lines, multiple useEffects doing different things, or handles both API + UI + navigation — it violates SRP
📱 Before vs After

OrderScreen: fetches orders, formats prices, handles payment, shows toast, navigates on success, tracks analytics — 6 responsibilities

OrderScreen → orchestrates:

  • useOrders() — data fetching
  • OrderList — renders list
  • paymentService.charge() — payment logic
  • useAnalytics().track() — analytics
O
Open/Closed Principle (OCP)
Open for extension, closed for modification — add without changing existing code
🧠 Analogy

A power strip — you can plug in new devices (extend) without rewiring the strip (modify). The strip's core doesn't change when you add a new device.

In React Native
Variant props: <Button variant="primary" | "outline" | "ghost"> — new variant added via props + StyleSheet map, not by opening Button.tsx
Strategy pattern: Pass sorting/filtering function as prop instead of hardcoding logic inside component
Higher-Order Components: withAuth(Component) adds auth-checking without touching Component
Plugin / Registry pattern: Factory using a registry Map — add new types without modifying factory code
📱 React Native Example

❌ Bad: if(type==='primary') ... else if(type==='outline') ... inside Button — every new type requires opening the file

✅ Good:

  • const styles = { primary: {...}, outline: {...}, ghost: {...} }
  • return <Pressable style={[base, styles[variant]]} />
  • New variant? Add one entry to styles object. Button.tsx untouched.
L
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base type without breaking the app
🧠 Analogy

If you order a vehicle, you should be able to drive whatever shows up — car, truck, bus. They all steer, accelerate, brake. A vehicle that doesn't steer would violate LSP.

In React Native
Component contracts: All Button variants (PrimaryButton, IconButton, GhostButton) must accept the same base props: onPress, disabled, title
TypeScript enforces LSP: Define a base interface; all variants implement it. TypeScript compiler catches violations
Hook contracts: useAsyncStorage() and useMMKV() both return {get, set, remove} — components don't care which is injected
Violation example: IconButton that ignores the disabled prop — screens using Button type would break when receiving IconButton
lsp.ts — Button Contract (TypeScript + RN)
// Base contract — all button variants MUST follow this
interface ButtonProps {
  title: string;
  onPress: () => void;
  disabled?: boolean;
  loading?: boolean;
  testID?: string;
}

// ✅ All variants implement the contract
function PrimaryButton({ title, onPress, disabled, loading }: ButtonProps) {
  return <Pressable onPress={!disabled && !loading ? onPress : undefined}>...</Pressable>;
}

function GhostButton({ title, onPress, disabled, loading }: ButtonProps) {
  // Must honour disabled + loading — or it violates LSP!
  return <Pressable onPress={!disabled && !loading ? onPress : undefined}>...</Pressable>;
}

// Screen is agnostic — any ButtonProps-conforming component works
function SubmitArea({ ButtonComponent = PrimaryButton }: { ButtonComponent?: React.FC<ButtonProps> }) {
  return <ButtonComponent title="Submit" onPress={handleSubmit} />;
  // SubmitArea works with ANY button variant — LSP guaranteed
}
I
Interface Segregation Principle (ISP)
Don't force components to depend on props they don't use
🧠 Analogy

A printer + scanner + fax machine vs three separate devices. If you only need to print, you shouldn't have to depend on (and break when) the scanner breaks. Separate interfaces for separate needs.

In React Native
Specific props: <UserAvatar name={user.name} url={user.avatarUrl} /> not <UserAvatar user={entireUser} />
Re-render performance: Passing only used fields = component only re-renders when THOSE fields change. Passing whole object = re-renders on ANY field change.
Split large prop interfaces: ProductCardProps should not include addToCart if some cards are display-only
React.memo + specific props: Works best when props are primitives/specific — memo comparison is more effective
📱 Performance Impact
  • user object passed → re-renders on ANY user field change (even unrelated ones)
  • name + avatarUrl passed → only re-renders when name or avatar actually changes
  • Combine with React.memo(UserCard) for maximum optimisation
  • Use useCallback for function props to stabilise references
D
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules — both should depend on abstractions
🧠 Analogy

Your laptop charger has a standard plug (abstraction). You can use it in any country with an adapter (implementation). The laptop doesn't care if it's UK or US power — it depends on the plug standard, not the wall socket.

In React Native
Service interfaces: Define interface StorageService — inject AsyncStorage or MMKV or MockStorage without changing consuming code
Analytics abstraction: interface Analytics { track(event, props): void } — swap Firebase for Amplitude for tests without code changes
Context injection: Provide implementations via Context — consumers call useStorage(), don't import AsyncStorage directly
Testing benefit: Jest tests inject MockStorageService — no real storage, no disk I/O, fast tests
Feature flags: A/B test implementations by injecting different service based on flag — zero component changes
dip.ts — Analytics DI + Testing (React Native)
// 1️⃣ Abstraction (interface) — the contract
interface AnalyticsService {
  track(event: string, props?: Record<string, any>): void;
  identify(userId: string, traits?: Record<string, any>): void;
  screen(name: string): void;
}

// 2️⃣ Concrete: Firebase (production)
class FirebaseAnalytics implements AnalyticsService {
  track(event, props) { analytics().logEvent(event, props); }
  identify(userId)    { analytics().setUserId(userId); }
  screen(name)        { analytics().logScreenView({ screen_name: name }); }
}

// 3️⃣ Concrete: Console logger (development)
class DevAnalytics implements AnalyticsService {
  track(event, props)  { console.log('📊 track', event, props); }
  identify(userId)     { console.log('👤 identify', userId); }
  screen(name)         { console.log('📱 screen', name); }
}

// 4️⃣ Concrete: Mock (testing)
class MockAnalytics implements AnalyticsService {
  calls: any[] = [];
  track(event, props)  { this.calls.push({ type: 'track', event, props }); }
  identify(userId)     { this.calls.push({ type: 'identify', userId }); }
  screen(name)         { this.calls.push({ type: 'screen', name }); }
}

// 5️⃣ Inject via context — components never import concrete class
const AnalyticsContext = createContext<AnalyticsService>(null!);
export const useAnalytics = () => useContext(AnalyticsContext);

// App.tsx — pick implementation at app boundary
<AnalyticsContext.Provider value={__DEV__ ? new DevAnalytics() : new FirebaseAnalytics()}>
  <App />
</AnalyticsContext.Provider>

// Any component — uses abstraction, zero coupling to Firebase
function ProductScreen() {
  const analytics = useAnalytics();
  useEffect(() => { analytics.screen('ProductScreen'); }, []);
  const handleBuy = () => { analytics.track('purchase_clicked', { productId }); };
}
LetterFull NameRN Violation SignRN FixTest Signal
SSingle Responsibility500+ line screen, multiple useEffects for different concernsCustom hooks + sub-components + service filesEasy to unit test each piece
OOpen/Closedif/else chain grows every time you add a featureVariant props + StyleSheet map + registry patternAdd feature without touching existing code
LLiskov SubstitutionVariant component ignores base props (disabled, onPress)TypeScript interface for all variants + enforce contractSwap variants in Storybook — all behave same
IInterface SegregationPassing full object when only 1–2 fields are usedDestructure and pass only needed primitivesComponent re-renders only on relevant changes
DDependency InversionDirect import of AsyncStorage, Firebase inside componentInterface + Context injection + swap at app boundaryTests use MockService — no real API/storage