I also posted the question to stackoverflow and got an answer that was helpful in resolving the issue.
There were 2 issues, one was that the scripts needed to be loaded synchronously, one after another in sequence, I did this using the useScript hook from the usehooks-ts library. The other was that I needed to create a div with an id matching that of the servable target in the panel component in the python code.
A github repo with the working app with the corrections in can be viewed here
The component which runs the python code with pyodide looks like so:
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import {useScript} from 'usehooks-ts'
/**
* Pyodide component
*
* @param {object} props - react props
* @param {string} props.pythonCode - python code to run
* @param {string} [props.loadingMessage] - loading message
* @param {string} [props.evaluatingMessage] - evaluating message
* @returns {object} - pyodide node displaying result of python code
*/
function Pyodide({
pythonCode,
loadingMessage = "loading…",
evaluatingMessage = "evaluating…",
}) {
const pyodideStatus = useScript(`https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js`, {
removeOnUnmount: false,
})
const bokehStatus = useScript(`https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.js`, {
removeOnUnmount: false, shouldPreventLoad: pyodideStatus !== "ready"
})
const bokehWidgetsStatus = useScript(`https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js`, {
removeOnUnmount: false, shouldPreventLoad: bokehStatus !== "ready"
})
const bokehTablesStatus = useScript(`https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js`, {
removeOnUnmount: false, shouldPreventLoad: bokehWidgetsStatus !== "ready"
})
const panelStatus = useScript(`https://cdn.jsdelivr.net/npm/@holoviz/panel@0.14.0/dist/panel.min.js`, {
removeOnUnmount: false, shouldPreventLoad: bokehTablesStatus !== "ready"
})
console.log(pyodideStatus, bokehStatus, bokehWidgetsStatus, bokehTablesStatus, panelStatus);
const indexURL = "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/";
const pyodide = useRef(null);
const [isPyodideLoading, setIsPyodideLoading] = useState(true);
const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage); // load pyodide wasm module and initialize it
useEffect(() => {
if (panelStatus === "ready") {
setTimeout(()=>{
(async function () {
pyodide.current = await globalThis.loadPyodide({ indexURL });
setIsPyodideLoading(false);
})();
}, 1000)
}
}, [pyodide, panelStatus]); // evaluate python code with pyodide and set output
useEffect(() => {
if (!isPyodideLoading) {
const evaluatePython = async (pyodide, pythonCode) => {
try {
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install("panel");
return await pyodide.runPython(pythonCode);
} catch (error) {
console.error(error);
return "Error evaluating Python code. See console for details.";
}
};
(async function () {
setPyodideOutput(await evaluatePython(pyodide.current, pythonCode));
})();
}
}, [isPyodideLoading, pyodide, pythonCode]);
if (panelStatus !== "ready") {
return <div></div>
}
return (
<>
<div>
{isPyodideLoading ? loadingMessage : pyodideOutput}
</div>
</>
);
}
Pyodide.propTypes = {
pythonCode: PropTypes.string.isRequired,
loadingMessage: PropTypes.string,
evaluatingMessage: PropTypes.string
};
export default Pyodide;
And example usage looks like:
import Pyodide from "./pyodide";
import "./styles.css";
let myPythonCodeString = `
import panel as pn
pn.extension(sizing_mode="stretch_width")
slider = pn.widgets.FloatSlider(start=0, end=10, name='Amplitude')
def callback(new):
return f'Amplitude is: {new}'
component = pn.Row(slider, pn.bind(callback, slider))
component.servable(target='my_panel_widget');
`;
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Pyodide pythonCode={myPythonCodeString} />
<div id="my_panel_widget"></div>
</div>
);
}