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.
Try it out: Open the example below in a new tab and try it out. Open in new tab
Overview
The save functionality provides a complete workflow:
- Create - Save new designs and get a unique post ID
- Edit - Load existing designs by post ID and make changes
- Update - Save changes to existing posts while preserving metadata
- 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.
"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
Note: We're using Redis (Upstash) as the database, but you can adapt this to work with any database (PostgreSQL, MongoDB, etc.).
The upsertPost
function handles both creating new posts and updating existing ones.
"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:
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} />
}