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.
Repository Health
Technical Analysis
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
asyncScriptOnLoadcallback prop delivered to the wrapped component once the script has loaded (or errored), plus an optional named global exposed as a prop viaglobalName removeOnUnmountsupport to strip the injected<script>tag when the last consuming instance unmounts, andscriptId/attributesoptions for scripts needing custom tag metadata- Ref forwarding and static-method hoisting (via
React.forwardRefandhoist-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.grecaptchais available (the library’s own documented example, used by the relatedreact-google-recaptchapackage) - Loading the Google Maps JavaScript API asynchronously and exposing the resulting
window.googleglobal to a map component as a prop - Integrating a third-party payment or analytics widget that attaches itself to
windowand 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.