Skip to content

Third-party scripts

Analytics, tag managers and widgets — and why there is no Script component.

Updated View as Markdown

Write the tag. React 19 does the rest, and it does the parts Next’s <Script> component existed for.

An external script

src/app/layout.tsxtsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <script async src="https://www.clarity.ms/tag/abc123" />
      </body>
    </html>
  )
}

Rendered from a server component, React hoists it into <head> and deduplicates it — render the same src in three components and one tag goes out. async means it never blocks the page. That is afterInteractive, without an import.

An inline snippet

Most analytics ship one — a few lines that stub a global and load the real thing:

src/app/layout.tsxtsx
<script
  id="ms-clarity"
  dangerouslySetInnerHTML={{
    __html: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
      t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
      y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
    })(window, document, "clarity", "script", "abc123");`,
  }}
/>

It renders where you wrote it and runs during parse, before hydration. For a snippet whose job is to start recording as early as possible, that is the earlier of the two moments and the one its authors intended. Next’s Script with strategy="afterInteractive" would have made it later.

Porting from Next

Next’s Script strategy here
beforeInteractive <script src> without async — the browser blocks on it, which is what that strategy meant
afterInteractive <script async src>, or the inline snippet above
lazyOnload the effect below, inside requestIdleCallback
worker not supported — that was Partytown, a separate project

The id prop carries across unchanged. onLoad and onReady are the one thing that needs a component, because they need to run in the browser:

When a script has to run after hydration

Rare, and specific: a script that touches DOM React rendered, and would find it missing if it ran during parse. A ten-line client component covers it, and it is yours rather than ours because there is nothing to abstract:

src/components/AfterHydration.tsxtsx
'use client'

import { useEffect } from 'react'

export function AfterHydration({ src, onLoad }: { src: string; onLoad?: () => void }) {
  useEffect(() => {
    const tag = document.createElement('script')

    tag.src = src
    tag.async = true
    if (onLoad) tag.onload = onLoad

    document.head.append(tag)

    return () => tag.remove()
  }, [src, onLoad])

  return null
}

Wrap the body in requestIdleCallback and it is lazyOnload.

Why nothing ships for this

A Script component would be a wrapper around a script tag whose two useful behaviours — hoisting and deduplication — React already provides. The package’s own tests pin that React does, so the day it stops, this page is what changes rather than your app.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close