next-shell

Providers

AppProviders and the individual provider layers — theme, data, toasts, errors, and i18n.

@jonmatum/next-shell/providers exports a single AppProviders composer that nests all standard providers in the correct order, plus individual providers for opt-in composition.

AppProviders

The one-import solution for the full provider stack:

ThemeProvider
  QueryProvider (TanStack Query v5)
    ToastProvider (Sonner)
      ErrorBoundary
        TooltipProvider (Radix)
          I18nProvider
            {children}

Each layer can be disabled by passing false:

import { AppProviders } from '@jonmatum/next-shell/providers';
 
<AppProviders
  themeProps={{ defaultTheme: 'dark' }}
  queryProps={{ client: serverQueryClient }}
  toastProps={{ position: 'top-center' }}
  i18nProps={false}
>
  {children}
</AppProviders>;

ThemeProvider

Wraps next-themes. All props are forwarded.

import { ThemeProvider, ThemeToggle } from '@jonmatum/next-shell/providers';
 
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
  <ThemeToggle />
</ThemeProvider>;

Use useTheme() to read and set the current theme:

import { useTheme } from '@jonmatum/next-shell/providers';
 
function MyComponent() {
  const { theme, setTheme, resolvedTheme } = useTheme();
  return <button onClick={() => setTheme('dark')}>Go dark</button>;
}

QueryProvider

TanStack Query v5 with sane defaults (staleTime: 60s, retry: 1, refetchOnWindowFocus: false).

import { QueryProvider } from '@jonmatum/next-shell/providers';
 
<QueryProvider>{children}</QueryProvider>;

SSR prefetching

// app/page.tsx (Server Component)
import { makeServerQueryClient } from '@jonmatum/next-shell/providers';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
 
export default async function Page() {
  const queryClient = makeServerQueryClient();
  await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts });
 
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostList />
    </HydrationBoundary>
  );
}

ToastProvider

Wraps Sonner. Import toast from sonner directly in your code.

import { toast } from 'sonner';
 
toast.success('Saved!');
toast.error('Something went wrong', { description: 'Try again later.' });

Override position, theme, or any Sonner prop via toastProps:

<AppProviders toastProps={{ position: 'top-right', richColors: true }}>

ErrorBoundary

Class-based boundary with a default "Something went wrong / Try again" fallback. Accepts a custom fallback render prop:

import { ErrorBoundary } from '@jonmatum/next-shell/providers';
 
<ErrorBoundary
  fallback={({ error, reset }) => (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
  onError={(error, info) => captureException(error, info)}
>
  <MyComponent />
</ErrorBoundary>;

I18nProvider

Adapter-based i18n — plug in any library:

import { I18nProvider } from '@jonmatum/next-shell/providers';
import { useTranslations } from 'next-intl';
 
function Root({ children }: { children: React.ReactNode }) {
  const t = useTranslations();
  return <I18nProvider adapter={{ t }}>{children}</I18nProvider>;
}

In components, useI18n() returns the adapter or null when no provider is mounted — always guard the call:

import { useI18n } from '@jonmatum/next-shell/providers';
 
function MyComponent() {
  const i18n = useI18n();
  return <span>{i18n?.t('hello') ?? 'Hello'}</span>;
}

Read the theme before React renders to eliminate the light-flash:

// app/layout.tsx (Server Component)
import { cookies } from 'next/headers';
import { getThemeFromCookies } from '@jonmatum/next-shell/providers/server';
 
export default async function RootLayout({ children }) {
  const theme = getThemeFromCookies(await cookies()) ?? 'system';
  return (
    <html lang="en" data-theme={theme} suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

On this page