React Async Script

A React higher-order component that loads third-party scripts asynchronously, dedupes them across component instances, and forwards refs to the wrapped component.

Library
npm
v1.2.0
177stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
41/100Fair
Development Activity0
Maintenance20
Community64
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
60/100Good
Architecture78
Code Quality68
Innovation40
Learning Curve55

React Async Script wraps any component in a higher-order component (HOC) that injects a <script> tag for a third-party resource — reCAPTCHA, Google Maps, a payment widget, or any other externally-hosted script — and only renders the wrapped component’s dependent behavior once that script has finished loading. It solves a problem that comes up constantly when integrating third-party JS into a React app: knowing when the script is actually ready, rather than assuming it loaded before your component mounted.

The library keeps a module-level registry of in-flight and completed script loads keyed by URL, so if the same script is requested by multiple component instances (for example, two ReCAPTCHA widgets on one page), only one <script> tag is inserted and every subscribed instance is notified via an observer callback once it loads or errors. It supports optional global-callback registration (callbackName) for scripts that call a window-level function on load, exposes the resulting global (globalName) as a prop to the wrapped component, and can remove its script tag on unmount. React.forwardRef plus hoist-non-react-statics ensure refs and static members pass through the wrapper untouched, so consumers can still call imperative methods on the wrapped component as if the HOC weren’t there.

What You Get

  • makeAsyncScriptLoader(getScriptUrl, options) — a HOC factory that wraps any component and manages script injection, load-state tracking, and cleanup for it
  • A cross-instance script registry that dedupes concurrent requests for the same script URL, so multiple mounted components sharing a resource only trigger one network load
  • An asyncScriptOnLoad callback prop delivered to the wrapped component once the script has loaded (or errored), plus an optional named global exposed as a prop via globalName
  • removeOnUnmount support to strip the injected <script> tag when the last consuming instance unmounts, and scriptId/attributes options for scripts needing custom tag metadata
  • Ref forwarding and static-method hoisting (via React.forwardRef and hoist-non-react-statics) so the wrapped component’s public API is unaffected by the wrapper

Common Use Cases

  • Wrapping a reCAPTCHA widget component so it only renders/interacts once window.grecaptcha is available (the library’s own documented example, used by the related react-google-recaptcha package)
  • Loading the Google Maps JavaScript API asynchronously and exposing the resulting window.google global to a map component as a prop
  • Integrating a third-party payment or analytics widget that attaches itself to window and calls a global callback once ready
  • Any React component that depends on an externally hosted script and needs to avoid inserting duplicate <script> tags when rendered multiple times on the same page

Under The Hood

Architecture The entire library is a single file (src/async-script-loader.js) exporting makeAsyncScript(getScriptURL, options), which returns a wrapper function that takes a WrappedComponent and produces an AsyncScriptLoader class component. The design is a flat, single-responsibility higher-order-component pattern: a module-scoped SCRIPT_MAP object keyed by script URL tracks each script’s load/error state and a per-instance map of observer callbacks. componentDidMount either creates the <script> tag (first consumer) or subscribes as an observer if the script is already loading or loaded; componentWillUnmount optionally removes the tag and always cleans up the instance’s observer entry. render forwards all props except its own (asyncScriptOnLoad, forwardedRef) straight through to the wrapped component, with React.forwardRef and hoist-non-react-statics preserving refs and static members across the wrap. Because the dedupe/observer registry lives at module scope rather than per-instance, any consumer relying on shared-script behavior across multiple mounted instances (the library’s core value proposition) would silently break if that abstraction changed.

Tech Stack Plain ES2015+ JavaScript transpiled via Babel (@babel/preset-env, @babel/preset-react) into two output targets — CommonJS in lib/ and ESM in lib/esm/ via a BABEL_ENV=esm flag passed to cross-env — with no bundler involved; the babel CLI compiles the src/ directory directly. Runtime dependencies are minimal: hoist-non-react-statics for the static-hoisting behavior and prop-types for a single runtime-checked prop, with react/react-dom as peer dependencies pinned to >=16.4.1 (the version that introduced forwardRef, which the library depends on internally). Testing runs on Jest with jest-environment-jsdom and @testing-library/react; linting and formatting are handled by ESLint (eslint-plugin-react) and Prettier with eslint-config-prettier wiring them together.

Code Quality A single but substantial Jest spec file (test/async-script-loader-spec.js, 347 lines) exercises real behavioral scenarios: mount/unmount lifecycle, script dedupe across multiple instances sharing one URL, removeOnUnmount tag cleanup, global-callback registration via callbackName, and ref forwarding — this is meaningful coverage, not smoke testing. Error handling is deliberate rather than accidental: asyncScriptLoaderTriggerOnScriptLoaded throws explicitly if invoked with no matching SCRIPT_MAP entry, and the onerror handler marks the map entry errored and notifies observers rather than swallowing the failure. Method names are consistently prefixed (asyncScriptLoader*) to avoid colliding with the wrapped component’s own methods. There is no TypeScript and no static typing beyond a single PropTypes check, and the whole implementation lives in one file, which keeps it easy to audit but limits how much structure there is to evaluate.

API Design The public surface is intentionally tiny: one factory function, makeAsyncScriptLoader(getScriptUrl, options)(Component), that returns a drop-in replacement component. Getting started requires no boilerplate beyond calling the factory and rendering the result with an asyncScriptOnLoad prop; the README documents the full option set (attributes, callbackName, globalName, removeOnUnmount, scriptId) with a complete worked example (wrapping a reCAPTCHA component). The main ergonomic rough edge is implicit: consumers need to know the global-registry dedupe behavior exists to reason about why a script only loads once across instances, since nothing in the API surface signals it directly.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search