<script>

Loads an external or inline script, hoisting and de-duplicating it by src when marked async.

Signature
<script async src="https://example.com/script.js" />
  • Added in React 19.0.0

Parameters

ParameterTypeDescription
$childrenoptionalstringThe source code of an inline script. Provide either this or src, not both.
$srcoptionalstringThe URL of an external script. Provide either this or children.
$asyncoptionalbooleanAllows the browser to defer execution until the rest of the document has been processed. Required, together with src, for React's hoisting and de-duplication.
$crossOriginoptionalstringThe CORS policy to use: "anonymous" or "use-credentials".
$fetchPriorityoptionalstring= "auto"Ranks scripts against each other when several are fetched at once: "high", "low" or "auto" (the default).
$integrityoptionalstringA cryptographic hash of the script, used to verify its authenticity.
$noModuleoptionalbooleanDisables the script in browsers that support ES modules, allowing a fallback for those that do not.
$nonceoptionalstringA cryptographic nonce that allows the resource under a strict Content Security Policy.
$referreroptionalstringSays what Referer header to send when fetching the script and anything the script fetches in turn.
$typeoptionalstringWhether the script is a classic script, an ES module or an import map.
$onLoadoptionalfunctionCalled when the script finishes loading. Supplying it disables React's special treatment of scripts.
$onErroroptionalfunctionCalled when the script fails to load. Supplying it disables React's special treatment of scripts.
$blockingoptionalstringNot recommended with React. Set to "render", it tells the browser not to render the page until the script loads; Suspense gives finer control.
$deferoptionalstringNot recommended with React. Prevents execution until the document has finished loading, and is not compatible with streaming server-rendered components. Use async instead.

Return value

Renders nothing visible. With src and async={true}, React inserts a single deduplicated <script> node into the document <head>; otherwise the node renders in place.

Changelog

  • PHP 19.0.0A <script> with src and async={true} is hoisted into the document <head> and de-duplicated by src. Inline scripts and non-async scripts are still rendered in place.

Rendering a <script> lets the component that needs a third-party library declare it, instead of hard-coding the tag in index.html and hoping the component that uses it is mounted. Give it src and async={true} and React hoists the node into <head> and de-duplicates by src, so five checkout widgets requesting the same SDK produce one tag and one execution.

async is not optional for hoisting

Both conditions must hold. Without src, or with async omitted or false, React renders the tag exactly where it sits in the tree and does no de-duplication at all. The requirement exists because moving a synchronous script would change when it runs relative to the document; only an async script can be relocated safely.

What trips people up

Inline scripts — the analytics snippet you pasted in — are never hoisted and never de-duplicated, so mounting the component twice runs the code twice. Props are frozen after the first render: changing src on a mounted script does nothing but warn. React may also leave the node in the DOM after unmount, which is harmless for the tag but does not undo what the script did, so anything that registers globals or timers still needs its own teardown.

Example

Example · jsx
function MapWidget({ id, center }) {
  return (
    <div>
      {/* src + async: hoisted into <head> and de-duplicated by src,
          so both widgets below share one tag and one execution. */}
      <script async src="https://maps.example.com/sdk.js" />
      <div id={id} data-center={center} style={{ height: 180, background: "#e2e8f0" }}>
        Map: {center}
      </div>
    </div>
  );
}

function StructuredData({ article }) {
  const json = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: article.headline,
    datePublished: article.datePublished
  };

  // Inline script: rendered in place, never hoisted or de-duplicated.
  return (
    <script type="application/ld+json">
      {JSON.stringify(json)}
    </script>
  );
}

export default function LocationsPage() {
  const article = { headline: "Our offices", datePublished: "2024-11-02" };

  return (
    <main>
      <h1>Our offices</h1>
      <StructuredData article={article} />
      <MapWidget id="map-lisbon" center="38.72,-9.14" />
      <MapWidget id="map-berlin" center="52.52,13.40" />
    </main>
  );
}

When to use it

  • Loading a payment or maps SDK from the component that renders the widget, once no matter how many render
  • Emitting a JSON-LD structured-data block for the current page
  • Declaring an import map before module scripts run
  • Shipping a small inline bootstrap that must run before hydration

Frequently asked questions

Why is my script tag not moved into the head?

Hoisting requires both a src prop and async={true}. An inline script (one with children), a script without async, or any script carrying onLoad or onError is rendered in place in the tree instead. React only relocates async external scripts because moving anything else would change when it executes relative to the surrounding document.

How do I load a third-party script once in React?

Render <script async src="..." /> from the component that needs it. React de-duplicates by src, so however many instances mount, one tag is inserted and the code runs once. Avoid adding onLoad, which disables de-duplication; if you need a readiness signal, mount the script in a single owner component and publish its state through context.

Does React remove the script when the component unmounts?

Not necessarily — React may leave the node in the DOM after unmount. For the tag itself this is harmless, since a script only executes when inserted. But it does not undo the script's side effects: globals, event listeners, timers and injected DOM all survive, so anything that needs cleaning up must be torn down in your own effect.

More examples

Knowing when the SDK is ready

The button stays disabled until the SDK reports ready, and shows a fallback message if the fetch fails. The trade-off is stated in the comment: supplying onLoad or onError tells React you are managing this resource, so the tag is neither moved to <head> nor de-duplicated by src. Render two PaymentButtons and you get two script tags. If you need both readiness callbacks and single-loading, render the script once high in the tree and share its state through context.

Example · jsx
import { useState } from "react";

export default function PaymentButton() {
  const [sdk, setSdk] = useState("loading");

  return (
    <div>
      {/* onLoad/onError disable hoisting AND de-duplication: this tag
          stays in place, and a second instance would load it again. */}
      <script
        src="https://pay.example.com/sdk.js"
        onLoad={() => setSdk("ready")}
        onError={() => setSdk("failed")}
      />

      {sdk === "loading" && <p>Preparing payment...</p>}
      {sdk === "failed" && <p>Payment is unavailable right now.</p>}
      <button type="button" disabled={sdk !== "ready"}>
        Pay now
      </button>
    </div>
  );
}

Import map, which must not be hoisted or deferred

Neither tag is hoisted: import maps and inline module scripts have no src and cannot be async, so React leaves them exactly where they appear in the tree, which is precisely what the ordering rules require. This also shows the children-versus-src rule — a script takes one or the other, never both — and why inline scripts carry no de-duplication guarantee: render ModuleHost twice and the browser evaluates the module script twice.

Example · jsx
export default function ModuleHost() {
  const imports = {
    imports: {
      "chart": "https://cdn.example.com/chart@4/index.js"
    }
  };

  return (
    <>
      {/* An import map must appear before any module script and cannot
          be async, so it is rendered in place rather than hoisted. */}
      <script type="importmap">{JSON.stringify(imports)}</script>

      <script type="module">{`
        import { render } from "chart";
        render(document.querySelector("#chart"), [3, 1, 4, 1, 5]);
      `}</script>

      <div id="chart" />
    </>
  );
}

Discussion

  • Be the first to comment on this lesson.