Search code examples
javascriptreactjswebpack

es6 module loaded through url cannot access "useState" as it is undefined


I am building a React library using webpack, and the library builds correctly into an es6 module. We are then trying to host this module on a CDN so we can import it through a url instead of node_modules.

We then import it like this, and I get the correct component.

import('http://example.com/test.js').then(m => console.log(m.default));

The library has this test component which works when I display it within the application:

This is exported through the library:

export default ({ name }) => {
  return <div>{name}</div>;
};

The app then implements it like this:

export default () => {
  const [Test, setTest] = useState();

  useEffect(() => {
    import('http://example.com/test.js').then(m => setTest(m.default));
  }, []);

  return <Test name="Joe" />;
}

However, when I add useState, like this:

import { useState } from 'react';
export default ({ name }) => {
  const [displayName] = useState('Billy');
  return <div>{displayName}</div>;
};

I then get these two errors:

test.tsx:12 Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
react.development.js:1623 Uncaught TypeError: Cannot read properties of null (reading 'useState')
    at useState (react.development.js:1623:21)
    at default (test.tsx:12:76)

my webpack looks like this:

export default {
  // module rules and entry snipped out

  output: {
    path: path.resolve(__dirname, '../../dist'),
    filename: 'test.js',
    library: {
      type: 'module',
    },
  },
  experiments: {
    outputModule: true,
  }
}

When I add externals: ['react', 'react-dom'] to webpack, I then get this error:

Uncaught (in promise) TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".

Why isn't my library able to use useState when loaded via a URL?


Solution

  • So, to get this to work, I needed to create a child app within the current application, so from the component that comes from a url sorce I also exported the react and react-dom so the parent application could create it:

    // index.jsx
    import React, { useState } from 'react';
    import ReactDOM from 'react-dom/client';
    
    const _Component = ({name}) => {
      const [name] = useState(name ?? 'Billy');
      return <div>{name}</div>;
    }
    
    // I converted this into a reusable function:
    const div = document.createElement('div');
    const root = reactDOM.createRoot(div);
    
    const info = {
      component,
      react,
      renderer: root,
      domElement: div,
    };
    
    export info as RemoteComponent;
    

    Next, in the application I created a component that would bootstrap the data and display the internal application:

    // widget.jsx
    export const Widget = ({ widget, props }) => {
      const [uuid] = useState(() => crypto.randomUUID());
      const ref = useRef(null);
    
      // If the widget changes, we need to re-render the new widget and append it to the DOM
      useEffect(() => {
        if (!widget || !ref.current) return;
        widget.renderer.render(widget.react.createElement(widget.component, props));
        ref.current.appendChild(widget.domElement);
      }, [widget]);
    
      // If the props change, we need to re-render the widget but not append it to the DOM
      useEffect(() => {
        if (!widget) return;
        widget.renderer.render(widget.react.createElement(widget.component, props));
      }, [props]);
    
      return <div ref={ref} id={uuid}></div>;
    };
    

    Lastly, I load the component from the remote and and pass it to the Widget. The widget then creates an application as a child of the current application:

    // app.jsx
    export default function App() {
      const [module, setModule] = useState({});
    
      useEffect(() => {
        const endpoint = 'http://example.com/library.js';
        import(endpoint).then(setModule);
        // This contains:
        // {
        //   RemoteComponent: {domElement, renderer, react, component}
        // }
      }, []);
    
      return (
        <>
          External Widget:
          <Widget widget={module} props={{name: 'Joe'}} />
        </>
      );
    }