Skip to main content

🧱 Part 10 — Context API & Dependency Injection (Lightweight Global State) 🪢


Sometimes you don’t need Redux—just a small, type‑safe way to share a value or service across components without prop‑drilling. That’s exactly what React Context is for. Below is a clean, error‑free, copy‑paste‑ready setup with thorough explanations and commented code.


🧩 Core Ideas

  • Context: A container that can provide a value deep in the tree.
  • Provider: A component that owns the value and makes it available to its descendants.
  • useContext: A hook to read that value.
  • Dependency Injection (DI): Pass a service (e.g., API client) via Context so components can swap implementations (e.g., mock vs real).

1) Theme Context (with safe typing + memo + persistence)

A minimal, production‑ready pattern:

  • Throws a helpful error if used outside its provider.
  • Memoizes the context value to avoid unnecessary re‑renders.
  • Persists the theme to localStorage and respects system preference on first load.
// src/contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';

// 1) Value shape for this context
export type Theme = 'light' | 'dark';
export type ThemeContextValue = {
  theme: Theme;                     // current theme
  setTheme: (t: Theme) => void;     // explicit setter
  toggle: () => void;               // convenience action
};

// 2) Context (default is undefined so we can catch misuse at runtime)
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

function getInitialTheme(): Theme {
  // SSR/Non‑DOM guard
  if (typeof window === 'undefined') return 'light';
  // localStorage wins if set
  const stored = window.localStorage.getItem('theme');
  if (stored === 'light' || stored === 'dark') return stored;
  // otherwise, respect system preference
  return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light';
}

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>(getInitialTheme);

  // Keep <html data-theme> in sync (useful for CSS variables/Tailwind themes)
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
    window.localStorage.setItem('theme', theme);
  }, [theme]);

  const value = useMemo<ThemeContextValue>(() => ({
    theme,
    setTheme,
    toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
  }), [theme]);

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

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider>');
  return ctx;
}

Why this is robust:

  • createContext<T | undefined>(undefined) + a custom hook yields a clear error when the provider is missing.
  • useMemo ensures consumers don’t re‑render unless theme changes.
  • The getInitialTheme function avoids hydration issues and respects system settings.

Usage:

// src/components/ThemeSwitcher.tsx
import { useTheme } from '@/contexts/ThemeContext';

export function ThemeSwitcher() {
  const { theme, toggle } = useTheme();
  return (
    <button className="border px-3 py-1 rounded" onClick={toggle}>
      Theme: {theme} (toggle)
    </button>
  );
}

Provider wiring:

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider } from '@/contexts/ThemeContext';
import AppRouter from '@/app/AppRouter';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ThemeProvider>
      <AppRouter />
    </ThemeProvider>
  </React.StrictMode>,
);

2) Dependency Injection via Context (API client example)

Provide a service object so components don’t import concrete implementations directly.

// src/contexts/ApiContext.tsx
import { createContext, useContext, type ReactNode } from 'react';
import { http } from '@/lib/api/http'; // your Axios instance from earlier parts

// Service contract (simple for now; expand if you add more services)
export type ApiContextValue = { http: typeof http };

const ApiContext = createContext<ApiContextValue | undefined>(undefined);

export function ApiProvider({ children }: { children: ReactNode }) {
  // You can swap this for a mock in tests
  const value: ApiContextValue = { http };
  return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
}

export function useApi() {
  const ctx = useContext(ApiContext);
  if (!ctx) throw new Error('useApi must be used inside <ApiProvider>');
  return ctx;
}

Consume the injected service:

// src/components/ServerPing.tsx
import { useEffect, useState } from 'react';
import { useApi } from '@/contexts/ApiContext';

export function ServerPing() {
  const { http } = useApi(); // DI: get the service from context
  const [status, setStatus] = useState('checking…');

  useEffect(() => {
    const ctrl = new AbortController();
    http.get('/ping', { signal: ctrl.signal })
      .then(() => setStatus('✅ online'))
      .catch(() => setStatus('❌ offline'));
    return () => ctrl.abort();
  }, [http]);

  return <p>Server status: {status}</p>;
}

Provider composition (example):

// src/app/AppProviders.tsx
import type { ReactNode } from 'react';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { ApiProvider } from '@/contexts/ApiContext';

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider>
      <ApiProvider>
        {children}
      </ApiProvider>
    </ThemeProvider>
  );
}
}

3) Avoiding common Context pitfalls

  • Using useContext without Provider → wrap with a custom hook that throws.
  • Unnecessary re‑renders → memoize the value object (useMemo) and avoid inline object literals as values.
  • Over‑contexting → split by domain (Theme, Api, I18n) instead of one huge context.
  • Mutating non‑stable services → keep service instances stable; replace via Provider if you need a different impl.

4) Optional: Split State/Dispatch Contexts (advanced)

Reducing re‑renders further by separating readers (state) from writers (dispatch/setters).

// src/contexts/ThemeSplitContext.tsx
import { createContext, useContext, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
import type { Theme } from './ThemeContext';

const ThemeStateCtx = createContext<Theme | undefined>(undefined);
const ThemeSetCtx = createContext<Dispatch<SetStateAction<Theme>> | undefined>(undefined);

export function ThemeSplitProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');
  return (
    <ThemeStateCtx.Provider value={theme}>
      <ThemeSetCtx.Provider value={setTheme}>{children}</ThemeSetCtx.Provider>
    </ThemeStateCtx.Provider>
  );
}

export function useThemeState() {
  const v = useContext(ThemeStateCtx);
  if (v === undefined) throw new Error('useThemeState must be used within ThemeSplitProvider');
  return v;
}

export function useThemeSet() {
  const v = useContext(ThemeSetCtx);
  if (v === undefined) throw new Error('useThemeSet must be used within ThemeSplitProvider');
  return v;
}

Consumers that only read the theme subscribe to ThemeStateCtx and won’t re‑render when only the setter changes.


✅ Wrap‑Up

You now have a fault‑tolerant Context pattern that:

  • Guards against missing providers
  • Memoizes values to minimize renders
  • Persists theme and respects system preference
  • Injects services cleanly for testing and environment swaps

Use Context for small, cross‑cutting values and DI; keep server data in your data layer and complex UI state in a dedicated state manager.

Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...