@rsc-kit/mcp
Version:
An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.
106 lines (80 loc) • 3.5 kB
Markdown
# Third-party scripts
> Analytics, tag managers and widgets — and why there is no Script component.
Write the tag. React 19 does the rest, and it does the parts Next's `<Script>`
component existed for.
## An external script
```tsx title="src/app/layout.tsx"
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:
```tsx title="src/app/layout.tsx"
<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.
:::note[Put it in the root layout]
The root layout renders once and is kept across navigations, so the snippet
runs once. In a *page*, a client-side navigation to it re-renders the tag, and
whether the browser runs a script inserted that way is not something to rely on
either way. Site-wide scripts go in the layout; that is where they belong
anyway.
:::
## 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:
```tsx title="src/components/AfterHydration.tsx"
'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.