Search code examples
pythonreactjsholovizpyodideholoviz-panel

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


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.


Solution

  • 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