A better way to create React Context providers

Oct 20, 2025

If you use React Context, you'll be familiar with the boilerplate involved. It typically looks something like this:

tsx
import React, { createContext, useContext, useState } from "react";

interface SearchContextValue {
  query: string;
  setQuery: (query: string) => void;
}

const SearchContext = createContext<SearchContextValue | undefined>(undefined);

export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [query, setQuery] = useState<SearchContextValue["query"]>("");

  const ctx: SearchContextValue = {
    query,
    setQuery,
  };

  return (
    <SearchContext.Provider value={ctx}>{children}</SearchContext.Provider>
  );
};

export function useSearch() {
  const context = useContext(SearchContext);
  if (!context) {
    throw new Error("useSearch must be used within a SearchProvider");
  }
  return context;
}

This can be abstracted away into a simple utility. I can't remember where I first came across it, but have found myself copying into virtually every React project I work on.

tsx
"use client";
import {
  createContext as _createContext,
  useContext as _useContext,
} from "react";

export interface CreateContextOptions {
  strict?: boolean;
  errorMessage?: string;
  name?: string;
}

type CreateContextReturn<T> = [React.Provider<T>, () => T, React.Context<T>];

export function createContext<ContextType>(options: CreateContextOptions = {}) {
  const {
    strict = true,
    errorMessage = "useContext: `context` is undefined. Seems you forgot to wrap component within the Provider",
    name,
  } = options;

  const Context = _createContext<ContextType | undefined>(undefined);
  Context.displayName = name;

  function useContext() {
    const context = _useContext(Context);
    if (!context && strict) {
      throw new Error(errorMessage);
    }
    return context;
  }

  return [
    Context.Provider,
    useContext,
    Context,
  ] as CreateContextReturn<ContextType>;
}

It can be used like this. As a bonus, if you forget to wrap your component tree properly, it will throw an error.

tsx
interface SearchContextValue {
  query: string;
  setQuery: (query: string) => void;
}

export const [SearchContext, useSearch] = createContext<
  SearchContextValue | undefined
>(undefined);

export const SearchProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [query, setQuery] = useState<SearchContextValue["query"]>("");

  const ctx: SearchContextValue = {
    query,
    setQuery,
  };

  return <SearchContext value={ctx}>{children}</SearchContext>;
};

More articles