How to render a python panel component from react with pyodide?

Question:

I am trying to use an example from the panel documentation of how to display a panel component from python using pyodide, but from a react component, instead of from pure html.

I have set up a minimal NextJS react app which can be cloned, and ran locally simply with npm i && npm start. My example works for simple python code returning a string or number, but when I attempt to use it with the example panel code for a simple slider I am unsure what to return in order for react to render the slider.

The python code is contained in src/App.js. I am simply overwriting the myPythonCodeString variable from the panel code to a simple 1+9 arithmetic to demonstrate it works in that simple case.

Any help would be much appreciated.

Edit: I have added commits to this repo fixing the problem, the state of the repo when this question was asked can be seen in commit 3c735653dda0e873f17a98d0fb74edaca367ca00.

Answers:

Your code is actually correct, and produces the widget. The only problem is that Helmet does not load the scripts in sync, as <head> does. So, your scripts will end up being loaded at the same time, and because they depend on each other, the loading will fail.

There is a simple way to get the correct output:

  1. Add the element with id="simple_app" to your application
root.render(
  <StrictMode>
    <App />
    <div id="simple_app">Replace this</div>
  </StrictMode>
);
  1. Comment every script in Helmet, apart from pyodide and bokeh
  2. Start the app with npm run start
  3. Uncomment the next script and save the file, making the application reload its state
  4. Wait for the app to reload in the browser, and repeat with the next script

At the end, all the scripts will have loaded in the correct manner and you will be left with the widget working.

The simplest solution is to make Helmet load the scripts in sync, or load the scripts using another method.

Answered By: TachyonicBytes

Thanks to @TachyonicBytes for the assistance in solving this. As they said, 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/[email protected]/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>
  );
}

The result being:

A web page displaying a title and a slider to select an amplitude

Answered By: SomeRandomPhysicist