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?
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'}} />
</>
);
}