Python in React with Pyodide
Tags: react, python, webassembly
Published: 2021-07-13 23:45:42

Pyodide allows you to run Python code within the browser via WebAssembly (wasm). It's a great option if, like me, you're someone who wants to escape some of the limitations of working with JavaScript.

Getting things up and running involves a few steps, described in the [Pyodide docs](https://pyodide.org/en/stable/usage/quickstart.html):
1. Include Pyodide.
2. Set up the Python environment (load the Pyodide wasm module and initialize it).
3. Run your Python code.

Cool, but it'd be nice to handle all of this in a reusable React component. How can we make it work?

Let's take it step by step.

## Step 1: Include Pyodide

The first task is easy enough: add a `script` tag to the document `head` with the Pyodide CDN url as the `src` attribute. Better yet, if you're using a framework like Gatsby or Next.js (I used the latter for this example), wrap your `script` inside a [built-in](https://nextjs.org/docs/api-reference/next/head) `Head` component that will append tags to the `head` of the page for you ([react-helmet](https://github.com/nfl/react-helmet) is another great option). That way you won't have to worry about accidentally forgetting to include Pyodide in your project, since it's already part of your component.

Let's call our component `Pyodide`. Here's what we have so far:
```jsx
import Head from 'next/head'

export default function Pyodide() {
  return (
    <Head>
      <script src={'https://cdn.jsdelivr.net/pyodide/dev/full/pyodide.js'} />
    </Head>
  )
}
```

## Step 2: Set Up Python Environment

Here things get tricky.

Our script will attach a function called `loadPyodide` to the [global object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) of our environment. In the browser, this is the `window` object, but more generally it is called `globalThis`. As long as our script is loaded, we can call this function as follows, where `indexURL` is a string matching the first part of the CDN url from earlier:
```jsx
globalThis.loadPyodide({
  indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
})
```

The return value of `loadPyodide` is the Pyodide module itself, which we will eventually call to run our Python code. Can we simply assign the result to a variable? Not quite! We need to consider a couple caveats.

First, `loadPyodide` takes awhile to execute (unfortunately, several seconds), so we'll need to call it asynchronously. We can handle this with [async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await). Second, this function creates side effects. We'll need React's [`useEffect`](https://reactjs.org/docs/hooks-effect.html) hook, which is placed before the `return` statement of a function component.

The effect will look something like this:
```jsx
useEffect(() => {
  ;(async function () {
    pyodide = await globalThis.loadPyodide({
      indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
    })
  })()
}, [pyodide])
```

The `await` expression gets wrapped inside an `async` [IIFE (Immediately Invoked Function Expression)](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) that runs as soon as it's defined.

In addition, note the second argument of `useEffect`, which is an array of the effect's dependencies. By default, an effect will run after every component render, but including an empty array `[]` of dependencies limits the effect to running only after a component mounts. Adding a dependency causes the effect to run again any time that value changes.

So far, our dependency list only includes the `pyodide` variable we're using to store the result of `loadPyodide`. However, you might have noticed that `pyodide` hasn't actually been defined yet. As it turns out, we can't just add `let pyodide` above our effect, since doing so would cause the value to be lost on every render. We need the value of `pyodide` to persist across renders.

To accomplish this, we can use another hook, called [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref), that stores our mutable value in the `.current` property of a plain object, like so:
```jsx
import { useEffect, useRef } from 'react'

export default function Pyodide() {
  const pyodide = useRef(null)

  useEffect(() => {
    ;(async function () {
      pyodide.current = await globalThis.loadPyodide({
        indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
      })
    })()
  }, [pyodide])

  // ...
}
```

The argument we pass into `useRef` sets the initial value of `pyodide.current` to `null`. Notice that the `pyodide` object itself is immutable: it never changes, even when we update the value of its `.current` property. As a result, our effect only gets called once on component mount, which is exactly what we want.

Now we just need to figure out how to use the loaded Pyodide module to run Python code.

## Step 3: Evaluate Python Code

Let's jump right into this one.

We'll use a function provided by Pyodide called `runPython` to evaluate a string of Python code. For simplicity, we'll add everything to a new effect:
```jsx
const [isPyodideLoading, setIsPyodideLoading] = useState(true)
const [pyodideOutput, setPyodideOutput] = useState(null)

useEffect(() => {
  if (!isPyodideLoading) {
    ;(async function () {
      setPyodideOutput(await pyodide.current.runPython(pythonCode))
    })()
  }
}, [isPyodideLoading, pyodide, pythonCode])
```

The first thing to notice is that we've added yet another hook, called [`useState`](https://reactjs.org/docs/hooks-state.html), which returns a pair of values. The first value is the current state, and the second is a function used to update the state with whatever value is passed as an argument. We also have the option to set the initial state by passing an argument to `useState`.

Here we set the initial state of `isPyodideLoading` to `true` and add a condition inside the effect to call `runPython` only when Pyodide is done loading. Just like with the first effect, we wrap `runPython` inside an `async` IIFE to `await` the result. That result is then passed to `setPyodideOutput`, which updates the variable `pyodideOutput` from its initial value of `null`.

This effect has three dependencies. As before, `pyodide` remains constant, and therefore it will never cause our effect to rerun. We also expect the value of `pythonCode` to remain unchanged, unless we decide to enable some sort of user input later on. Regardless, we have yet to actually declare this variable. Where should we do that?

Our string of `pythonCode` is really the defining characteristic of the component. Thus, it makes sense to include `pythonCode` in `props`. Using the component would then look something like this:
```jsx
<Pyodide pythonCode={myPythonCodeString} />
```

We need to consider `isPyodideLoading`, too. This is a dependency we want updated: it should change from `true` to `false` once Pyodide is finished loading and ready to evaluate Python code. Doing so would re-render the component, run the effect, and meet the criteria of the `if` statement in order to call `runPython`. To accomplish this, we'll need to update the state with `setIsPyodideLoading` inside our first effect.

Of course, we also need to render the results!

## Complete React Component
Let's put all of it together as a complete, working component:
```jsx
import { useEffect, useRef, useState } from 'react'
import Head from 'next/head'

export default function Pyodide({
  pythonCode,
  loadingMessage = 'loading...',
  evaluatingMessage = 'evaluating...'
}) {
  const indexURL = 'https://cdn.jsdelivr.net/pyodide/dev/full/'
  const pyodide = useRef(null)
  const [isPyodideLoading, setIsPyodideLoading] = useState(true)
  const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage)

  // load pyodide wasm module and initialize it
  useEffect(() => {
    ;(async function () {
      pyodide.current = await globalThis.loadPyodide({ indexURL })
      setIsPyodideLoading(false)
    })()
  }, [pyodide])

  // evaluate python code with pyodide and set output
  useEffect(() => {
    if (!isPyodideLoading) {
      const evaluatePython = async (pyodide, pythonCode) => {
        try {
          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])

  return (
    <>
      <Head>
        <script src={`${indexURL}pyodide.js`} />
      </Head>
      <div>
        Pyodide Output: {isPyodideLoading ? loadingMessage : pyodideOutput}
      </div>
    </>
  )
}
```

As promised, we now have `pythonCode` included as one of the component's `props`. We've also added `setIsPyodideLoading` to the first effect, calling it inside the `async` function after `loadPyodide` resolves. Furthermore, we render `pyodideOutput` inside a `div`, which is wrapped within a [React fragment](https://reactjs.org/docs/fragments.html) underneath the `Head` component. There are a few other additions to the code, as well. Let's go over them.

Our output is rendered conditionally. Initially, `isPyodideLoading` is `true`, so a `loadingMessage` gets displayed. When `isPyodideLoading` becomes `false`, `pyodideOutput` is shown instead. However, even though Pyodide has finished loading at this point, that doesn't mean `runPython` is done evaluating code. We need an `evaluatingMessage` in the meantime.

In many cases, this message will appear for only a fraction of a second, but for more complicated code it could hang around for much longer. To make it work, we've set `evaluatingMessage` as the initial value of `pyodideOutput`. A React component re-renders any time its state changes, so we can be sure all of our outputs get displayed as expected. Both messages have been added to `props` with a default string value.

We've also encapsulated a bit of the second effect's contents inside an asynchronous function called `evaluatePython`, which adds a [`try...catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) statement to handle any errors that might occur when calling `runPython`.

Finally, we've added a variable called `indexURL` so it can be updated easily if needed. Its value is passed to `loadPyodide` and embedded in a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) to build the full `src` string of the `script` tag.

Great! We've got a working Pyodide component. That's it, right?!? Well, no... Unfortunately, we have one final problem to solve.

## One Final Problem: Multiple Components

If all you want is a single Pyodide component on your page, then you're good to go. However, if you're interested in multiple components per page, try it out. You'll get an error:
```
Uncaught (in promise) Error: Pyodide is already loading.
```

This error is a result of calling `loadPyodide` more than once. If we want multiple components on a single web page, we'll need to figure out how to prevent all but the first component from initializing Pyodide. Unfortunately, Pyodide doesn't provide any method to tell whether `loadPyodide` has already been called, so we have to find a way to share that information between components on our own.

### React Context

Enter [React context](https://reactjs.org/docs/context.html). This API allows us to share global data across components without having to deal with some external state management library. It works via the creation of a Context object, which comes with a special component called a Provider. The Provider gets wrapped around a high level component in the tree (usually the root of an application) and takes a `value` prop to be passed along to child components that subscribe to it. In our case, we'll utilize the [`useContext`](https://reactjs.org/docs/hooks-reference.html#usecontext) hook to listen for changes in the Provider's `value` prop.

Alright, so we need to build a Provider component. We'll call it `PyodideProvider`. Let's start by identifying the values that all of our lower-level Pyodide components will need to share.

### Provider Component

Our goal is to ensure that only the first Pyodide component on a page calls `loadPyodide`, so we know we'll need to create some condition in the first effect that depends on a shared value describing whether or not `loadPyodide` has been called. Let's be explicit about it and call this value `hasLoadPyodideBeenCalled`. It'll need to be a boolean that's initially set to `false`, and then changed to `true`. When does this change occur?

Well, since `loadPyodide` is asynchronous, the update of `hasLoadPyodideBeenCalled` must happen before calling `loadPyodide` to be of any use. This is the reason why we do in fact need a new variable for our condition, rather than using `isPyodideLoading` like in the second effect. We can't wait for Pyodide to load. Instead, the information must propagate immediately to our context value to keep subsequent components from running before they receive the update.

This need actually leads us to another, more subtle requirement for how we handle `hasLoadPyodideBeenCalled`. The global values we define need to persist across component renders, meaning they'll have to be set with `useRef` or `useState`. Although `useState` might seem like the natural option, it turns out this won't work. React doesn't guarantee immediate state updates. Instead, it batches multiple `setState` calls asynchronously. Using state to handle our update to `hasLoadPyodideBeenCalled` would likely be too slow to prevent later components from calling `loadPyodide` more than once. Luckily, `useRef` doesn't suffer from this latency: changes are reflected right away, so we'll use this hook instead.

Are there any other values that need to be shared globally? Yep! There are three more: `pyodide`, `isPyodideLoading`, and `setIsPyodideLoading`.

Since `loadPyodide` is now only being called a single time, it's also being assigned just once to `pyodide.current`, the wasm module we want to share between all Pyodide components on a page. Furthermore, `setIsPyodideLoading` gets called inside the first effect's condition, which again, only runs for the first component on the page. That function is paired with the state variable `isPyodideLoading`, a value that, when updated, needs to trigger the second effect for every component. As a result, each of these variables needs to be shared globally via context.

Let's put it all together. Here's the complete Provider component:
```jsx
import { createContext, useRef, useState } from 'react'

export const PyodideContext = createContext()

export default function PyodideProvider({ children }) {
  const pyodide = useRef(null)
  const hasLoadPyodideBeenCalled = useRef(false)
  const [isPyodideLoading, setIsPyodideLoading] = useState(true)

  return (
    <PyodideContext.Provider
      value={{
        pyodide,
        hasLoadPyodideBeenCalled,
        isPyodideLoading,
        setIsPyodideLoading
      }}
    >
      {children}
    </PyodideContext.Provider>
  )
}
```

We first create and export a Context object called `PyodideContext` using `createContext`. Then we export our `PyodideProvider` as `default`, wrap `PyodideContext.Provider` around any `children` that may exist, and pass our global variables into the `value` prop.

The Provider component can be imported wherever it's needed in the application. In Next.js, for example, wrapping `PyodideProvider` around the application root happens in the `_app.js` file and looks something like this:
```jsx
import PyodideProvider from '../components/pyodide-provider'

export default function MyApp({ Component, pageProps }) {
  return (
    <PyodideProvider>
      <Component {...pageProps} />
    </PyodideProvider>
  )
}
```

## The Final Pyodide Component

At last, we're ready for the final Pyodide component, which can be included multiple times on a single page.

We only need to make a few adjustments to the original component. For starters, we'll have to import `PyodideContext` from our Provider and extract the global values from it with `useContext`. Then we update our first effect as described earlier to include `hasLoadPyodideBeenCalled`.

Lastly, we add `hasLoadPyodideBeenCalled` to the first effect's dependency list, along with `setIsPyodideLoading`. Including the latter is necessary because, although React guarantees that `setState` functions are stable and won't change on re-renders (which is why we could exclude it initially), we are now getting the value from `useContext`. Since this context is defined in the Provider, our separate Pyodide component has no way of knowing that `setIsPyodideLoading` is truly stable.

That's all of it! Here it is, the final Pyodide component:
```jsx
import { useContext, useEffect, useState } from 'react'
import Head from 'next/head'
import { PyodideContext } from './pyodide-provider'

export default function Pyodide({
  pythonCode,
  loadingMessage = 'loading...',
  evaluatingMessage = 'evaluating...'
}) {
  const indexURL = 'https://cdn.jsdelivr.net/pyodide/dev/full/'
  const {
    pyodide,
    hasLoadPyodideBeenCalled,
    isPyodideLoading,
    setIsPyodideLoading
  } = useContext(PyodideContext)
  const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage)

  useEffect(() => {
    if (!hasLoadPyodideBeenCalled.current) {
      hasLoadPyodideBeenCalled.current = true
      ;(async function () {
        pyodide.current = await globalThis.loadPyodide({ indexURL })
        setIsPyodideLoading(false)
      })()
    }
  }, [pyodide, hasLoadPyodideBeenCalled, setIsPyodideLoading])

  useEffect(() => {
    if (!isPyodideLoading) {
      const evaluatePython = async (pyodide, pythonCode) => {
        try {
          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])

  return (
    <>
      <Head>
        <script src={`${indexURL}pyodide.js`} />
      </Head>
      <div>
        Pyodide Output: {isPyodideLoading ? loadingMessage : pyodideOutput}
      </div>
    </>
  )
}
```

I've added both the `Pyodide` React component and the `Provider` to a Gist, as well. Feel free to view them [here](https://gist.github.com/lambertbrady/3b70a860b1ee6118d141f477c914e5c2).