Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to type context in React with TypeScript

Typing React Context in TypeScript is essential for maintaining a scalable codebase and avoiding runtime errors caused by accessing undefined state.
With over 25 years of experience in software development and as the creator of CoreUI, I’ve implemented complex state management systems in numerous enterprise-level React applications since 2014.
The most efficient and modern solution involves defining a clear interface for your context value and utilizing a custom hook to encapsulate the useContext logic with proper null checking.
This pattern ensures that your components are strictly typed and prevents the common “context is undefined” pitfalls found in larger projects.

Use a TypeScript interface as a generic type for createContext and create a custom hook to safely access the context value.

import { createContext, useCallback, useContext, useMemo, useState, ReactNode } from 'react'

/**
 * 1. Define the shape of your context state
 */
interface UIContextType {
  sidebarShow: boolean
  toggleSidebar: () => void
  setSidebarShow: (show: boolean) => void
}

/**
 * 2. Create the context with a generic type
 * We initialize with undefined to enforce Provider usage
 */
const UIContext = createContext<UIContextType | undefined>(undefined)

/**
 * 3. Create the Provider component
 */
export const UIProvider = ({ children }: { children: ReactNode }) => {
  const [sidebarShow, setSidebarShow] = useState(true)

  const toggleSidebar = useCallback(() => {
    setSidebarShow((prev) => !prev)
  }, [])

  const value = useMemo(() => ({
    sidebarShow,
    toggleSidebar,
    setSidebarShow
  }), [sidebarShow, toggleSidebar])

  return (
    <UIContext.Provider value={value}>
      {children}
    </UIContext.Provider>
  )
}

/**
 * 4. Create a custom hook for type-safe consumption
 */
export const useUI = () => {
  const context = useContext(UIContext)
  
  if (context === undefined) {
    throw new Error('useUI must be used within a UIProvider')
  }
  
  return context
}

This implementation starts by defining the UIContextType interface, which explicitly declares the properties and methods available in the context. By passing this interface as a generic to createContext<UIContextType | undefined>(undefined), we inform TypeScript that the context will eventually hold this specific structure but starts as undefined. The UIProvider component then manages the actual state and provides it to the component tree. Finally, the useUI hook is the “secret sauce”—it checks if the context is undefined. If it is, it throws a descriptive error; otherwise, it returns the typed context, allowing you to access sidebarShow or toggleSidebar without constant null checks in your functional components.

Best Practice Note:

This is the standard pattern we use in the CoreUI React Dashboard Template to handle global UI state. It provides a clean API for developers while ensuring that the Sidebar component and Header component remain perfectly synced. Always prefer a custom hook over direct useContext calls to maintain a single point of truth for your type assertions and error handling.

1. Defining the Context Interface

The first step in typing React Context is defining the interface that represents the data and functions you want to share. This interface acts as the contract for your context.

interface UIContextType {
  sidebarShow: boolean
  toggleSidebar: () => void
  setSidebarShow: (show: boolean) => void
}

By explicitly defining UIContextType, you ensure that any component consuming this context knows exactly what properties are available. This prevents bugs where a developer might try to access a property that doesn’t exist or pass the wrong type to a setter function.

2. Initializing Context with Generics

When calling createContext, TypeScript needs to know what type of data the context will hold. Since the provider might not be at the root of every test or component tree, it’s common to initialize it with undefined.

import { createContext } from 'react'

const UIContext = createContext<UIContextType | undefined>(undefined)

Using <UIContextType | undefined> tells the compiler that the value is either your interface or nothing. Initializing with undefined is better than using a fake “default” object (like {} as UIContextType) because it forces you to handle the case where a provider is missing, leading to more robust code.

3. Building the Provider Component

The Provider component is responsible for holding the state and making it available to children. In TypeScript, you should type the children prop using ReactNode.

import { useCallback, useMemo, useState, ReactNode } from 'react'

export const UIProvider = ({ children }: { children: ReactNode }) => {
  const [sidebarShow, setSidebarShow] = useState(true)

  const toggleSidebar = useCallback(() => {
    setSidebarShow((prev) => !prev)
  }, [])

  const value = useMemo(() => ({
    sidebarShow,
    toggleSidebar,
    setSidebarShow
  }), [sidebarShow, toggleSidebar])

  return (
    <UIContext.Provider value={value}>
      {children}
    </UIContext.Provider>
  )
}

Notice how the value object matches the UIContextType interface exactly. TypeScript will flag an error if you forget a property or provide a mismatched type, ensuring your provider implementation is always correct. Wrapping toggleSidebar in useCallback and the entire value in useMemo prevents unnecessary re-renders in consuming components.

4. Creating a Custom Consumer Hook

Consuming context directly with useContext(UIContext) is repetitive and requires a null check every single time. A custom hook solves this.

import { useContext } from 'react'

export const useUI = () => {
  const context = useContext(UIContext)
  
  if (!context) {
    throw new Error('useUI must be used within a UIProvider')
  }
  
  return context
}

This hook acts as a type guard. Once it passes the !context check, TypeScript knows that the returned value is exactly UIContextType. You no longer need to use the optional chaining operator ?. when accessing your context data.

5. Implementation in Components

With the typed hook in place, using the context in your components is straightforward and fully supported by IDE intellisense.

const SidebarToggle = () => {
  const { sidebarShow, toggleSidebar } = useUI()

  return (
    <button onClick={toggleSidebar}>
      Sidebar is {sidebarShow ? 'visible' : 'hidden'}
    </button>
  )
}

This clean syntax is exactly what we strive for in CoreUI. It keeps the component logic focused on the UI while the complex typing logic remains hidden inside the context definition. If you ever need to perform data transformations before providing them, you can use helpers like how to filter an array in javascript to prepare your state.

6. Performance Considerations with Context

While Context is powerful, it can trigger re-renders for all consumers whenever the value object changes. To optimize this in TypeScript, you should memoize both the callback functions with useCallback and the value object with useMemo.

const toggleSidebar = useCallback(() => {
  setSidebarShow((prev) => !prev)
}, [])

const value = useMemo(() => ({
  sidebarShow,
  toggleSidebar,
  setSidebarShow
}), [sidebarShow, toggleSidebar])

By wrapping functions in useCallback and the context value in useMemo, you ensure that components only re-render when the actual state (sidebarShow) changes, rather than on every render of the UIProvider. This is a critical pattern for performance-sensitive applications, such as those built with our React Dashboard Template.

7. Handling Multiple Contexts

In large applications, it is often better to split your state into multiple focused contexts (e.g., AuthContext, ThemeContext, UIContext) rather than one giant global object.

const App = () => {
  return (
    <AuthProvider>
      <UIProvider>
        <AppContent />
      </UIProvider>
    </AuthProvider>
  )
}

This modular approach makes your code easier to maintain and prevents unnecessary re-renders. Each context can follow the same typing pattern described above, ensuring a consistent developer experience across the entire project. For more advanced data manipulation within your context, you might need to know how to sort an array in javascript when managing lists in your global state.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author