Troubleshooting live previews in Next.js project with dynamic loading and React errors.

3 replies
Last updated: Jan 5, 2024
Hi all, I'm having a bit of trouble with live previews in my Next.js project.
I'm trying to avoid duplicating every single component just to have a preview version and a regular one, so instead I made a generic preview component that dynamically loads the correct one and passes it the initial data and the query that it requires.

This kind of works but in preview mode everything flashes (probably due to the dynamic loading logic) and sometimes I get weird React errors saying
A previously unvisited boundary must have exactly one root segment. This is a bug in React.
.
Any suggestions on how to make all this work without weird generic shenanigans and code duplication? Thank you!
AI Update

I feel your pain! The good news is you don't need that generic wrapper component at all. The modern approach with Sanity's Live Content API is much cleaner and avoids the code duplication issue entirely.

The React error you're seeing ("A previously unvisited boundary must have exactly one root segment") is likely caused by the dynamic loading creating unstable component boundaries. This is a known React issue that can happen when components are dynamically loaded in certain ways, especially with Suspense boundaries.

Here's the recommended approach using Sanity's Live Content API with defineLive:

1. Set up defineLive once (in something like sanity/lib/live.ts):

import { defineLive } from "next-sanity/live"
import { client } from "./client"

export const { sanityFetch, SanityLive } = defineLive({
  client,
  serverToken: process.env.SANITY_API_READ_TOKEN,
  browserToken: process.env.NEXT_PUBLIC_SANITY_API_READ_TOKEN,
})

2. Use the same component everywhere - just swap client.fetch for sanityFetch:

// app/blog/[slug]/page.tsx
import { sanityFetch } from "@/sanity/lib/live"
import { BlogPost } from "@/components/BlogPost"

export default async function BlogPostPage({ params }) {
  const { slug } = await params
  
  const { data: post } = await sanityFetch({
    query: `*[_type == "post" && slug.current == $slug][0]`,
    params: { slug }
  })
  
  return <BlogPost post={post} />
}

3. Add the <SanityLive /> component once in your root layout:

// app/layout.tsx
import { SanityLive } from "@/sanity/lib/live"
import { draftMode } from "next/headers"

export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        {(await draftMode()).isEnabled && <SanityLive />}
      </body>
    </html>
  )
}

That's it! No component duplication, no dynamic loading, no weird wrapper logic. The same BlogPost component works for both live preview and production. When draft mode is enabled, <SanityLive /> automatically makes all sanityFetch calls reactive to content changes.

The Live Content API handles the real-time updates without websockets or complex cache invalidation. Your components stay simple, and you avoid those gnarly React boundary errors entirely.

If you're on an older version of next-sanity (pre-v11), note that the import path changed from next-sanity to next-sanity/live - check the changelog if you run into import issues.

👋 We probably can’t help you much without seeing your code.
Yeah, I know but unfortunately I can't share it cause it's for a client and I signed an nda... 😅
But I managed to solve my issues by just using the
LiveQuery
component provided by
next-sanity
.
Thanks for the reply!
Oh great! I’m glad you found a solution.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?