Hone logo
Hone
Problems

Dynamic Script Loading with useScript Hook

In modern React applications, it's often necessary to dynamically load external JavaScript resources, such as third-party analytics scripts, advertisement tags, or libraries that are only needed on specific pages or under certain conditions. This challenge asks you to implement a custom React hook, useScript, that handles the loading and management of external script tags. This hook should provide a clear interface for tracking the script's loading status.

Problem Description

Your task is to create a custom React hook named useScript in TypeScript. This hook will accept a URL to a JavaScript file and manage the lifecycle of a <script> tag associated with that URL.

Key Requirements:

  • Loading: The hook should append a <script> element to the document's <head> when the component using the hook mounts.
  • Status Tracking: The hook must return the current loading status of the script. This status should be one of the following:
    • 'idle': The script has not started loading yet.
    • 'loading': The script is currently being fetched.
    • 'ready': The script has successfully loaded.
    • 'error': An error occurred while loading the script.
  • Error Handling: The hook should correctly detect and report script loading errors.
  • Unmounting: When the component using the hook unmounts, the hook should remove the associated <script> tag from the document to prevent memory leaks and unintended side effects.
  • Idempotency: If the hook is called multiple times with the same script URL within the application, it should ideally reuse the existing script tag and its loading status, rather than creating duplicate tags.

Expected Behavior:

The useScript hook should return an object containing the current status of the script. When the script is successfully loaded, the status should transition from 'loading' to 'ready'. If an error occurs, it should transition to 'error'.

Edge Cases to Consider:

  • Script already loaded: If a script with the same URL is already present in the DOM and its loading status is known, the hook should reflect that status immediately.
  • Multiple calls with the same URL: Ensure that adding the same script multiple times doesn't lead to duplicates or race conditions.
  • Network failures: Handle scenarios where the script URL is invalid or unreachable.

Examples

Example 1: Loading a simple script.

Input:

function MyComponent() {
  const { status } = useScript('https://example.com/my-script.js');

  return (
    <div>
      <p>Script Status: {status}</p>
    </div>
  );
}

Output (during loading):

Script Status: loading

Output (after successful loading):

Script Status: ready

Explanation: Initially, the status is 'loading'. Once my-script.js is successfully downloaded and executed by the browser, the status updates to 'ready'.

Example 2: Handling a script error.

Input:

function ErrorProneComponent() {
  const { status } = useScript('https://example.com/non-existent-script.js');

  return (
    <div>
      <p>Script Status: {status}</p>
    </div>
  );
}

Output (during loading):

Script Status: loading

Output (after error):

Script Status: error

Explanation: If non-existent-script.js cannot be fetched by the browser (e.g., 404 error), the status will eventually change to 'error'.

Example 3: Script already loaded.

Scenario: Imagine a script https://example.com/shared-script.js has already been loaded by another component using useScript.

Input:

function AnotherComponent() {
  const { status } = useScript('https://example.com/shared-script.js');

  return (
    <div>
      <p>Script Status: {status}</p>
    </div>
  );
}

Output (assuming it's already ready):

Script Status: ready

Explanation: The useScript hook should detect that https://example.com/shared-script.js is already in the DOM and is marked as loaded (or is currently loading if it was initiated by another hook instance but not yet ready). It should then return the appropriate status without re-requesting the script.

Constraints

  • The useScript hook must be implemented in TypeScript.
  • The hook should accept a single argument: url (string).
  • The hook must return an object with a single property: status (string, one of 'idle', 'loading', 'ready', 'error').
  • The script should be added to the <head> of the document.
  • Ensure proper cleanup by removing the script tag on component unmount.

Notes

  • Consider using React's useState, useEffect, and potentially useRef to manage the script's state and DOM manipulation.
  • You'll need to attach event listeners (onload and onerror) to the script element to track its status.
  • A simple approach to handle idempotency is to maintain a global or module-level cache of script statuses and DOM elements.
  • The 'idle' state can be the initial state before any attempt to load the script is made, or it could be represented by the absence of a script tag if no component is actively requesting it. For this challenge, assume 'idle' is the state before the first useEffect runs to append the script.
Loading editor...
typescript