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!