+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 159 of 354

πŸ“˜ React Context with TypeScript: Global State

Master react context with typescript: global state in TypeScript with practical examples, best practices, and real-world applications πŸš€

πŸš€Intermediate
25 min read

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:

  1. Type Safety πŸ”’: Catch Context errors at compile-time
  2. Better IDE Support πŸ’»: Autocomplete for Context values
  3. Prop Drilling Solution πŸš«β›οΈ: Share data without passing props everywhere
  4. 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

  1. 🎯 Keep Context Focused: One context per concern (auth, theme, cart)
  2. πŸ“ Use TypeScript: Always type your context values
  3. πŸ›‘οΈ Error Boundaries: Handle context errors gracefully
  4. 🎨 Custom Hooks: Create useAuth, useTheme, etc.
  5. ⚑ Memoize Values: Prevent unnecessary re-renders
  6. πŸ” 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:

  1. πŸ’» Practice with the shopping cart example
  2. πŸ—οΈ Build a small app using multiple contexts
  3. πŸ“š Learn about state management libraries (Redux, Zustand)
  4. 🌟 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! πŸŽ‰πŸš€βœ¨