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<{}>'.
you accidentally wrapped createContext
in a function:
export const TodoContext = () => createContext({});
Remove the arrow so you export the Context instance, not a function:
export const TodoContext = createContext({});
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’svalue
.
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({});
What happens?
-
TodoContext
’s type becomes() => Context<{}>
—just a factory function. - At call‑sites:
<TodoContext.Provider …/> // ❌ TS2339: 'Provider' does not exist
TypeScript is right: the function has no .Provider
.
Correct code
// ✅ Do this
export const TodoContext = createContext({});
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: () => {},
});
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>
);
};
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;
}
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);
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)
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.