react-compose-providers
v0.1.4
Published
A lightweight, type-safe helper to compose React context providers as a flat array
Maintainers
Readme
react-compose-providers
Compose React context providers as a flat, fully-typed array. A small helper with real type safety — extracted from a production application.
Motivation
Every React app reaches a point where the root looks like this:
<QueryClientProvider client={queryClient}>
<ThemeProvider theme="dark">
<I18nProvider locale="en">
<AuthProvider>
<RouterProvider>
<App />
</RouterProvider>
</AuthProvider>
</I18nProvider>
</ThemeProvider>
</QueryClientProvider>This is the React variant of callback hell — a pyramid that grows every time a new cross-cutting concern shows up. And like callback hell, the fix is the same in spirit: flatten it.
react-compose-providers is a ~45-line helper that lets you declare those providers as a flat, fully-typed array:
import { composeProviders, provider } from 'react-compose-providers';
const AppProviders = composeProviders([
provider(QueryClientProvider, { client: queryClient }),
provider(ThemeProvider, { theme: 'dark' }),
provider(I18nProvider, { locale: 'en' }),
provider(AuthProvider),
provider(RouterProvider),
]);
function App() {
return (
<AppProviders>
<Routes />
</AppProviders>
);
}This is not a panacea
The providers don't disappear — they're still there, still wrapping your app, still doing the same thing. What changes is how you read, diff, and reorder them:
- Git history stays clean. Adding or reordering a provider is a one-line diff, not a re-indentation storm.
- Cognitive load drops. A flat list reads top-to-bottom. A 7-level pyramid you have to unfold in your head.
- Types stay tight. Each provider's props stay required when they should be, optional when they can be. No
any[], no manual casts.
When you don't need it
If your root has two providers — just write them nested. YAGNI. This helper earns its keep when the pyramid reaches ~5 levels, not before.
The motto: make the complex simple — not by hiding complexity, but by removing the cognitive tax on reading it.
Install
npm install react-compose-providerspnpm add react-compose-providersyarn add react-compose-providersRequires react >= 16.8 as a peer dependency. No other runtime dependencies.
Quick Start
import { composeProviders, provider } from 'react-compose-providers';
import { ThemeProvider } from './theme';
import { AuthProvider } from './auth';
// 1. Compose once, at module level
const AppProviders = composeProviders([
provider(ThemeProvider, { theme: 'dark' }),
provider(AuthProvider),
]);
// 2. Use it like any other wrapper component
function App() {
return (
<AppProviders>
<Routes />
</AppProviders>
);
}The first provider in the array is the outermost in the tree. provider(Component, props?) is a tiny factory that preserves each component's type so TypeScript can check props per entry.
Full Example
A realistic app shell with four providers, most of them requiring props:
// providers.ts
import { composeProviders, provider } from 'react-compose-providers';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from './theme';
import { I18nProvider } from './i18n';
import { AuthProvider } from './auth';
const queryClient = new QueryClient();
export const AppProviders = composeProviders([
provider(QueryClientProvider, { client: queryClient }),
provider(ThemeProvider, { theme: 'light' }),
provider(I18nProvider, { locale: 'en' }),
provider(AuthProvider),
]);// App.tsx
import { AppProviders } from './providers';
import { Routes } from './routes';
export function App() {
return (
<AppProviders>
<Routes />
</AppProviders>
);
}Adding, removing, or reordering a provider is a one-line edit to providers.ts. No JSX reshuffling, no re-indentation, no silently broken nesting.
API
| Export | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| composeProviders(entries) | Factory that returns a React.FC<PropsWithChildren> wrapping its children in the given providers, outermost-first. |
| provider(Component, props?) | Creates a typed entry for the array. The props argument is conditionally required based on the component's type. |
| ProviderEntry<P> | Type of an entry. Exported for advanced use — most users can let TypeScript infer this and never reference it. |
TypeScript
The library's main feature: required props stay required. A provider with a required prop cannot be composed without passing it — at compile time, not at runtime.
import { composeProviders, provider } from 'react-compose-providers';
// Requires `theme`
function ThemeProvider({
theme,
children,
}: {
theme: 'light' | 'dark';
children: React.ReactNode;
}) {
/* ... */
}
// No required props
function DebugProvider({ children }: { children: React.ReactNode }) {
/* ... */
}
composeProviders([
// ✓ Props inferred and checked
provider(ThemeProvider, { theme: 'dark' }),
// ✓ No props — one-arg call is allowed
provider(DebugProvider),
// ✗ Type error: Expected 2 arguments, but got 1
// @ts-expect-error
provider(ThemeProvider),
// ✗ Type error: "red" is not assignable to 'light' | 'dark'
// @ts-expect-error
provider(ThemeProvider, { theme: 'red' }),
]);This is all compile-time work. At runtime, provider() is a thin factory — no validation, no overhead, nothing to trip over.
How It Works
composeProviders(entries)returns a component that usesArray.reduceRightto wrap children in each provider, outermost-first.provider()is a factory with two overloads: a one-argument form that accepts any component whose only declared prop ischildren, and a two-argument form that requires a matchingpropsobject. TypeScript picks the first overload when applicable, the second otherwise — so providers with required props force you to pass them at the call site.- That's the whole library. ~55 lines of source, zero runtime dependencies.
Call
composeProvidersonce, at module level (or memoize withuseMemo). Calling it insiderendercreates a new component on every render, which remounts the entire subtree. This isn't a library quirk — it's how React treats component identity.
Comparison with Alternatives
| Library | Approach | TypeScript | Conditional required props | Maintained |
| ------------------------------------------- | --------------------------- | ---------------------------- | -------------------------- | ------------ |
| react-compose-providers | reduceRight helper | Full, per-provider inference | Yes | Active |
| compose-providers | Similar helper | Provider<any>[] | No | Active |
| react-pipeline-component | JSX <Pipeline> components | — | No | Inactive |
| @ascodelife/react-context-provider-composer | Helper | Partial | No | Low activity |
Note: this pattern doesn't eliminate providers — it organizes them. If your root has 2 providers, you don't need this library.
