Write the tag. React 19 does the rest, and it does the parts Next’s <Script>
component existed for.
An external script
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:
<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:
'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.