UNPKG

@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
# 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.