Passing props down through many levels of components — also known as prop drilling — can quickly make your React app harder to maintain and understand.

The good news? React's Context API can help — and with TypeScript, you can make it fully type-safe.

In this post, I’ll show you how to create a context from scratch using React + TypeScript, and how it helps keep your code clean.


✅ The Problem: Prop drilling everywhere

Imagine this scenario:

  • You have a deeply nested component.
  • You need to pass the same data (like user info or theme) through multiple layers.

Without context, you end up doing something like this:

function App() {
  const user = { name: 'Alice' };
  return <Page user={user} />;
}

function Page({ user }: { user: { name: string } }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }: { user: { name: string } }) {
  return <UserInfo user={user} />;
}

function UserInfo({ user }: { user: { name: string } }) {
  return <p>Hello, {user.name}!p>;
}

Every component in the chain has to receive and forward the user prop. This is where Context shines.


💡 The Solution: React Context API

We can create a UserContext and access the user data from any component — no drilling needed.

Step 1: Create the context and types

// UserContext.tsx
import { createContext, useContext } from 'react';

export type User = {
  name: string;
};

export const UserContext = createContext<User | null>(null);

export const useUser = () => {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
};

Step 2: Create a provider component

// UserProvider.tsx
import { User, UserContext } from './UserContext';

export function UserProvider({ children }: { children: React.ReactNode }) {
  const user: User = { name: 'Alice' }; // this could come from API or auth

  return (
    <UserContext.Provider value={user}>
      {children}
    UserContext.Provider>
  );
}

Step 3: Use it in your app

// App.tsx
import { UserProvider } from './UserProvider';
import { Page } from './Page';

export default function App() {
  return (
    <UserProvider>
      <Page />
    UserProvider>
  );
}

And now, in any nested component:

// UserInfo.tsx
import { useUser } from './UserContext';

export function UserInfo() {
  const user = useUser();
  return <p>Hello, {user.name}!p>;
}

No need to pass the user prop through multiple layers — just consume it directly from context.


⚙️ Why this approach works well

  • Centralized state without global libraries
  • Clean and decoupled components
  • Fully type-safe with TypeScript
  • Reusable across multiple components

🧠 Tips for using Context effectively

  • Split context into small focused providers (e.g., UserContext, ThemeContext, CartContext)
  • Wrap only the parts of the tree that need that data
  • Combine with custom hooks for better DX (useUser, useCart, etc.)
  • Avoid putting deeply dynamic data (like rapidly changing values) in context

🚀 Final Thoughts

Using the Context API with TypeScript is a powerful way to avoid prop drilling and keep your React code organized.

This pattern is simple, scalable, and widely used in production apps.

👉 Do you use context in your React projects? What’s your go-to pattern for avoiding prop drilling? Let’s discuss! 👇


🔗 Follow me for more React + TypeScript tips!