Skip to content

Repository files navigation

add-javascript 📜

MIT licensenpm bundle sizeVersion

Just add JavaScript.

Includes validation, integrity checking, caching, and a cross-framework hook.

addScript()

import{addScript}from"add-javascript";addScript("https://www.example.com/script.js",options);// "added" | "already-added" | "already-added (unmanaged)" | "skipped"//// "unmanaged" - means the script was not added by `add-javascript`//// "skipped" - means `skipLoading()` returned true. Callbacks will// still have run, so this does not imply a no-op.//

options (defaults shown, see generated index.d.ts for all, loadBehavior visualised here):

// options:{isModule: false,loadBehavior: "async",fetchPriority: "auto",noopIfModulesSupported: false,ignoreQueryString: true,security: {crossOrigin: "",nonce: "",integrity: "",referrerPolicy: "",warnOnCrossOrigin: true// warn on absolute cross-origin without integrity},dataSet: {},skipLoading: ()=>false,onLoad: detail=>{},onError: detail=>{}}
// detail (optional on unmanaged / already-in-DOM paths):{event: Event,removeScriptTag: ()=>void}|undefined

loadScript()

import{loadScript}from"add-javascript";loadScript("https://www.example.com/script.js",{// Same options as addScript(), but// without onLoad and onError since we// can use Promise callbacks instead}).then(successDetail=>{}).catch(failDetail=>{});
// Typed as TLoadScriptPromise:// .then(successDetail => …) // TLoadScriptSuccess// .catch(failDetail => …) // TLoadScriptError (typed on .catch / then's onRejected)// successDetail (TLoadScriptSuccess — narrow on type):// "added" → event + removeScriptTag required// "already-added" | "already-added (unmanaged)" → event fields optional// "skipped" → skipLoading only (not a cache hit); no event fields// failDetail (TLoadScriptError):{type: "error",event: Event,removeScriptTag: ()=>void}// Note: `try { await loadScript(...) } catch (e)` does not type `e` (TypeScript// limitation). Prefer `.catch`, or narrow: `e as TLoadScriptError`.
// Multiple scriptsawaitPromise.all([loadScript("https://www.example.com/script-1.js",options),loadScript("https://www.example.com/script-2.js",options)]);// ...or if you have common options:awaitPromise.all(["https://www.example.com/script-1.js","https://www.example.com/script-2.js"].map(src=>loadScript(src,options)));

makeHook()

React / Preact

import{makeHook}from"add-javascript";import{useState,useEffect}from"react";// Make the HookconstuseScriptReact=makeHook({ useState, useEffect });functionReactComponent(props){// Use itconst[state,detail]=useScriptReact("./my-script.js",options);// state == "pending" | "loading" | "loaded" | "error"// detail == undefined until loaded/error, then successDetail | failDetail// (typed as TUseScriptReturn — narrow on `state`)return<div>{state}</div>;}

Mithril

import{makeHook}from"add-javascript";import{withHooks,useState,useEffect}from"mithril-hooks";// Make the HookconstuseScriptMithril=makeHook({ useState, useEffect });constMithrilComponent=withHooks(()=>{// Use itconst[state,detail]=useScriptMithril("./my-script.js",options);// state == "pending" | "loading" | "loaded" | "error"// detail == undefined until loaded/error, then successDetail | failDetailreturnm("div",state);});

SolidJS

import{makeHook}from"add-javascript";import{createSignal,createEffect}from"solid-js";// Make the HookconstuseScriptSolidJS=makeHook({useState: createSignal,useEffect: createEffect});functionSolidJSComponent(){// Use itconst[state,detail]=useScriptSolidJS("./my-script.js",options);// With createSignal as useState, values are often accessors:// state() == "pending" | "loading" | "loaded" | "error"// detail() == undefined until loaded/error, then success | fail detailreturnhtml`${state()}`;}

enabled vs skipLoading

enabled: false (hook option)skipLoading: () => true (loader option)
Calls loadScript?NoYes
State when “off”pending, detail undefinedloaded + { type: "skipped" }
Flip off → onLoads when enabled becomes trueOnly if effect re-runs (deps include src / enabled, not arbitrary options)
const[state,detail]=useScript(url,{enabled: isReady});// enabled defaults to true

React / Preact / mithril-hooks: effect deps are [src, enabled]. Solid’s createEffect ignores the deps array and tracks signals read inside the effect — pass reactive sources accordingly.

You can use all of these frameworks on the same page (if you like.) Check out the tests for a working implementation of this.

Create useScript in its own file for your convenience:

// useScript.jsimport{makeHook}from"add-javascript";import{useState,useEffect}from"react";exportconstuseScript=makeHook({ useState, useEffect });
// ReactComponent.jsimport{useScript}from"./useScript";functionReactComponent(props){const[state]=useScript("./my-script.js");return<div>{state}</div>;}

Install / Use

$ pnpm i add-javascript

Supports import/require for ESM/CJS.

Browser/UMD version here:

<scriptsrc="https://unpkg.com/add-javascript/dist/browser/add-javascript.browser.js"></script><script>const{ loadScript }=addJs;</script>

But why?

"There are loooads of loadScript/useScript libraries. Why make another one?"

Good question. Perhaps I didn't really need my own library, but I once found myself battling a bug caused by Hot Module Reloading vs. a 3rd-party script and I just ended up writing one to solve the problem. This is the result.

As for the bug, the size of the codebase I was working on at the time make the problem difficult to debug. Long story short, the 3rd-party script was being loaded multiple times and was also not idempotent.

There was also a chance it could be loaded in a vanilla way or via a Hook, hence the desire to offer an API that would consolidate the script-adding logic. This also explains the options default of ignoreQueryString: true, which considers a script loaded regardless of its (in my case: cache-busting) query-string parameters.

The options format is also something of an itch I wanted to scratch.

All that is behind me now, but perhaps someone else will find the outcome of these efforts useful.

Still, if your codebase is under your full control you're likely far better just rolling your own little helper than installing add-javascript:

constloadScript=(src,options={async: true})=>newPromise((resolve,reject)=>{constscript=document.createElement("script");Object.assign(script,options);script.onload=resolve;script.onerror=reject;script.src=src;document.body.appendChild(script);});

Credits

add-javascript was written by Conan Theobald.

I hope you found it useful! If so, I like coffee ☕️ :)

License

MIT licensed: See LICENSE

About

Just add JavaScript. Includes validation, integrity checking, caching, and a cross-framework hook.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages