diff --git a/src/__tests__/useScript.test.tsx b/src/__tests__/useScript.test.tsx index 6cc1733..839baf2 100644 --- a/src/__tests__/useScript.test.tsx +++ b/src/__tests__/useScript.test.tsx @@ -66,3 +66,26 @@ test("idle until url", async () => { await vi.waitUntil(() => result.current === "ready"); }); + +test("should be able to add custom attributes to the script", async () => { + const { rerender } = renderHook(() => + useScript("/test-script.js", { + attributes: { + id: "test-id", + "data-test": "true", + nonce: "test-nonce", + }, + }), + ); + + const script = document.querySelector("script[src='/test-script.js']"); + if (script) { + expect(script).toHaveAttribute("id", "test-id"); + expect(script).toHaveAttribute("data-test", "true"); + expect(script).toHaveAttribute("nonce", "test-nonce"); + } + + rerender(); + rerender(); + rerender(); +}); diff --git a/src/hooks/useScript.ts b/src/hooks/useScript.ts index a6fe334..d21be08 100644 --- a/src/hooks/useScript.ts +++ b/src/hooks/useScript.ts @@ -2,13 +2,22 @@ import { useEffect, useState } from "react"; type ScriptStatus = "idle" | "loading" | "ready" | "error"; +type ScriptOptions = { + attributes?: Record; +}; + /** * Hook to load an external script. Returns true once the script has finished loading. * * @param url {string} The external script to load + * @param options {ScriptOptions} The options for the script + * @param options.attributes {HTMLScriptElement["attributes"]} Extra attributes to add to the script element * @returns {ScriptStatus} The status of the script * */ -export function useScript(url?: string): ScriptStatus { +export function useScript( + url: string | undefined, + options?: ScriptOptions, +): ScriptStatus { const [status, setStatus] = useState(() => { if (!url) return "idle"; if (typeof window === "undefined") return "loading"; @@ -20,6 +29,9 @@ export function useScript(url?: string): ScriptStatus { return (script?.getAttribute("data-status") as ScriptStatus) ?? "loading"; }); + const attributes = options?.attributes; + + // biome-ignore lint/correctness/useExhaustiveDependencies: We convert the attributes object to a string to see if it has changed, so it can't be detected by the rule useEffect(() => { if (!url) { setStatus("idle"); @@ -34,6 +46,7 @@ export function useScript(url?: string): ScriptStatus { script.src = url; script.async = true; script.setAttribute("data-status", "loading"); + document.body.appendChild(script); // Ensure the status is loading @@ -42,6 +55,14 @@ export function useScript(url?: string): ScriptStatus { setStatus(script.getAttribute("data-status") as ScriptStatus); } + if (attributes) { + // Add extra attributes to the script element + // If for some reason you have conflicting attributes, the last hook to execute will win + Object.entries(attributes).forEach(([key, value]) => { + script.setAttribute(key, value); + }); + } + const eventHandler = (e: Event) => { const status: ScriptStatus = e.type === "load" ? "ready" : "error"; script.setAttribute("data-status", status); @@ -56,7 +77,7 @@ export function useScript(url?: string): ScriptStatus { script.removeEventListener("load", eventHandler); script.removeEventListener("error", eventHandler); }; - }, [url]); + }, [url, attributes ? JSON.stringify(attributes) : undefined]); return status; }