react-context-manager
v1.0.1
Published
Automatic React Context composition with ordering and dependency management.
Maintainers
Readme
react-context-manager
Automatic React Context composition with ordering and dependency management.
react-context-manager makes it easier to manage multiple React Context providers in medium and large React applications.
Instead of manually nesting providers:
<AuthProvider>
<ConfigProvider>
<ThemeProvider>
<UserProvider>
<App />
</UserProvider>
</ThemeProvider>
</ConfigProvider>
</AuthProvider>you can register your contexts and let ContextRoot compose them automatically.
<ContextRoot>
<App />
</ContextRoot>The library can determine provider order using explicit dependencies and priorities.
Features
- Automatic composition of multiple React Context providers
- Dependency-based provider ordering
- Priority-based ordering for independent providers
- Support for existing React Contexts
- Create and register contexts through the package
- Create contexts and providers together with
defineProvider - Named context access with
useNamedContext - Retrieve registered contexts with
fetchContext - TypeScript support
- React 18 and React 19 support
- Provider registration validation
- Missing dependency detection
- Circular dependency detection
- Dynamic provider registration support
Installation
npm install react-context-managerReact must already be installed in your application.
npm install react react-context-managerBasic Concept
The package uses three main concepts:
defineContext
Register a provider that already exists in your application.
defineContext({
name: "AuthContext",
provider: AuthProvider,
});defineProvider
Create a React Context and its provider together.
const auth = defineProvider({
name: "AuthContext",
defaultValue: null,
useValue: () => null,
});ContextRoot
Automatically composes all registered providers.
<ContextRoot>
<App />
</ContextRoot>Example 1 — Using Existing Contexts From an Existing Application
This example is useful when you already have a React application with contexts and providers.
Suppose your existing application already contains an authentication context.
Existing context
// AuthContext.tsx
import React, { createContext, useContext, useState } from "react";
type User = {
id: string;
name: string;
};
type AuthContextValue = {
user: User | null;
login: (user: User) => void;
logout: () => void;
};
export const AuthContext = createContext<AuthContextValue>({
user: null,
login: () => {},
logout: () => {},
});
export function AuthProvider({ children }: React.PropsWithChildren) {
const [user, setUser] = useState<User | null>(null);
const value: AuthContextValue = {
user,
login: (user) => {
setUser(user);
},
logout: () => {
setUser(null);
},
};
return (
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}Normally, the application might use it like this:
<AuthProvider>
<App />
</AuthProvider>With react-context-manager, register the existing context and its existing provider.
Register the existing context
// contextRegistry.ts
import { defineContext, registerContext } from "react-context-manager";
import { AuthContext, AuthProvider } from "./AuthContext";
registerContext("AuthContext", AuthContext);
defineContext({
name: "AuthContext",
provider: AuthProvider,
});The two registrations have different purposes:
registerContext()registers the existing React Context with the manager.defineContext()registers the provider that should be composed byContextRoot.
This allows an existing context implementation to be integrated without rewriting it.
Use ContextRoot
// main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import { ContextRoot } from "react-context-manager";
import "./contextRegistry";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<ContextRoot>
<App />
</ContextRoot>,
);ContextRoot now renders the registered AuthProvider around the application.
Conceptually, the result is still:
<AuthProvider>
<App />
</AuthProvider>but the provider does not need to be manually nested in your application root.
Consuming the existing context
You can continue using the application's existing hook:
import { useAuth } from "./AuthContext";
export function Profile() {
const { user, logout } = useAuth();
return (
<div>
<p>{user?.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}You can also access the registered context by name:
import { useNamedContext } from "react-context-manager";
type AuthContextValue = {
user: {
id: string;
name: string;
} | null;
login: (user: { id: string; name: string }) => void;
logout: () => void;
};
export function Profile() {
const { user, logout } = useNamedContext<AuthContextValue>("AuthContext");
return (
<div>
<p>{user?.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}When to use this approach
Use this approach when:
- your project already has React Contexts
- providers already exist
- you want to introduce automatic provider composition
- you don't want to rewrite existing context implementations
The existing Context and Provider can remain in the application while react-context-manager handles their registration and composition.
Example 2 — Creating Contexts Using Only react-context-manager
You can also build your contexts entirely through the package.
This approach uses defineProvider().
Let's create an authentication context and a user context.
Define the authentication provider
// contexts.ts
import { useState } from "react";
import { defineProvider } from "react-context-manager";
type User = {
id: string;
name: string;
};
type AuthValue = {
user: User | null;
login: (user: User) => void;
logout: () => void;
};
export const Auth = defineProvider<AuthValue>({
name: "AuthContext",
defaultValue: {
user: null,
login: () => {},
logout: () => {},
},
useValue: () => {
const [user, setUser] = useState<User | null>(null);
return {
user,
login: (user: User) => {
setUser(user);
},
logout: () => {
setUser(null);
},
};
},
});defineProvider() creates and registers:
- the React Context
- the Provider
- the context definition
- a
useContext()helper
The returned registration contains:
Auth.name;
Auth.context;
Auth.provider;
Auth.useContext;Define a dependent provider
Now create a user context that depends on the authentication context.
type UserValue = {
userId: string | null;
};
export const User = defineProvider<UserValue>({
name: "UserContext",
defaultValue: {
userId: null,
},
dependsOn: ["AuthContext"],
useValue: () => {
const auth = Auth.useContext();
return {
userId: auth.user?.id ?? null,
};
},
});The important part is:
dependsOn: ["AuthContext"];This tells the manager that AuthContext must be composed before UserContext.
You don't need to manually write:
<Auth.provider>
<User.provider>
<App />
</User.provider>
</Auth.provider>The manager handles that ordering.
Use ContextRoot
Registering the providers is enough.
// main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import { ContextRoot } from "react-context-manager";
import "./contexts";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<ContextRoot>
<App />
</ContextRoot>,
);The package automatically composes the providers based on their dependency graph.
Conceptually:
<Auth.provider>
<User.provider>
<App />
</User.provider>
</Auth.provider>Consume the context
You can use the useContext function returned from defineProvider():
import { Auth, User } from "./contexts";
export function Profile() {
const auth = Auth.useContext();
const user = User.useContext();
return (
<div>
<h2>{auth.user?.name ?? "Guest"}</h2>
<p>User ID: {user.userId ?? "Not logged in"}</p>
{auth.user && <button onClick={auth.logout}>Logout</button>}
</div>
);
}Alternatively, use the named API:
import { useNamedContext } from "react-context-manager";
export function Profile() {
const auth = useNamedContext<AuthValue>("AuthContext");
const user = useNamedContext<UserValue>("UserContext");
return (
<div>
<h2>{auth.user?.name ?? "Guest"}</h2>
<p>User ID: {user.userId ?? "Not logged in"}</p>
</div>
);
}When to use this approach
Use defineProvider() when:
- you are starting a new application
- you want the package to create the Contexts for you
- you want providers to be registered automatically
- you want dependency relationships between contexts
- you want to access contexts through named APIs
Provider Ordering
Provider ordering is one of the main features of the package.
There are two ways to control ordering:
- Dependencies
- Priority
Dependencies
Use dependsOn when one provider depends on another.
defineContext({
name: "UserContext",
provider: UserProvider,
dependsOn: ["AuthContext"],
});
defineContext({
name: "AuthContext",
provider: AuthProvider,
});The resulting order is:
AuthProvider
↓
UserProvider
↓
ApplicationEven though UserContext was registered first, its dependency causes AuthContext to be placed before it.
Priority
For independent contexts, use priority.
defineContext({
name: "LowPriority",
provider: LowPriorityProvider,
priority: 10,
});
defineContext({
name: "HighPriority",
provider: HighPriorityProvider,
priority: 1,
});Lower priority values are composed first:
HighPriorityProvider
↓
LowPriorityProvider
↓
ApplicationPriority is useful when providers do not depend directly on each other but still need deterministic ordering.
Combining Dependencies and Priority
You can use both:
defineContext({
name: "UserContext",
provider: UserProvider,
priority: 10,
dependsOn: ["AuthContext"],
});
defineContext({
name: "AuthContext",
provider: AuthProvider,
priority: 1,
});Dependencies determine required ordering relationships, while priority provides ordering for contexts that are otherwise independent.
API Reference
ContextRoot
Automatically composes all registered providers around its children.
<ContextRoot>
<App />
</ContextRoot>defineContext()
Registers an existing provider.
defineContext({
name: "AuthContext",
provider: AuthProvider,
});Options
defineContext({
name: "UserContext",
provider: UserProvider,
priority: 10,
dependsOn: ["AuthContext"],
});| Property | Description |
| ----------- | --------------------------------------------------- |
| name | Unique name of the context |
| provider | React provider component |
| priority | Optional ordering priority |
| dependsOn | Optional list of context names that must come first |
defineProvider()
Creates a React Context and Provider and registers them with the manager.
const Auth = defineProvider({
name: "AuthContext",
defaultValue: null,
useValue: () => null,
});Options
defineProvider({
name: "AuthContext",
defaultValue: null,
useValue: () => null,
displayName: "AuthProvider",
priority: 1,
dependsOn: ["ConfigContext"],
});| Property | Description |
| -------------- | ------------------------------------------------------------------- |
| name | Unique context name |
| defaultValue | Default React Context value |
| useValue | Function used by the generated Provider to obtain the current value |
| displayName | Optional React Provider display name |
| priority | Optional ordering priority |
| dependsOn | Optional provider dependencies |
The returned registration contains:
{
name,
context,
provider,
useContext,
}createContext()
Creates and registers a React Context without creating a provider definition.
const AuthContext = createContext("AuthContext", null);This is useful when you want the package to manage the Context registry but want to define the provider separately.
const AuthContext = createContext("AuthContext", null);
function AuthProvider({ children }: React.PropsWithChildren) {
return <AuthContext.Provider value={null}>{children}</AuthContext.Provider>;
}
defineContext({
name: "AuthContext",
provider: AuthProvider,
});registerContext()
Registers an existing React Context.
const AuthContext = React.createContext(null);
registerContext("AuthContext", AuthContext);This is particularly useful when integrating the package into an existing application.
fetchContext()
Retrieves a registered React Context by name.
const AuthContext = fetchContext<AuthValue>("AuthContext");If the context does not exist, the function throws an error.
useNamedContext()
Reads a registered React Context by name.
const auth = useNamedContext<AuthValue>("AuthContext");This can be useful when you want to consume contexts without importing individual Context objects.
Choosing the Right API
| Situation | API |
| ------------------------------------------------------------ | --------------------------------------- |
| Existing Context + existing Provider | registerContext() + defineContext() |
| Existing Provider but no need to register Context separately | defineContext() |
| Create Context + Provider through the package | defineProvider() |
| Create a Context through the package | createContext() |
| Retrieve a Context object by name | fetchContext() |
| Consume a Context by name | useNamedContext() |
| Automatically compose providers | ContextRoot |
Error Handling
The manager validates the provider registry and dependency graph.
For example, if a React Context is created but no provider is registered:
createContext("AuthContext", null);and the application renders:
<ContextRoot>
<App />
</ContextRoot>the manager reports the missing provider registration.
Missing dependencies are also detected:
defineContext({
name: "UserContext",
provider: UserProvider,
dependsOn: ["AuthContext"],
});If AuthContext has not been registered, an error is raised.
Circular dependencies are also detected:
ContextA → ContextB
ContextB → ContextAThis prevents an invalid provider composition from being rendered.
Dynamic Registration
Providers can be registered after ContextRoot has already mounted.
function App() {
return (
<ContextRoot>
<Application />
</ContextRoot>
);
}Later, another provider can be registered:
defineProvider({
name: "DynamicContext",
defaultValue: null,
useValue: () => null,
});When the application renders again, ContextRoot can incorporate the updated registry.
TypeScript
The package is written in TypeScript and provides type declarations.
For example:
type AuthValue = {
user: string | null;
};
const Auth = defineProvider<AuthValue>({
name: "AuthContext",
defaultValue: {
user: null,
},
useValue: () => ({
user: "Arush",
}),
});Context consumers remain type-safe:
const auth = Auth.useContext();
auth.user;You can also provide the type when using named access:
const auth = useNamedContext<AuthValue>("AuthContext");Recommended Project Structure
A project can keep context definitions in a dedicated directory:
src/
├── contexts/
│ ├── auth.ts
│ ├── user.ts
│ └── index.ts
│
├── components/
│ └── App.tsx
│
├── App.tsx
└── main.tsxFor example:
// contexts/auth.ts
export const Auth = defineProvider<AuthValue>({
name: "AuthContext",
defaultValue: {
user: null,
},
useValue: useAuthValue,
});Then import the context definitions before rendering ContextRoot:
import "./contexts";
createRoot(root).render(
<ContextRoot>
<App />
</ContextRoot>,
);How Provider Composition Works
The library maintains a registry of:
- React Contexts
- Provider definitions
- Dependencies
- Priorities
When ContextRoot renders, the registered definitions are resolved into an ordered provider chain.
For example:
defineContext({
name: "ConfigContext",
provider: ConfigProvider,
priority: 1,
});
defineContext({
name: "AuthContext",
provider: AuthProvider,
priority: 2,
});
defineContext({
name: "UserContext",
provider: UserProvider,
dependsOn: ["AuthContext"],
});The resulting structure is conceptually:
ConfigProvider
↓
AuthProvider
↓
UserProvider
↓
ApplicationThe application only needs:
<ContextRoot>
<App />
</ContextRoot>React Compatibility
This package declares React as a peer dependency and supports:
- React 18
- React 19
Install React separately in your application.
License
MIT License
Copyright (c) 2026 Arush Shrivastava
See LICENSE.md for the full license text.
Author
Arush Shrivastava
GitHub: https://github.com/arush-shri
