Save to Database

This example shows how to save and load layers from a database.

Learn how to persist designer layers to a database and load them back for editing. We'll use Next.js server actions with a unified upsertPost function to handle both create and update operations.

Overview

The save functionality provides a complete workflow:

  1. Create - Save new designs and get a unique post ID
  2. Edit - Load existing designs by post ID and make changes
  3. Update - Save changes to existing posts while preserving metadata
  4. View - Display saved designs in a static, read-only format

ActionSave Component

The ActionSave component demonstrates how to save layers using the useLayers hook and server actions.

components/action-save.tsx
"use client"
 
import { useLayers } from "@shadcn/designer"
import { Button } from "@shadcn/designer/ui"
import { useRouter } from "next/navigation"
import { useTransition } from "react"
import { toast } from "sonner"
import { upsertPost } from "./actions"
 
function ActionSave() {
  const layers = useLayers()
  const [isPending, startTransition] = useTransition()
  const router = useRouter()
 
  const handleSave = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    startTransition(async () => {
      const { error, postId } = await upsertPost(layers)
 
      if (error) {
        toast.error(error)
        return
      }
 
      if (postId) {
        router.push(`/examples/save/${postId}`)
      }
    })
  }
 
  return (
    <form onSubmit={handleSave}>
      <Button
        variant="primary"
        type="submit"
        disabled={isPending}
        isLoading={isPending}
      >
        Save
      </Button>
    </form>
  )
}

upsertPost

The upsertPost function handles both creating new posts and updating existing ones.

actions.ts
"use server"
 
import type { Layer } from "@shadcn/designer"
import { generateId } from "@shadcn/designer/utils"
import { Redis } from "@upstash/redis"
 
const redis = Redis.fromEnv()
 
export async function upsertPost(layers: Layer[], postId?: string) {
  try {
    // If postId provided, update existing post.
    if (postId) {
      const existing = await redis.get(`post:${postId}`)
 
      if (!existing) {
        return {
          postId: null,
          error: "Post not found",
        }
      }
 
      await redis.set(`post:${postId}`, {
        layers,
        createdAt:
          typeof existing === "object" && "createdAt" in existing
            ? existing.createdAt
            : new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      })
 
      return {
        postId,
        error: null,
      }
    }
 
    // Create new post.
    const newPostId = generateId()
    await redis.set(`post:${newPostId}`, {
      layers,
      createdAt: new Date().toISOString(),
    })
 
    return {
      postId: newPostId,
      error: null,
    }
  } catch (error) {
    console.error("Failed to upsert post", error)
    return {
      postId: null,
      error: error instanceof Error ? error.message : "Unknown error",
    }
  }
}

Loading Saved Layers

To load an existing design for editing, create a dynamic route that fetches the layers and passes both the layers and postId to the editor:

app/[postId]/page.tsx
import { notFound } from "next/navigation"
import { getPost } from "../actions"
import { SaveExampleEditor } from "../editor"
 
export default async function SaveExampleViewPage(props: {
  params: Promise<{
    postId: string
  }>
}) {
  const params = await props.params
  const layers = await getPost(params.postId)
 
  if (!layers) {
    notFound()
  }
 
  return <SaveExampleEditor layers={layers} postId={params.postId} />
}

getPost

import { Redis } from "@upstash/redis"
 
const redis = Redis.fromEnv()
 
export async function getPost(postId: string) {
  const entry = await redis.get<
    { layers: Layer[]; createdAt: string } | string
  >(`post:${postId}`)
 
  if (!entry) {
    return null
  }
 
  if (typeof entry === "string") {
    try {
      const parsed = JSON.parse(entry) as {
        layers: Layer[]
        createdAt: string
      }
      return parsed.layers ?? null
    } catch (error) {
      console.error("Failed to parse stored layers", error)
      return null
    }
  }
 
  if (!Array.isArray(entry.layers)) {
    return null
  }
 
  return entry.layers
}

Static View

For a read-only preview, use the DesignerStaticFrame component:

import { DesignerStaticFrame } from "@shadcn/designer"
 
export function SaveExampleViewStaticFrame({ layers }: { layers: Layer[] }) {
  return <DesignerStaticFrame layers={layers} width={1080} height={1080} />
}