# SSR & Next.js

> What renders on the server, who owns the controller, and complete setups for Next.js and Vite.

Source: https://cloudlet.dev/toaster/docs/ssr

Cloudlet renders nothing on the server and appears after hydration, so it never causes a hydration mismatch. This page explains what that means for where the controller lives and when it ends, then gives complete setups for the Next.js App Router and Pages Router and a note on Vite.

## What renders on the server

The Toaster's server output is empty, and so is its first render during hydration, even when notifications were accepted before it mounted. After mount it portals its host into the document and presents whatever is pending. Notifications you `show` before the Toaster mounts are not lost: they wait in the controller.

The package keeps the `"use client"` directive on its React entry, so importing `Toaster`, `CloudletProvider` or `useCloudlet` from a Server Component file marks the boundary correctly. The controller itself is a live object with methods and subscriptions; it is not serializable and never crosses from a Server Component into a client one.

If you read the controller's state in your own components, subscribe the same way the Toaster does. `getServerSnapshot()` returns an empty snapshot, so the server and the first client render agree, and the real state arrives right after hydration:

```tsx title="notification-count.tsx"
'use client';
import { useSyncExternalStore } from 'react';
import { useCloudlet } from '@cloudlet/toast';

/** How many notifications wait in the stack; 0 on the server and during hydration. */
export function NotificationCount() {
  const cloudlet = useCloudlet();
  const count = useSyncExternalStore(
    cloudlet.subscribe,
    () => cloudlet.getSnapshot().stack.length,
    () => cloudlet.getServerSnapshot().stack.length,
  );
  return count > 0 ? <span>{count} new</span> : null;
}
```

## One controller per session or request

A controller holds one user's notifications. Create it once per browser session, and never share one between users:

- **In the browser**, one controller lives as long as the page or the part of the application that owns it.
- **On the server**, a controller that takes part in rendering belongs to that request alone. A module-level controller in code that also runs on the server would be shared by every request, so in server-rendered applications the controller is created in the browser, by a client component, as below.

When you render on the server yourself, create the controller inside the request and dispose it once the response is written:

```tsx title="entry-server.tsx"
import { renderToString } from 'react-dom/server';
import { CloudletProvider, createCloudlet, Toaster } from '@cloudlet/toast';

function App() {
  return <main>Weather station</main>;
}

export function render() {
  // This request's own controller: the Toaster renders nothing into the HTML.
  const cloudlet = createCloudlet();
  try {
    return renderToString(
      <CloudletProvider controller={cloudlet}>
        <App />
        <Toaster />
      </CloudletProvider>,
    );
  } finally {
    cloudlet.dispose();
  }
}
```

The browser entry then creates its own controller and hydrates the same tree.

### When to dispose

`dispose()` is final: it closes every notification, releases the controller's timers and subscriptions, and every later call returns `disposed`. Business operations you tracked keep running to their own result. Call it only when the session the controller belongs to is over: a test that finished, a micro-frontend removed for good, a server request that has been answered. A single-page application usually never calls it; the controller lives as long as the page.

Unmounting the Toaster is not the end of the controller. It pauses visible time and keeps every record and every tracked operation; mounting a Toaster again resumes the same queue.

Two mistakes to avoid:

- **Creating the controller in a component body.** Every render would create a new controller with an empty stack. Create it in the bootstrap, or in an effect of a client owner as in the [App Router](#nextjs-app-router) setup.
- **Sharing a server controller between users.** On the server a controller belongs to one request.

## Next.js App Router

The App Router renders every page on the server first, so the controller is owned by a client component. Its effect creates the controller, and its cleanup disposes exactly that instance. Your pages keep rendering on the server: until the controller exists, the Provider gets `null`, the Toaster renders nothing and your children render as usual.

```tsx title="components/notification-owner.tsx"
'use client';
import { useEffect, useState, type ReactNode } from 'react';
import { CloudletProvider, createCloudlet, Toaster, type ReactCloudletController } from '@cloudlet/toast';

export function NotificationOwner({ children }: { children: ReactNode }) {
  const [controller, setController] = useState<ReactCloudletController | null>(null);

  useEffect(() => {
    const instance = createCloudlet();
    setController(instance);
    // Dispose this instance, not whichever controller is current.
    return () => instance.dispose();
  }, []);

  // `null` on the server and in the first client render: the Toaster waits, the children do not.
  return (
    <CloudletProvider controller={controller}>
      {children}
      <Toaster />
    </CloudletProvider>
  );
}
```

Under a strict Content Security Policy the owner also passes the request's nonce to the Toaster: [Content Security Policy](https://cloudlet.dev/toaster/docs/csp#nextjs-app-router) shows how.

The root layout imports the stylesheet once and wraps the application in the owner:

```tsx title="app/layout.tsx"
import '@cloudlet/toast/styles.css';
import type { ReactNode } from 'react';
import { NotificationOwner } from '../components/notification-owner';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NotificationOwner>{children}</NotificationOwner>
      </body>
    </html>
  );
}
```

Client components show notifications through `useOptionalCloudlet()`. Before mount it returns `null`, which only matters for code that runs during render; events fire after hydration, when the controller exists:

```tsx title="components/save-button.tsx"
'use client';
import { useOptionalCloudlet } from '@cloudlet/toast';

export function SaveButton() {
  const cloudlet = useOptionalCloudlet();
  return (
    <button
      disabled={!cloudlet}
      onClick={() => cloudlet?.show({ title: 'Forecast saved', kind: 'success' })}
    >
      Save forecast
    </button>
  );
}
```

`useCloudlet()` throws while the controller is `null`, so keep it for components that render only after the owner has mounted, such as content of a dialog the user opens.

The shadcn registry item works a little differently: its wrapper renders the Provider and the Toaster for a controller you pass in, and the `notification-owner.tsx` of [Installation](https://cloudlet.dev/toaster/docs/installation#the-shadcn-registry-item) renders it once that controller exists.

### React Strict Mode

In development Strict Mode runs the owner's effect, its cleanup, and the effect again. The first controller is disposed before anything uses it, the second one is stored, and the Toaster attaches to the second. That works because the cleanup disposes the instance its own effect created. Two variants break under the replay:

- disposing the controller from state (`controller.dispose()` in a cleanup), which disposes the instance the second setup still needs;
- creating the controller once outside the effect and disposing it in the cleanup, which leaves the remounted owner with a disposed controller.

## Next.js Pages Router

In the Pages Router, global CSS is imported from the custom App, and the same client owner wraps every page. `_app` stays mounted across client-side navigations, so one controller serves the whole session:

```tsx title="pages/_app.tsx"
import '@cloudlet/toast/styles.css';
import type { AppProps } from 'next/app';
import { NotificationOwner } from '../components/notification-owner';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <NotificationOwner>
      <Component {...pageProps} />
    </NotificationOwner>
  );
}
```

With the shadcn wrapper, its two CSS imports move into `_app` too, in the order given in [Installation](https://cloudlet.dev/toaster/docs/installation#the-shadcn-registry-item).

Both routers need a matching pair of `react` and `react-dom`, in the range Cloudlet supports.

## Vite and other client-only apps

A single-page application without server rendering has nothing to hydrate. Create the controller once at module scope of the client entry and render the Provider and the Toaster there, as in the [Quick start](https://cloudlet.dev/toaster/docs/quick-start#mount-the-toaster). With Vite's SSR mode, the server entry follows [One controller per session or request](#one-controller-per-session-or-request).

## Next steps

- [Content Security Policy](https://cloudlet.dev/toaster/docs/csp): the request's nonce and a policy Cloudlet works under.
- [Toaster props](https://cloudlet.dev/toaster/docs/api/toaster): `container`, `nonce` and every other prop.
