Skip to content

Commit

Permalink
fix(useScript): support attributes on useScript
Browse files Browse the repository at this point in the history
  • Loading branch information
thebuilder committed Jul 29, 2024
1 parent 839c9e4 commit dc258dd
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/__tests__/useScript.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
25 changes: 23 additions & 2 deletions src/hooks/useScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { useEffect, useState } from "react";

type ScriptStatus = "idle" | "loading" | "ready" | "error";

type ScriptOptions = {
attributes?: Record<string, string>;
};

/**
* 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<ScriptStatus>(() => {
if (!url) return "idle";
if (typeof window === "undefined") return "loading";
Expand All @@ -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");
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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;
}

0 comments on commit dc258dd

Please sign in to comment.