Skip to content
usefy

useScript

Browser & device

Load an external script with idle/loading/ready/error status, tag deduplication, and ref-counted cleanup.

Install

$ npm install @usefy/use-script

Quick start

useScript.tsx
import { useScript } from "@usefy/use-script";

function StripeCheckout() {
  const status = useScript("https://js.stripe.com/v3", {
    attributes: { id: "stripe-js" },
  });

  if (status === "loading") return <span>Loading payment form…</span>;
  if (status === "error") return <span>Failed to load Stripe.</span>;
  if (status !== "ready") return null;

  return <PaymentForm stripe={window.Stripe(PUBLISHABLE_KEY)} />;
}

API reference

const status = useScript(src, options?);

Parameters

ParameterTypeDescription
srcstring | null | undefinedThe script URL. null/undefined keeps the hook idle and injects nothing — the idiomatic way to load conditionally.
optionsUseScriptOptionsOptional. See below.

UseScriptOptions

OptionTypeDefaultDescription
removeOnUnmountbooleanfalseRemove the injected <script> on unmount, only if no other mounted useScript for the same src remains (ref-counted). Removing a script does not un-run its side effects.
shouldPreventLoadbooleanfalseWhen true, the hook stays idle and loads nothing regardless of src — gate a load behind consent or a feature flag.
attributesRecord<string, string>Extra attributes for the created tag (id, data-*, crossorigin, async, defer, …). Applied only on creation; an adopted pre-existing tag is left untouched. Defaults to async unless async/defer is supplied here.

Returns

ScriptStatus — one of:

ValueMeaning
"idle"Nothing is loading (src is null/undefined, shouldPreventLoad is set, or on the server).
"loading"The <script> has been injected and is downloading/executing.
"ready"The script fired load (or an already-loaded tag was adopted).
"error"The script fired error (network failure, blocked, …).

Deduplication & shared status

All state lives in a module-level registry keyed by src. The first useScript(src) to mount creates one <script> (or adopts a matching tag already in the DOM) and sets a data-status attribute; every later useScript(src) shares that single tag and its status. When the script resolves, the registry flips the shared status and notifies all subscribers, so every component re-renders together.

// Two components, one <script> tag — both observe the same status.
function WidgetA() {
  const status = useScript("https://cdn.example.com/widget.js");
  return <div>A: {status}</div>;
}

function WidgetB() {
  const status = useScript("https://cdn.example.com/widget.js");
  return <div>B: {status}</div>;
}

Adopting an existing tag

If a matching <script> is already in the DOM, useScript adopts it instead of re-injecting. When it carries a data-status attribute, that status is trusted; a tag without data-status (e.g. server-rendered or added by another library) is treated as ready, because a load listener attached after the fact would never fire for a script that has already executed.

getScriptStatus(src)

Read the current shared status for a src imperatively (outside React), once some component has begun loading it:

import { getScriptStatus } from "@usefy/use-script";

if (getScriptStatus("https://cdn.example.com/sdk.js") === "ready") {
  window.SDK.init();
}

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in browser & device