Provider Composition
How to compose global React providers at the app root without nesting hell.
What Problem This Solves
You have 5+ global providers (Theme, QueryClient, Router, Toast, ErrorBoundary) and your main.tsx becomes a pyramid of nested JSX. This pattern composes them cleanly.
Prerequisites
- React 18+
Files
providers.tsx
tsx
import { domAnimation, LazyMotion } from "framer-motion";
import type { PropsWithChildren } from "react";
import { ThemeProvider } from "./theme-provider";
import { Toaster } from "./sonner";
import { TooltipProvider } from "./tooltip";
export const Providers = ({ children }: PropsWithChildren) => (
<ThemeProvider defaultTheme="light" storageKey="app-theme">
<LazyMotion features={domAnimation} strict>
<TooltipProvider>{children}</TooltipProvider>
</LazyMotion>
<Toaster />
</ThemeProvider>
);main.tsx
tsx
import { QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { Providers } from "./providers";
import { queryClient } from "./services/api/constants";
import { routeTree } from "./routeTree.gen";
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: "intent",
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<Providers>
<RouterProvider router={router} />
</Providers>
</QueryClientProvider>
</StrictMode>,
);__root.tsx (TanStack Router)
tsx
import { createRootRouteWithContext, HeadContent, Outlet } from "@tanstack/react-router";
import type { QueryClient } from "@tanstack/react-query";
import { Providers } from "./providers";
interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [{ title: "My App" }, { name: "description", content: "My application" }],
}),
component: () => (
<>
<HeadContent />
<Providers>
<Outlet />
</Providers>
</>
),
});The Pattern
The key insight is two layers:
main.tsx— Framework-level providers (QueryClient, Router). These are singletons that exist once per app.Providerscomponent — UI-level providers (Theme, Tooltip, Toast, Motion). These can be reused or nested.
This separation means:
- Your router root (
__root.tsx) can also wrap withProviders - You can render
Providersin Storybook without the router - You can test components with just the UI providers, no QueryClient needed
Variations
Without Framer Motion
Remove LazyMotion if you don't use Framer Motion:
tsx
export const Providers = ({ children }: PropsWithChildren) => (
<ThemeProvider defaultTheme="light">
<TooltipProvider>{children}</TooltipProvider>
<Toaster />
</ThemeProvider>
);With Error Boundary
Add an error boundary around the router:
tsx
<ErrorBoundary FallbackComponent={ErrorFallback}>
<RouterProvider router={router} />
</ErrorBoundary>