Recently, while working on a React project with TypeScript, I needed to share state between multiple components. Managing shared data like user info, theme preferences, or a shopping cart across deeply nested components was becoming difficult with unorganized code.
While searching for an alternate I came to know about React’s Context API specifically the useContext hook.
In this article, I’ll explain how to use React’s useContext with TypeScript to create clean, type-safe shared state in your React applications.
What is useContext in React?
The useContext hook is part of React’s Context API that allows you to share values between components without explicitly passing props through every level of the component tree.
When working with TypeScript, we gain the additional benefit of type safety, but it requires proper configuration.
Check out: Convert TypeScript Objects to JSON
Setting Up Context with TypeScript
First, create a React + TypeScript Project, and for that, you can use Vite (fastest option):
npm create vite@latest usecontext-demo -- --template react-ts
cd usecontext-demo
npm install
npm run dev
To use Vite, we need the Node.js version above 20.0.
Method 1: Basic Context Creation
In the small React + TypeScript app using useContext to share user information across components.
Let’s start by creating a simple context for a user authentication system:
//UserContext.tsx
import { createContext } from "react";
type User = {
name: string;
age: number;
};
export const UserContext = createContext<User | null>(null);
Now we can use this context in our components:
- UserProvider.tsx
//UserProvider.tsx
import React, { useState } from "react";
import { UserContext } from "./UserContext";
type Props = {
children: React.ReactNode;
};
const UserProvider: React.FC<Props> = ({ children }) => {
const [user] = useState({ name: "Henry", age: 28 });
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
};
export default UserProvider;
- UserProfile.tsx
//UserProfile.tsx
import React, { useContext } from "react";
import { UserContext } from "./UserContext";
const UserProfile: React.FC = () => {
const user = useContext(UserContext);
if (!user) return <p>Loading...</p>;
return (
<div style={{ border: "1px solid #ccc", padding: "1rem", width: "250px" }}>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
</div>
);
};
export default UserProfile;
- App.tsx
//App.tsx
import React from "react";
import UserProvider from "./UserProvider";
import UserProfile from "./UserProfile";
const App: React.FC = () => {
return (
<UserProvider>
<h1>User Dashboard</h1>
<UserProfile />
</UserProvider>
);
};
export default App;
In the terminal, enter npm run dev, then follow the link http://localhost:/

In this, we can see the value that we have provided from UserProvider.tsx to UserProfile.tsx.
Check out: Ternary Operator in TypeScript
Method 2: Using Context with Initial Values
Sometimes you’ll want to provide initial values to your context. Here’s how to do it safely with TypeScript:
Create context files.
// src/ShoppingCartContext.tsx
import React, { createContext, useContext, useReducer } from 'react';
interface Product {
id: string;
name: string;
price: number;
}
interface CartItem {
product: Product;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Product }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR_CART' };
interface CartContextType {
state: CartState;
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
clearCart: () => void;
}
const initialState: CartState = {
items: [],
total: 0
};
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM': {
const existingItemIndex = state.items.findIndex(
item => item.product.id === action.payload.id
);
let newItems: CartItem[];
if (existingItemIndex >= 0) {
newItems = [...state.items];
newItems[existingItemIndex] = {
...newItems[existingItemIndex],
quantity: newItems[existingItemIndex].quantity + 1
};
} else {
newItems = [...state.items, { product: action.payload, quantity: 1 }];
}
return {
items: newItems,
total: newItems.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
)
};
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.product.id !== action.payload);
return {
items: newItems,
total: newItems.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
)
};
}
case 'CLEAR_CART':
return initialState;
default:
return state;
}
};
const CartContext = createContext<CartContextType>({
state: initialState,
addItem: () => {},
removeItem: () => {},
clearCart: () => {}
});
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (product: Product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (productId: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: productId });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
return (
<CartContext.Provider value={{ state, addItem, removeItem, clearCart }}>
{children}
</CartContext.Provider>
);
};
export const useCart = () => useContext(CartContext);
Create ProductList.tsx in src/
// src/ProductList.tsx
import React from 'react';
import { useCart } from './ShoppingCartContext';
const products = [
{ id: '1', name: 'Apple iPhone 13', price: 799 },
{ id: '2', name: 'Samsung Galaxy S22', price: 749 },
{ id: '3', name: 'Google Pixel 6', price: 599 }
];
const ProductList: React.FC = () => {
const { addItem } = useCart();
return (
<div>
<h2>Available Products</h2>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
<button onClick={() => addItem(product)}>Add to Cart</button>
</li>
))}
</ul>
</div>
);
};
export default ProductList;
3. Create a Cart.tsx file to show cart data.
// src/Cart.tsx
import React from 'react';
import { useCart } from './ShoppingCartContext';
const Cart: React.FC = () => {
const { state, removeItem, clearCart } = useCart();
return (
<div>
<h2>Cart</h2>
{state.items.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<>
<ul>
{state.items.map(({ product, quantity }) => (
<li key={product.id}>
{product.name} (x{quantity}) - ${product.price * quantity}
<button onClick={() => removeItem(product.id)}>Remove</button>
</li>
))}
</ul>
<p>Total: ${state.total}</p>
<button onClick={clearCart}>Clear Cart</button>
</>
)}
</div>
);
};
export default Cart;
Check out: TypeScript keyof Operator
Wrap the App with CartProvider edit src/App.tsx:
// src/App.tsx
import React from 'react';
import { CartProvider } from './ShoppingCartContext';
import ProductList from './ProductList';
import Cart from './Cart';
const App: React.FC = () => {
return (
<CartProvider>
<h1>Shopping Cart Demo</h1>
<ProductList />
<Cart />
</CartProvider>
);
};
export default App;
Now, run the app with npm run dev.
Output:

Method 3: Using Multiple Contexts Together
In complex applications, you might need to use multiple contexts together. Here’s how to organize this with TypeScript:
In the multiple contexts we will create contexts for user login, shopping cart and to change theme. In the src, create a folder contexts and inside that folder, create the contexts below.
- contexts/UserContext.tsx
import React, { createContext, useContext, useState } from 'react';
interface User {
id: string;
name: string;
}
interface UserContextType {
user: User | null;
setUser: (user: User) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be used within a UserProvider');
return context;
};
- contexts/ShoppingCartContext.tsx
import React, { createContext, useContext, useState } from 'react';
interface CartItem {
id: string;
name: string;
quantity: number;
}
interface CartContextType {
cart: CartItem[];
addItem: (item: CartItem) => void;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [cart, setCart] = useState<CartItem[]>([]);
const addItem = (item: CartItem) => {
setCart((prev) => [...prev, item]);
};
return (
<CartContext.Provider value={{ cart, addItem }}>
{children}
</CartContext.Provider>
);
};
export const useCart = () => {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within a CartProvider');
return context;
};
- contexts/ThemeContext.tsx
import React, { createContext, useContext, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within a ThemeProvider');
return context;
};
Check out: Call REST APIs Using TypeScript
Now, create a context wrapper AppContexts.tsx inside the same contexts folder.
import React from 'react';
import { UserProvider } from './UserContext';
import { CartProvider } from './ShoppingCartContext';
import { ThemeProvider } from './ThemeContext';
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<ThemeProvider>
<UserProvider>
<CartProvider>
{children}
</CartProvider>
</UserProvider>
</ThemeProvider>
);
};
Inside the src, create a main component, MainLayout.tsx.
import React from 'react';
import { useUser } from './contexts/UserContext';
import { useCart } from './contexts/ShoppingCartContext';
import { useTheme } from './contexts/ThemeContext';
const MainLayout: React.FC = () => {
const { user, setUser } = useUser();
const { cart, addItem } = useCart();
const { theme, toggleTheme } = useTheme();
return (
<div style={{ padding: '2rem', background: theme === 'dark' ? '#222' : '#fff', color: theme === 'dark' ? '#fff' : '#000' }}>
<h1>Multi-Context App</h1>
<section>
<h2>User</h2>
{user ? (
<>
<p>Hello, {user.name}</p>
<button onClick={() => setUser(null)}>Logout</button>
</>
) : (
<button onClick={() => setUser({ id: '1', name: 'Henry' })}>Login</button>
)}
</section>
<section>
<h2>Cart</h2>
<button onClick={() => addItem({ id: 'p1', name: 'Product 1', quantity: 1 })}>Add Item</button>
<ul>
{cart.map((item, idx) => (
<li key={idx}>{item.name} x {item.quantity}</li>
))}
</ul>
</section>
<section>
<h2>Theme</h2>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</section>
</div>
);
};
export default MainLayout;
Then, in your main App component:
import React from 'react';
import { AppProviders } from './contexts/AppContexts';
import MainLayout from './MainLayout';
const App: React.FC = () => {
return (
<AppProviders>
<MainLayout />
</AppProviders>
);
};
export default App;
Output:

Check out: TypeScript Global Variables
Best Practices for Using useContext with TypeScript
1. Always Define Types for Your Context
Always define an explicit interface for your context value. This ensures type safety across your application.
// Bad: No explicit typing
const MyContext = createContext({});
// Good: Properly typed context
interface MyContextType {
value: string;
setValue: (value: string) => void;
}
const MyContext = createContext<MyContextType | undefined>(undefined);
2. Create Custom Hooks for Context Consumption
Create a custom hook for each context to encapsulate the error handling logic:
export const useMyContext = () => {
const context = useContext(MyContext);
if (context === undefined) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
3. Provide Default Values When Appropriate
For non-optional contexts, consider providing sensible defaults:
const ThemeContext = createContext<ThemeContextType>({
theme: 'light',
toggleTheme: () => {},
});
4. Split Complex Contexts
If your context is growing too large, consider splitting it into smaller, more focused contexts:
// Instead of one large context:
interface HugeContext {
user: User;
cart: Cart;
theme: Theme;
// many more properties...
}
// Split into focused contexts:
interface UserContext { user: User; }
interface CartContext { cart: Cart; }
interface ThemeContext { theme: Theme; }
Check out: Check If Object Is Undefined in TypeScript
Handling Context Updates Efficiently
To optimize performance when using context, consider using the useMemo hook to memoize your context value:
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = useCallback(() => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
}, []);
// Memoize the context value to prevent unnecessary re-renders
const value = useMemo(() => ({
theme,
toggleTheme
}), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
Real-World Example: Theme Switcher
Let’s create a complete theme switcher example that’s commonly used in American websites:
// ThemeContext.ts