Auto-reload Studio when changes are deployed

By Corey Ward & Espen Hovlandsdal

Drop this into your Studio to let editors know when there's a more recent version of your Studio available, making sure they have the latest fields and validations.

components/Layout.tsx

import {useState, useEffect, useRef} from 'react'
import {type LayoutProps, isDev} from 'sanity'
import {Flex, Text, Button, Dialog, Box} from '@sanity/ui'

function reload() {
  window.location.reload()
}

const CHECK_INTERVAL = 30000

function getBundleHash(html?: string): string | null {
  const scripts = html
    ? new DOMParser().parseFromString(html, 'text/html').querySelectorAll('script[src]')
    : document.querySelectorAll('script[src]')

  for (const script of scripts) {
    const src = script.getAttribute('src')
    if (src?.includes('sanity-') && src.endsWith('.js')) {
      const match = src.match(/sanity-([a-zA-Z0-9_-]+)\.js/)
      if (match?.[1]) {
        return match[1]
      }
    }
  }

  return null
}

export const Layout = (props: LayoutProps) => {
  const [showDialog, setShowDialog] = useState(false)
  const initialHashRef = useRef<string | null>(null)
  const checkIntervalRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    if (isDev) {
      return
    }

    // Extract initial bundle hash on mount
    const initialHash = getBundleHash()
    initialHashRef.current = initialHash

    // Set up interval to check for new version every 30 seconds
    checkIntervalRef.current = setInterval(async () => {
      try {
        const response = await fetch(window.location.origin + window.location.pathname)
        const html = await response.text()
        const newHash = getBundleHash(html)

        if (newHash && initialHashRef.current && newHash !== initialHashRef.current) {
          setShowDialog(true)
        }
      } catch (error) {
        // Silently handle errors (network failures, etc.)
        console.error('Failed to check for new version:', error)
      }
    }, CHECK_INTERVAL)

    return () => {
      if (checkIntervalRef.current) {
        clearInterval(checkIntervalRef.current)
      }
    }
  }, [])

  return (
    <>
      {showDialog && (
        <Dialog
          header="New version available"
          id="dialog-example"
          animate
          onClose={() => {}}
          footer={
            <Flex width="full" gap={3} justify="flex-start" padding={4} align="center">
              <Button
                mode="default"
                padding={2}
                text="Reload"
                tone="primary"
                data-testid="confirm-button"
                onClick={reload}
              />
            </Flex>
          }
          zOffset={1000}
        >
          <Box padding={4}>
            <Text>A new version of the Studio is available. Please reload to update.</Text>
          </Box>
        </Dialog>
      )}
      {props.renderDefault({...props})}
    </>
  )
}

sanity.config.ts

import {Layout} from './components/Layout'

export default defineConfig({
  // rest of config
  studio: {
    components: {
      layout: Layout,
    },
  },
})

v2 only - bundleChecker.js

// Studio version 2 only

import { useEffect } from "react"
import config from "config:sanity"

const BUNDLE_CHECK_INTERVAL = 60 * 1000
const CHANGES_AVAILABLE_MESSAGE =
  "New changes are available! For the best results the page will be refreshed to get the latest updates."

async function getCurrentHash() {
  const basePath = (config.project && config.project.basePath) || "/"
  const html = await window.fetch(basePath).then((res) => res.text())
  const [, hash] = html.match(/app\.bundle\.js\?(\w+)/) || []
  return hash
}

let hash = null
let interval = null

const BundleChecker = () => {
  useEffect(() => {
    getCurrentHash().then((newHash) => {
      hash = newHash
    })

    interval = createInterval()

    return () => clearInterval(interval)
  }, [])

  // We're a react component, in theory, so return null to not render anything
  return null
}

export default BundleChecker

const createInterval = () =>
  setInterval(async () => {
    const newHash = await getCurrentHash()

    if (hash && newHash !== hash) {
      clearInterval(interval)

      if (window.confirm(CHANGES_AVAILABLE_MESSAGE)) {
        window.location.reload()
      } else {
        interval = createInterval()
      }
    }
  }, BUNDLE_CHECK_INTERVAL)

v2 only - sanity.json

// Studio version 2 only

{
  "parts": [
    {
      "implements": "part:@sanity/base/absolutes",
      "path": "./bundleChecker.js"
    },
  ]
}

The Sanity Studio runs as a single-page app, so users don't get the latest version every time they change "pages". Instead, when you deploy changes to the Sanity Studio (e.g. when you run sanity deploy) your editors need to refresh their browser or they'll be working on an outdated version that might include different fields, validations, or types, creating inconsistent data that can lead to bugs.

Sanity Studio >=3

Add the the components/Layout.tsx file to your code repository, and configure your sanity.config.tsx to import this component and use it as your Studio layout component. This will check every 30 seconds (customize by changing the CHECK_INTERVAL variable) and show a dialog if a new Studio version is available.

Sanity Studio v2

Drop the contents of bundleChecker.js into your Studio and configure it to your sanity.json under the parts array and your Studio will now make periodic checks (set to once every 60 seconds by default) to see if there are any changes to your Studio code available. If so, it'll prompt the user to refresh.

Contributors

Corey Ward

Freelance full-stack dev focused on building awesome Jamstack experiences

Corey is located at Austin, Texas, US
Visit Corey Ward's profile

Other recipes by the contributors