Last updated January 22, 2024

Create a reusable live preview component

By Simeon Griggs

When setting up Visual Editing, you may create unique components to perform live queries. However, it might be more convenient in a larger application to reuse a single wrapper component that can perform a query and pass live-updating content to any child component.

If you’re setting up Sanity's interactive live preview and Presentation for the first time, start with one of these guides:

The guides above walk through how to set up these client libraries:

Assumptions:

  • You already have a Sanity Studio with draft and published content
  • You’re working on a React project with Visual Editing already configured

Protip

The code examples below are taken from a Next.js App router project, and some include the ‘use client’ directive. You can safely remove these lines from the code examples for frameworks that do not yet support React Server Components.

How this will work

  1. First, you’ll create a component named LiveQueryWrapper which will take the initial data you initially fetch server-side. Along with the query and parameters that were used for that initial data.
  2. You’ll wrap that component around a single component designed to consume and render that initial data. This wrapped component must expect data to arrive in a prop named data.
  3. Depending on if live updates are enabled:
    1. the wrapper will perform live updates and continually update the child component's data prop
    2. otherwise, the initial data will pass through to the child component

Why this works

Traditionally, you could use “render props” and pass a rendering function to render either published or preview content conditionally. However, passing functions from server to client is not currently possible with React Server components. The technique shown in this guide relies on conditionally loaded components that prop-drill published or preview content to their child component.

Working example

The "Course Platform Demo" is a Sanity Studio and Next.js App router application that uses this method to reuse the LiveQueryWrapper component in every route to make each unique layout automatically updated with live preview.

Note: It is predominately a demonstration of internationalization strategies with Sanity and is not intended as a starter template for new projects.

Creating the preview components

The magic of this approach is handled by @radix-ui/react-slot, which enables components to pass props to their direct children, no matter what they are.

Install it to your front-end project:

npm install @radix-ui/react-slot

Create a new component file for LiveQueryWrapper, which will wrap the component you use to render Sanity content:

// ./components/LiveQueryWrapper.tsx

import {Slot} from '@radix-ui/react-slot'
import {QueryParams} from '@sanity/client'
import {QueryResponseInitial} from '@sanity/react-loader'
import {PropsWithChildren} from 'react'

import {LiveQueryData} from '@/components/LiveQueryData'

type PreviewWrapperProps<T> = PropsWithChildren<{
  initial: QueryResponseInitial<T>
  isEnabled?: boolean
  query?: string
  params?: QueryParams
}>

// Component just renders its children if preview mode is not enabled
export function LiveQueryWrapper<T>(props: PreviewWrapperProps<T>) {
  const {
    // Is live query mode active?
    isEnabled = false,
    // If so, listen to this query
    query = null,
    // With these params
    params = {},
    // Separate remaining props to pass to the child
    ...rest
  } = props

  // Render child, with the wrapper's initial data and props
  if (!isEnabled || !query) {
    const nonPreviewProps = {...rest, data: props.initial.data}

    return <Slot {...nonPreviewProps} />
  }

  // Swap initialData for live data
  return (
    <LiveQueryData<typeof props.initial.data> initial={props.initial} query={query} params={params}>
      {props.children}
    </LiveQueryData>
  )
}

Create the LiveQueryData component file:

// ./components/LiveQueryData.tsx

'use client'

import {Slot} from '@radix-ui/react-slot'
import {QueryParams} from '@sanity/client'
import {QueryResponseInitial, useQuery} from '@sanity/react-loader'
import {PropsWithChildren} from 'react'

type PreviewDataProps<T = any> = PropsWithChildren<{
  initial: QueryResponseInitial<T>
  query: string
  params: QueryParams
}>

// Browser-only preview component
export function LiveQueryData<T = any>(props: PreviewDataProps<T>) {
  const {initial, query, params = {}, ...rest} = props
  const {data} = useQuery<T>(query, params, {initial})

  const previewProps = {...rest, data}

  return <Slot {...previewProps} />
}

Using the preview components

Here is a simplified example of a Next.js route using these preview wrapper components.

Take note that:

  • The React Loader's loadQuery function is checking if draftMode is enabled so that the initial data is either fetched using either the published or previewDrafts perspective. Without this, you may see an initial flash of published content before live preview updates to draft content
  • The same query and params used for the initial fetch are prop-drilled to LiveQueryWrapper and passed to the useQuery() hook
  • The child PresenterLayout component does not have any props, because LiveQueryWrapper will handle passing them down either from initial, or live-updating content as a prop named data
  • Therefore, any child of LiveQueryWrapper should expect to receive a data prop with the published or draft Sanity content
// ./app/presenter/[slug]/page.tsx

import {draftMode} from 'next/headers'

import {LiveQueryWrapper} from '@/components/LiveQueryWrapper'
import type {PresenterLayoutProps} from '@/components/PresenterLayout'
import PresenterLayout from '@/components/PresenterLayout'
import {loadQuery} from '@/sanity/lib/store'
import {PRESENTER_QUERY} from '@/sanity/queries'

export default async function Page({params}) {
  const {isEnabled} = draftMode()
  const initial = await loadQuery<PresenterLayoutProps['data']>(PRESENTER_QUERY, params, {
    perspective: isEnabled ? 'previewDrafts' : 'published',
    next: {tags: ['presenter']},
  })

  return (
    <LiveQueryWrapper
      isEnabled={isEnabled}
      query={isEnabled ? PRESENTER_QUERY : ''}
      params={isEnabled ? params : {}}
      initial={initial}
    >
      <PresenterLayout />
    </LiveQueryWrapper>
  )
}

Here is a truncated example of what the PresenterLayout component might look like, receiving the Sanity Content in the data prop:

type PresenterLayoutProps = {
  data?: {
    name?: string
    title?: string
  }
}

export default function PresenterLayout(props: PresenterLayoutProps) {
  if (!props.data) {
    console.log(`PresenterLayout data empty: ${JSON.stringify(props)}`)
    return null
  }

  const {name, title} = props.data

  return <div>{name}: {title}</div>
}

That's it! You should now be able to use your LiveQueryWrapper component to wrap any component that expects to receive a data prop – and whether you are in preview mode or production, see the expected layout with the right content.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by author

Create richer array item previews

Object types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.

Simeon Griggs
Go to Create richer array item previews