Skip to content

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:

  1. main.tsx — Framework-level providers (QueryClient, Router). These are singletons that exist once per app.
  2. Providers component — UI-level providers (Theme, Tooltip, Toast, Motion). These can be reused or nested.

This separation means:

  • Your router root (__root.tsx) can also wrap with Providers
  • You can render Providers in 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>