Prerequisites
- Basic understanding of JavaScript π
- TypeScript installation β‘
- VS Code or preferred IDE π»
What you'll learn
- Understand React Context fundamentals π―
- Apply React Context in real projects ποΈ
- Debug common Context issues π
- Write type-safe Context code β¨
π― Introduction
Welcome to this exciting tutorial on React Context with TypeScript! π In this guide, weβll explore how to manage global state in your React applications using the powerful combination of React Context API and TypeScriptβs type safety.
Youβll discover how React Context can transform your state management experience, making your components cleaner and your data flow more predictable. Whether youβre building shopping apps π, user dashboards π, or theme systems π¨, understanding Context with TypeScript is essential for scalable React applications.
By the end of this tutorial, youβll feel confident implementing type-safe global state management in your own projects! Letβs dive in! πββοΈ
π Understanding React Context
π€ What is React Context?
React Context is like a magical delivery service π¦ for your React app. Think of it as a way to pass data through your component tree without having to pass props down manually at every level - no more prop drilling! π«βοΈ
In TypeScript terms, Context provides a way to share values between components without explicitly passing them through props. This means you can:
- β¨ Avoid prop drilling hell
- π Manage global state efficiently
- π‘οΈ Maintain type safety across your app
- π± Create cleaner component hierarchies
π‘ Why Use Context with TypeScript?
Hereβs why developers love Context + TypeScript:
- Type Safety π: Catch Context errors at compile-time
- Better IDE Support π»: Autocomplete for Context values
- Prop Drilling Solution π«βοΈ: Share data without passing props everywhere
- Scalable State Management π: Perfect for medium-sized applications
Real-world example: Imagine building a shopping app π. With Context, you can share the userβs cart, theme preferences, and authentication status across all components without prop drilling!
π§ Basic Syntax and Usage
π Simple Context Example
Letβs start with a friendly theme context:
// π Hello, React Context with TypeScript!
import React, { createContext, useContext, useState, ReactNode } from 'react';
// π¨ Define our theme type
type Theme = 'light' | 'dark';
// π Create the context type
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// π― Create the context
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
π‘ Explanation: We define a Theme
type and create a context that can hold our theme state and toggle function. The undefined
initial value ensures we handle cases where the context isnβt provided.
π― Context Provider Pattern
Hereβs how to create a provider component:
// ποΈ Theme Provider component
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('light');
// π Toggle function
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// π¨ Custom hook for using theme context
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
π‘ Practical Examples
π Example 1: Shopping Cart Context
Letβs build a type-safe shopping cart:
// ποΈ Define our product and cart types
interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
interface CartItem extends Product {
quantity: number;
}
interface CartContextType {
items: CartItem[];
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
getTotalPrice: () => number;
getItemCount: () => number;
}
// π― Create cart context
const CartContext = createContext<CartContextType | undefined>(undefined);
// π Cart Provider component
export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [items, setItems] = useState<CartItem[]>([]);
// β Add item to cart
const addItem = (product: Product) => {
setItems(prev => {
const existingItem = prev.find(item => item.id === product.id);
if (existingItem) {
// π Increase quantity if item exists
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
// β¨ Add new item
return [...prev, { ...product, quantity: 1 }];
});
console.log(`Added ${product.emoji} ${product.name} to cart! π`);
};
// β Remove item from cart
const removeItem = (productId: string) => {
setItems(prev => prev.filter(item => item.id !== productId));
};
// π Update quantity
const updateQuantity = (productId: string, quantity: number) => {
if (quantity <= 0) {
removeItem(productId);
return;
}
setItems(prev =>
prev.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
};
// π° Calculate total price
const getTotalPrice = () => {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
};
// π’ Get total item count
const getItemCount = () => {
return items.reduce((count, item) => count + item.quantity, 0);
};
return (
<CartContext.Provider value={{
items,
addItem,
removeItem,
updateQuantity,
getTotalPrice,
getItemCount
}}>
{children}
</CartContext.Provider>
);
};
// π¨ Custom hook for cart
export const useCart = (): CartContextType => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
π― Using the Cart Context:
// πͺ Product component
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
const { addItem } = useCart();
return (
<div className="product-card">
<h3>{product.emoji} {product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>
Add to Cart π
</button>
</div>
);
};
// π Cart summary component
const CartSummary: React.FC = () => {
const { items, getTotalPrice, getItemCount } = useCart();
return (
<div className="cart-summary">
<h2>π Cart Summary</h2>
<p>Items: {getItemCount()}</p>
<p>Total: ${getTotalPrice().toFixed(2)}</p>
</div>
);
};
π€ Example 2: User Authentication Context
Letβs create a user auth system:
// π€ User types
interface User {
id: string;
name: string;
email: string;
avatar: string;
role: 'user' | 'admin';
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
isAuthenticated: boolean;
}
// π Auth context
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// π‘οΈ Auth Provider
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
// π Login function
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
// π Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// π Mock successful login
const mockUser: User = {
id: '1',
name: 'Sarah Developer',
email,
avatar: 'π©βπ»',
role: 'user'
};
setUser(mockUser);
console.log(`Welcome ${mockUser.avatar} ${mockUser.name}! π`);
} catch (error) {
console.error('Login failed! π');
} finally {
setIsLoading(false);
}
};
// πͺ Logout function
const logout = () => {
setUser(null);
console.log('Logged out successfully! π');
};
// β
Check if authenticated
const isAuthenticated = user !== null;
return (
<AuthContext.Provider value={{
user,
login,
logout,
isLoading,
isAuthenticated
}}>
{children}
</AuthContext.Provider>
);
};
// π¨ Custom hook for auth
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
π Advanced Concepts
π§ββοΈ Advanced Topic 1: Context with Reducers
For complex state management, combine Context with useReducer:
// π― Todo state and actions
interface Todo {
id: string;
text: string;
completed: boolean;
emoji: string;
}
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string; emoji: string } }
| { type: 'TOGGLE_TODO'; payload: { id: string } }
| { type: 'DELETE_TODO'; payload: { id: string } }
| { type: 'CLEAR_COMPLETED' };
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
}
// π Reducer function
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now().toString(),
text: action.payload.text,
completed: false,
emoji: action.payload.emoji
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
default:
return state;
}
};
// π¨ Context with reducer
interface TodoContextType {
state: TodoState;
dispatch: React.Dispatch<TodoAction>;
addTodo: (text: string, emoji: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
clearCompleted: () => void;
}
const TodoContext = createContext<TodoContextType | undefined>(undefined);
export const TodoProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
});
// π Action creators
const addTodo = (text: string, emoji: string) => {
dispatch({ type: 'ADD_TODO', payload: { text, emoji } });
};
const toggleTodo = (id: string) => {
dispatch({ type: 'TOGGLE_TODO', payload: { id } });
};
const deleteTodo = (id: string) => {
dispatch({ type: 'DELETE_TODO', payload: { id } });
};
const clearCompleted = () => {
dispatch({ type: 'CLEAR_COMPLETED' });
};
return (
<TodoContext.Provider value={{
state,
dispatch,
addTodo,
toggleTodo,
deleteTodo,
clearCompleted
}}>
{children}
</TodoContext.Provider>
);
};
ποΈ Advanced Topic 2: Multiple Context Composition
For complex apps, compose multiple contexts:
// π¨ App Provider that combines all contexts
export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
return (
<ThemeProvider>
<AuthProvider>
<CartProvider>
<TodoProvider>
{children}
</TodoProvider>
</CartProvider>
</AuthProvider>
</ThemeProvider>
);
};
// π Root component
const App: React.FC = () => {
return (
<AppProvider>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cart" element={<Cart />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Router>
</AppProvider>
);
};
β οΈ Common Pitfalls and Solutions
π± Pitfall 1: Context Provider Hell
// β Wrong way - too many nested providers!
const App = () => (
<ThemeProvider>
<AuthProvider>
<CartProvider>
<TodoProvider>
<UserProvider>
<SettingsProvider>
<Component />
</SettingsProvider>
</UserProvider>
</TodoProvider>
</CartProvider>
</AuthProvider>
</ThemeProvider>
);
// β
Correct way - compose providers!
const AllProviders: React.FC<{ children: ReactNode }> = ({ children }) => (
<ThemeProvider>
<AuthProvider>
<CartProvider>
<TodoProvider>
<UserProvider>
<SettingsProvider>
{children}
</SettingsProvider>
</UserProvider>
</TodoProvider>
</CartProvider>
</AuthProvider>
</ThemeProvider>
);
const App = () => (
<AllProviders>
<Component />
</AllProviders>
);
π€― Pitfall 2: Using Context Without Provider
// β Dangerous - no error handling!
const useTheme = () => {
return useContext(ThemeContext); // π₯ Might be undefined!
};
// β
Safe - proper error handling!
const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
π Pitfall 3: Context Re-renders Everything
// β Wrong - value object recreated every render!
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
// β
Correct - memoize the value!
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
π οΈ Best Practices
- π― Keep Context Focused: One context per concern (auth, theme, cart)
- π Use TypeScript: Always type your context values
- π‘οΈ Error Boundaries: Handle context errors gracefully
- π¨ Custom Hooks: Create useAuth, useTheme, etc.
- β‘ Memoize Values: Prevent unnecessary re-renders
- π Split Contexts: Donβt put everything in one massive context
π§ͺ Hands-On Exercise
π― Challenge: Build a Complete App State System
Create a mini social media app with global state management:
π Requirements:
- π€ User authentication with login/logout
- π± Posts with like/comment functionality
- π¨ Theme switching (light/dark)
- π Notification system
- πΎ Local storage persistence
π Bonus Points:
- Add loading states
- Implement optimistic updates
- Create a settings context
- Add error handling
π‘ Solution
π Click to see solution
// π― Complete app state system!
// π± Post types
interface Post {
id: string;
userId: string;
content: string;
likes: number;
comments: Comment[];
timestamp: Date;
emoji: string;
}
interface Comment {
id: string;
userId: string;
content: string;
timestamp: Date;
}
// π Notification types
interface Notification {
id: string;
type: 'like' | 'comment' | 'follow';
message: string;
timestamp: Date;
read: boolean;
emoji: string;
}
// π¨ Main app context
interface AppContextType {
// User state
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
// Posts state
posts: Post[];
addPost: (content: string, emoji: string) => void;
likePost: (postId: string) => void;
addComment: (postId: string, content: string) => void;
// Theme state
theme: 'light' | 'dark';
toggleTheme: () => void;
// Notifications
notifications: Notification[];
addNotification: (notification: Omit<Notification, 'id' | 'timestamp'>) => void;
markAsRead: (notificationId: string) => void;
// Loading states
isLoading: boolean;
}
// π Complete app provider
export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isLoading, setIsLoading] = useState(false);
// π Login function
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const mockUser: User = {
id: '1',
name: 'Social Media User',
email,
avatar: 'π€',
role: 'user'
};
setUser(mockUser);
addNotification({
type: 'follow',
message: `Welcome back! π`,
read: false,
emoji: 'π'
});
} finally {
setIsLoading(false);
}
};
// πͺ Logout
const logout = () => {
setUser(null);
setPosts([]);
setNotifications([]);
};
// π Add post
const addPost = (content: string, emoji: string) => {
if (!user) return;
const newPost: Post = {
id: Date.now().toString(),
userId: user.id,
content,
likes: 0,
comments: [],
timestamp: new Date(),
emoji
};
setPosts(prev => [newPost, ...prev]);
};
// π Like post
const likePost = (postId: string) => {
setPosts(prev =>
prev.map(post =>
post.id === postId
? { ...post, likes: post.likes + 1 }
: post
)
);
addNotification({
type: 'like',
message: 'Someone liked your post! π',
read: false,
emoji: 'β€οΈ'
});
};
// π¬ Add comment
const addComment = (postId: string, content: string) => {
if (!user) return;
const newComment: Comment = {
id: Date.now().toString(),
userId: user.id,
content,
timestamp: new Date()
};
setPosts(prev =>
prev.map(post =>
post.id === postId
? { ...post, comments: [...post.comments, newComment] }
: post
)
);
};
// π¨ Toggle theme
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// π Add notification
const addNotification = (notification: Omit<Notification, 'id' | 'timestamp'>) => {
const newNotification: Notification = {
...notification,
id: Date.now().toString(),
timestamp: new Date()
};
setNotifications(prev => [newNotification, ...prev]);
};
// β
Mark notification as read
const markAsRead = (notificationId: string) => {
setNotifications(prev =>
prev.map(notification =>
notification.id === notificationId
? { ...notification, read: true }
: notification
)
);
};
// π― Memoize context value
const value = useMemo(() => ({
user,
login,
logout,
posts,
addPost,
likePost,
addComment,
theme,
toggleTheme,
notifications,
addNotification,
markAsRead,
isLoading
}), [user, posts, theme, notifications, isLoading]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
π Key Takeaways
Youβve learned so much! Hereβs what you can now do:
- β Create type-safe Context with confidence πͺ
- β Avoid prop drilling with global state π‘οΈ
- β Combine multiple contexts for complex apps π―
- β Handle Context errors gracefully π
- β Build scalable React apps with TypeScript! π
Remember: Context is powerful but use it wisely - not everything needs to be global state! π€
π€ Next Steps
Congratulations! π Youβve mastered React Context with TypeScript!
Hereβs what to do next:
- π» Practice with the shopping cart example
- ποΈ Build a small app using multiple contexts
- π Learn about state management libraries (Redux, Zustand)
- π Explore React 18βs new features with Context
Remember: Every React expert started where you are now. Keep building, keep learning, and most importantly, have fun with Context! π
Happy coding! ππβ¨