DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Fixing “Property `Provider` does not exist on type `() => Context<…>`” in React 19 + TypeScript

Fixing “Property  raw `Provider` endraw  does not exist on type  raw `() => Context<…>` endraw ” in React 19 + TypeScript

Fixing “Property Provider does not exist on type () => Context<…>” in React 19 + TypeScript

Why a single arrow function can break your entire Context layer—and how to avoid it.


TL;DR

If you see this compile error:

TS2339: Property 'Provider' does not exist on type '() => Context<{}>'.
Enter fullscreen mode Exit fullscreen mode

you accidentally wrapped createContext in a function:

export const TodoContext = () => createContext({});
Enter fullscreen mode Exit fullscreen mode

Remove the arrow so you export the Context instance, not a function:

export const TodoContext = createContext({});
Enter fullscreen mode Exit fullscreen mode

That’s it! Keep reading for the deeper why, plus a type‑safe pattern you can copy‑paste.


1  React Context in 60 seconds

  • createContext<T>(defaultValue) returns a Context object with two keys:

    • <Context.Provider value={…}>
    • Context.displayName (plus legacy .Consumer)
  • Consumers call useContext(Context) to read the nearest Provider’s value.

Because the Provider is a property on the object, you must export that exact object.


2  The Arrow‑Function Trap

Buggy code

// 🚫 Don’t do this
export const TodoContext = () => createContext({});
Enter fullscreen mode Exit fullscreen mode

What happens?

  • TodoContext’s type becomes () => Context<{}>—just a factory function.
  • At call‑sites:
  <TodoContext.Provider />   // ❌ TS2339: 'Provider' does not exist
Enter fullscreen mode Exit fullscreen mode

TypeScript is right: the function has no .Provider.

Correct code

// ✅ Do this
export const TodoContext = createContext({});
Enter fullscreen mode Exit fullscreen mode

Now TodoContext is the Context instance, so .Provider and .displayName work.


3  Digging Deeper: Why You Might Have Wrapped It

  • Copy‑pasta from custom hook patterns (const useSomething = () => …).
  • Thinking you need lazy initialization—React does that internally.
  • Habit from other DI frameworks (Angular providers, NestJS, etc.).

Remember: createContext is cheap and pure; call it once at module top‑level.


4  A Fully‑Typed Example (Todo App)

// context/TodoContext.tsx
import { createContext } from 'react';

export interface Todo {
  id: string;
  text: string;
  done: boolean;
}

export interface TodoState {
  todos: Todo[];
}

export type TodoAction =
  | { type: 'add'; text: string }
  | { type: 'toggle'; id: string }
  | { type: 'remove'; id: string };

export interface TodoContextValue extends TodoState {
  dispatch: React.Dispatch<TodoAction>;
}

/**
 * ⚠️ We seed the context with a dummy default so that
 * consumers running outside a provider get a clear runtime error
 * instead of 'undefined is not an object'.
 */
export const TodoContext = createContext<TodoContextValue>({
  todos: [],
  /* eslint-disable @typescript-eslint/no-empty-function */
  dispatch: () => {},
});

Enter fullscreen mode Exit fullscreen mode

Provider

'use client';
import { FC, PropsWithChildren, useReducer } from 'react';
import { nanoid } from 'nanoid';
import { TodoContext, TodoAction, TodoState, Todo } from './TodoContext';

/** Pure reducer keeps state transitions deterministic and easy to test. */
function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'add':
      return {
        todos: [
          ...state.todos,
          { id: nanoid(6), text: action.text, done: false } as Todo,
        ],
      };
    case 'toggle':
      return {
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t,
        ),
      };
    case 'remove':
      return {
        todos: state.todos.filter(t => t.id !== action.id),
      };
    default:
      return state;
  }
}

export const TodoProvider: FC<PropsWithChildren> = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, { todos: [] });

  /** React 19 automatically batches state updates inside dispatch. */
  return (
    <TodoContext.Provider value={{ ...state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
};

Enter fullscreen mode Exit fullscreen mode

5  Checklist to Avoid This Error

Check Reason
Export the Context object directly Avoid wrapping in () => …
Place createContext at module top‑level Ensures a single instance
Give createContext a generic and a default Eliminates undefined checks
Use a custom hook (useTodo) Prevents forgetting the Provider

6  Bonus: Enforcing Provider Presence

import { useContext } from 'react';
import { TodoContext } from './TodoContext';

/**
 * Custom hook that exposes the context and ensures the caller
 * is wrapped in <TodoProvider>. Avoids repetitive useContext imports.
 */
export function useTodo() {
  const ctx = useContext(TodoContext);
  if (!ctx) {
    throw new Error('useTodo must be used inside <TodoProvider>');
  }
  return ctx;
}

Enter fullscreen mode Exit fullscreen mode

Now you get a clear runtime error in tests/dev if you forget the Provider.


7  Conclusion

Most “Context bugs” stem from exporting the wrong thing. Stick to:

export const MyContext = createContext<MyValue>(defaultValue);
Enter fullscreen mode Exit fullscreen mode

and you’re golden. No mysterious TS2339, no runtime undefined, just clean, type‑safe shared state—ready for React 19 Concurrent features.

Happy coding & may your Providers always compile! 🚀

✍️ Written by: Cristian Sifuentes – Full-stack dev crafting scalable apps with [NET - Azure], [Angular - React], Git, SQL & extensions. Clean code, dark themes, atomic commits

*#react #typescript #context #state‑management #frontend*

Top comments (1)

Collapse
 
parizad profile image
Parizad

that was a great article. I write about Agile in my blog. I would be happy if you read my articles and write me your comments.