React useScript Hook

动态加载外部脚本。

  • 使用 useState() hook 创建一个用于脚本加载状态的状态变量。
  • 使用 useEffect() hook 处理加载和卸载脚本的所有逻辑,任何时候 src 发生变化都会触发。
  • 如果没有 src 值,将 status 设置为 'idle' 并返回。
  • 使用 Document.querySelector() 检查是否存在具有相应 src 值的 <script> 元素。
  • 如果不存在相关元素,使用 Document.createElement() 创建一个并赋予适当的属性。
  • 使用 data-status 属性来指示脚本的状态,初始设置为 'loading'
  • 如果存在相关元素,则跳过初始化并从其 data-status 属性更新 status。这样可以确保不会创建重复的元素。
  • 使用 EventTarget.addEventListener() 监听 'load''error' 事件,并通过更新 data-status 属性和 status 状态变量来处理它们。
  • 最后,当组件卸载时,使用 Document.removeEventListener() 移除绑定到元素的任何监听器。
const useScript = src => {
  const [status, setStatus] = React.useState(src ? 'loading' : 'idle');

  React.useEffect(() => {
    if (!src) {
      setStatus('idle');
      return;
    }

    let script = document.querySelector(`script[src="${src}"]`);

    if (!script) {
      script = document.createElement('script');
      script.src = src;
      script.async = true;
      script.setAttribute('data-status', 'loading');
      document.body.appendChild(script);

      const setDataStatus = event => {
        script.setAttribute(
          'data-status',
          event.type === 'load' ? 'ready' : 'error'
        );
      };
      script.addEventListener('load', setDataStatus);
      script.addEventListener('error', setDataStatus);
    } else {
      setStatus(script.getAttribute('data-status'));
    }

    const setStateStatus = event => {
      setStatus(event.type === 'load' ? 'ready' : 'error');
    };

    script.addEventListener('load', setStateStatus);
    script.addEventListener('error', setStateStatus);

```javascript
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

const useScript = (src) => {
  const [status, setStatus] = useState('loading');

  useEffect(() => {
    const script = document.createElement('script');
    script.src = src;
    script.async = true;

    const setStateStatus = (event) => {
      setStatus(event.type === 'load' ? 'ready' : 'error');
    };

    script.addEventListener('load', setStateStatus);
    script.addEventListener('error', setStateStatus);

    document.body.appendChild(script);

    return () => {
      if (script) {
        script.removeEventListener('load', setStateStatus);
        script.removeEventListener('error', setStateStatus);
      }
    };
  }, [src]);

  return status;
};

const script =
  'data:text/plain;charset=utf-8;base64,KGZ1bmN0aW9uKCl7IGNvbnNvbGUubG9nKCdIZWxsbycpIH0pKCk7';

const Child = () => {
  const status = useScript(script);
  return <p>子组件状态: {status}</p>;
};

const MyApp = () => {
  const status = useScript(script);
  return (
    <>
      <p>父组件状态: {status}</p>
      <Child />
    </>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <MyApp />
);