Server Toast

PreviousNext

A server-side toast notification component for Next.js server actions.

This component is a custom implementation following the Build UI article.

About

The Server Toast component is a complete notification system for React Server Components that uses cookies, useOptimistic, and Sonner to display toast messages from Server Actions.

Features

  • Toast from Server Actions: Trigger toasts directly from your server actions
  • Four toast types: success, error, warning, info
  • Cookie persistence: Toasts survive redirects and page reloads
  • Instant dismissal with useOptimistic
  • Modern Sonner interface that's accessible
  • Complete TypeScript support
  • Zod validation for data security

Preview

Installation

1. Install the component

pnpm dlx shadcn@latest add https://ui.nowts.app/r/server-toast.json

2. Add the Toaster to your layout

app/layout.tsx
import { Suspense } from "react"
 
import { ServerToaster } from "@/components/server-toast/server-toast.server"
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
 
        <Suspense>
          <ServerToaster />
        </Suspense>
      </body>
    </html>
  )
}

Usage

Basic usage

actions.ts
import { serverToast } from "@/components/server-toast"
 
export async function saveUserAction(formData: FormData) {
  "use server"
 
  try {
    // Your save logic
    const user = await saveUser(formData)
 
    await serverToast("User saved successfully!", "success")
  } catch (error) {
    await serverToast("Failed to save user", "error")
  }
}

All toast types

actions.ts
import { serverToast } from "@/components/server-toast"
 
// Success toast
await serverToast("Operation completed!", "success")
 
// Error toast
await serverToast("Something went wrong!", "error")
 
// Warning toast
await serverToast("Please check your input", "warning")
 
// Info toast (default)
await serverToast("Here's some information", "info")
// or simply
await serverToast("Here's some information")

With forms

components/CreatePostForm.tsx
import { redirect } from "next/navigation"
 
import { serverToast } from "@/components/server-toast"
 
async function createPostAction(formData: FormData) {
  "use server"
 
  const title = formData.get("title") as string
  const content = formData.get("content") as string
 
  if (!title || !content) {
    await serverToast("Title and content are required", "warning")
    return
  }
 
  try {
    await createPost({ title, content })
    await serverToast("Post created successfully!", "success")
    redirect("/posts")
  } catch (error) {
    await serverToast("Failed to create post", "error")
  }
}
 
export function CreatePostForm() {
  return (
    <form action={createPostAction} className="space-y-4">
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Post content" required />
      <button type="submit">Create Post</button>
    </form>
  )
}

With API Routes

app/api/data/route.ts
import { NextRequest } from "next/server"
 
import { serverToast } from "@/components/server-toast"
 
export async function POST(request: NextRequest) {
  try {
    const data = await request.json()
 
    // Your API logic
    await processData(data)
 
    await serverToast("Data processed successfully!", "success")
 
    return Response.json({ success: true })
  } catch (error) {
    await serverToast("Failed to process data", "error")
    return Response.json({ error: "Failed" }, { status: 500 })
  }
}

API

serverToast

function serverToast(
  message: string,
  type?: "success" | "error" | "warning" | "info"
): Promise<void>
ParameterTypeDescriptionDefault
messagestringThe message to display-
type"success" | "error" | "warning" | "info"The toast type"info"

ServerToaster

Server component that reads toast cookies and displays them. Must be placed within a <Suspense> boundary.

<Suspense>
  <ServerToaster />
</Suspense>

Architecture

The Server Toast system works in 3 parts:

1. Toast creation (Server)

  • serverToast() creates a cookie with a unique ID
  • Message and type are serialized to JSON
  • Cookie expires after 24h

2. Toast reading (Server)

  • ServerToaster reads all cookies starting with "toast-"
  • Validates data with Zod
  • Creates server actions to delete each toast

3. Optimistic display (Client)

  • ClientToasts uses useOptimistic for instant dismissal
  • Integrates with Sonner for UI
  • Dismissing a toast triggers server action in background

Benefits

Cross-navigation persistence

Toasts survive:

  • Server redirects
  • Page reloads
  • Tab navigation
  • Browser close/reopen

Optimal performance

  • Server-side rendering: No content flash
  • Instant dismissal: useOptimistic for UX
  • Lazy loading: Toaster in <Suspense>

Security

  • HTTP-only cookies optional
  • Zod validation of data
  • No JS exposure of sensitive data

Advanced examples

With custom middleware

actions.ts
import { auth } from "@/lib/auth"
import { serverToast } from "@/components/server-toast"
 
export async function authenticatedAction(formData: FormData) {
  "use server"
 
  const user = await auth.getUser()
 
  if (!user) {
    await serverToast("Please sign in to continue", "warning")
    redirect("/login")
    return
  }
 
  // Authenticated action...
  await serverToast(`Welcome ${user.name}!`, "success")
}

Conditional toast

actions.ts
import { serverToast } from "@/components/server-toast"
 
export async function updateUserAction(formData: FormData) {
  "use server"
 
  const changes = getChanges(formData)
 
  if (changes.length === 0) {
    await serverToast("No changes to save", "info")
    return
  }
 
  const result = await updateUser(changes)
 
  if (result.warnings.length > 0) {
    await serverToast(
      `Saved with ${result.warnings.length} warnings`,
      "warning"
    )
  } else {
    await serverToast("All changes saved successfully!", "success")
  }
}

Complex notification system

actions.ts
import { serverToast } from "@/components/server-toast"
 
export async function bulkImportAction(formData: FormData) {
  "use server"
 
  const file = formData.get("file") as File
  const results = await processBulkImport(file)
 
  // Multiple messages based on result
  if (results.errors.length > 0) {
    await serverToast(
      `${results.errors.length} items failed to import`,
      "error"
    )
  }
 
  if (results.warnings.length > 0) {
    await serverToast(
      `${results.warnings.length} items imported with warnings`,
      "warning"
    )
  }
 
  await serverToast(
    `${results.success.length} items imported successfully!`,
    "success"
  )
}

Troubleshooting

Toasts don't appear

  • Check that <ServerToaster /> is in your layout
  • Make sure it's within a <Suspense> boundary
  • Verify that cookies are enabled

Duplicate toasts

  • Toasts use unique IDs to prevent duplicates
  • If you see duplicates, check that <ServerToaster /> is only rendered once

Slow performance

  • Toasts use useOptimistic for instant dismissal
  • Server-side deletion happens in the background
  • If slow, check your cookie configuration

Source

Based on the excellent article by Build UI about toast messages in React Server Components.