Create a Static Site Using React Running Python

Rob Blackbourn
8 min readJul 24, 2021

This article shows how to create a React app that uses Python that runs as a static site. It uses React, material-ui, pyodide, Python and numpy. You can see the final app here, and a step by step guide here.

The Problem

I’m getting up to speed with linear algebra, but it’s been so long since I did any matrix multiplication and I needed to do some exercises. I did the few I could find on the web, but what I really wanted was a little web site that would generate the exercises. I know how to do this with Python and numpy, but I don’t want to have to host a web site with a Python server.

I remember a few years ago there was a Mozilla project called iodide which was an alternative to Jupyter notebooks. There was a sub-project called pyodide which compiled the Python interpreter and data-science packages into WebAssembly. This sounded like what I needed. I took a look, and it seems to be a very active project; even with documentation!

Preparing The App

Before we can add the Python stuff we need to prepare the React app.

npx create-react-app --use-npm demo-react-pyodide

I want to use he material-ui toolkit. At the time of writing 2021–07–24 this toolkit uses React version 16, so the first thing to do is edit the “package.json”.

...
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react": "^16.8",
"react-dom": "^16.8",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
},
...

Now we update the packages and add “prop-types”.

npm install
npm install prop-types

Next we add “material-ui” and the “roboto” typeface.

npm install @material-ui/core@^4.12 @fontsource/roboto@^4.5

Now we change the source files to use these. First “src/index.js”:

import React from 'react'
import ReactDOM from 'react-dom'
import '@fontsource/roboto'
import CssBaseline from '@material-ui/core/CssBaseline'
import App from './App'
import reportWebVitals from './reportWebVitals'
ReactDOM.render(
<>
<CssBaseline />
<App />
</>,
document.getElementById('root')
)
reportWebVitals()

We’ve added the “roboto” typeface and added the “CssBaseline” to create the material-ui styles.

Next we can remove the “src/index.css”.

Now we change the “src/app.js”.

import React, { Component } from 'react'
import { withStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import Typography from '@material-ui/core/Typography'
const styles = (theme) => ({
message: {
margin: theme.spacing(2)
}
})
class App extends Component {render() {
const { classes } = this.props
return (
<div className={classes.message}>
<Typography variant="h2">Hello, World!</Typography>
</div>
)
}
}
App.propTypes = {
classes: PropTypes.object
}
export default withStyles(styles)(App)

Now we can remove: “src/App.css”, “src/App.test.js”, and “logo.svg”.

The app is now prepared. We can try it with:

npm start

Add Python

Now we have the React app setup with material-ui we can add Python.

There’s a project call pyodide which has compiled Python into WebAssembly. A preliminary npm package has been published, but I had difficulty crating a stable app with it. The solution that worked best was to add a “script” tag for the CDN at the end of the “head” of the “public/index.html”.

...
<head>
...
<script src="https://cdn.jsdelivr.net/pyodide/v0.17.0/full/pyodide.js"></script>
<head>
...

This loads all of the code we need to run Python in the browser. The functions are the available in the “window” global object.

Now we change “src/App.js” to load Python.

import React, { Component } from 'react'
import { withStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import Typography from '@material-ui/core/Typography'
const styles = (theme) => ({
message: {
margin: theme.spacing(2)
}
})
class App extends Component {
constructor(props) {
super(props)
this.state = {
version: null,
pyodide: null
}
}
componentDidMount() {
// Load Python.
window
.loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.17.0/full/' })
.then((pyodide) => {
// Run Python.
pyodide.runPythonAsync(`
import sys
sys.version
`)
.then((version) => {
this.setState({ version, pyodide })
})
})
.catch((error) => {
console.log(error)
})
}
render() {
const { version } = this.state
const { classes } = this.props
return (
<div className={classes.message}>
{version == null
? <Typography variant="h2">Loading Python</Typography>
: <Typography variant="h2">Version: {version}</Typography>
}
</div>
)
}
}
App.propTypes = {
classes: PropTypes.object
}
export default withStyles(styles)(App)

The main bit of code is in “componentDidMount”. First we call “loadPyodide” from the “window” global object, passing in the CDN url. This will load the “wasm” image for Python and set up the links between Python and JavaScript. This function is asynchronous. When the promise resolves it returns the “pyodide” object which is our gateway to the Python WebAssembly.

Using the “pyodide” object we can run Python code with “pyodide.runPythonAsync” passing in the code as a string. This async function returns a promise which resolves to the last value in the code. The code simply gets the Python version which is captured as the last value of the script. This gets set in the state along with the pyodide object, which we’ll need later on.

Finally the “render” method shows either a “loading” message, or the version we got from Python.

No we can run the app.

npm start

You should see the loading message then the version. We can now run Python code in the browser with no server support!

Generate Matrices

With Python now available in the browser we can do something more interesting.

As the purpose of the app is to generate exercises for matrix multiplication we will need some Python code to do this. We add the file “src/pythonCode.js” with the following contents.

function dotProductExerciseCode(maxNumberOfColumns, maxNumberOfRows) {
return `
import random
import numpy as np
# Generate random row and column sizes.
m = random.randint(1, ${maxNumberOfRows})
n = random.randint(1, ${maxNumberOfColumns})
p = random.randint(1, ${maxNumberOfColumns})
# Generate the random matrices and calculate the dot product.
rng = np.random.default_rng()
A = rng.integers(low=-10, high=10, size=(m, n), dtype=np.int32)
B = rng.integers(low=-10, high=10, size=(n, p), dtype=np.int32)
C = A @ B
`
}
export async function generateDotProductExercise(pyodide, maxNumberOfRows, maxNumberOfColumns) {
const code = dotProductExerciseCode(maxNumberOfRows, maxNumberOfColumns)
await pyodide.runPythonAsync(code)
const results = {
m: pyodide.globals.get('m'),
n: pyodide.globals.get('n'),
p: pyodide.globals.get('p'),
A: pyodide.globals
.get('A')
.toJs()
.map((x) => Array.from(x)),
B: pyodide.globals
.get('B')
.toJs()
.map((x) => Array.from(x)),
C: pyodide.globals
.get('C')
.toJs()
.map((x) => Array.from(x))
}
return results
}

The function “dotProductExerciseCode2 generates the Python code. The results are assigned to global variables. First we compute the random rows and columns. Then we generate the random matrices using “numpy”, and calculate the dot product. We set the “dtype” of the “np.array” to “np.int32”, as the default may be “np.int64” which would be returned as a “BigInt” to JavaScript which would require an extra step of transformation.

The function “generateDotProductExercise” runs the Python code asynchronously (so we don’t block the app), and then gets the globals variables from the Python environment. The row and column counts are just integer, so no transformation is required. The matrices get returned as an array of “Int32Array”. We use the “Array.from” class method to convert these to simple arrays. Finally all the data gets returned in an object.

Now we can create the shell of the matrix multiplication component.

First we make the folder “src/components”, then create the file “MatrixMultiplication.js” with the following contents:

import React, { Component } from 'react'
import { withStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import Container from '@material-ui/core/Container'
import Paper from '@material-ui/core/Paper'
import TextField from '@material-ui/core/TextField'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { generateDotProductExercise } from '../pythonCode'
const styles = (theme) => ({
paper: {
height: '100vh'
},
button: {
margin: theme.spacing(1, 1, 0, 0),
float: 'right'
},
parameter: {
width: '12ch',
margin: theme.spacing(1)
},
exercise: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}
})
class MatrixMultiplication extends Component {
constructor(props) {
super(props)
this.state = {
pyodide: props.pyodide,
maxNumberOfRows: 4,
maxNumberOfColumns: 4,
hasExercise: false,
m: 1,
n: 1,
p: 1,
A: [[0]],
B: [[0]],
C: [[0]]
}
}
generateExercise = () => {
const { maxNumberOfRows, maxNumberOfColumns, pyodide } = this.state
generateDotProductExercise(pyodide, maxNumberOfRows, maxNumberOfColumns)
.then((result) => {
this.setState({
...result,
answer: result.C.map((row) => row.map((col) => '')),
hasExercise: true
})
console.log(result)
})
.catch((error) => {
console.log(error)
})
}
handleSubmit = (event) => {
event.preventDefault()
this.generateExercise()
}
render() {
const { maxNumberOfRows, maxNumberOfColumns, A, B, C } = this.state
const { classes } = this.props
return (
<Container maxWidth="sm">
<Paper className={classes.paper}>
<form onSubmit={this.handleSubmit}>
<TextField className={classes.parameter} type="number" value={maxNumberOfRows} label="Max Rows" />
<TextField className={classes.parameter} type="number" value={maxNumberOfColumns} label="Max Columns" />
<Button type="submit" variant="outlined" color="primary" className={classes.button}>
New Exercise
</Button>
</form>
<div className={classes.exercise}>
<Typography variant="body1">
{JSON.stringify(A)} * {JSON.stringify(B)} = {JSON.stringify(C)}
</Typography>
</div>
</Paper>
</Container>
)
}
}
MatrixMultiplication.propTypes = {
classes: PropTypes.object,
pyodide: PropTypes.object
}
export default withStyles(styles)(MatrixMultiplication)

Looking in the render method we can see the component has two text boxes for the maximum rows and columns, and a button the generate the exercise. All of the data we will get back from the Python code we just display as text for now using “JSON.stringify”. The button calls the method “generateExercise” which calls the Python code that we created earlier, and sets the state with the results.

Finally we can add the component to the “src/App.js” file.

import React, { Component } from 'react'
import { withStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import Typography from '@material-ui/core/Typography'
import MatrixMultiplication from './components/MatrixMultiplication'
const styles = (theme) => ({
message: {
margin: theme.spacing(2)
}
})
class App extends Component {
constructor(props) {
super(props)
this.state = {
version: null,
pyodide: null
}
}
componentDidMount() {
window
.loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.17.0/full/' })
.then((pyodide) => {
pyodide.runPythonAsync(`
import sys
sys.version
`)
.then((version) => {
this.setState({ version, pyodide })
})
})
.catch((error) => {
console.log(error)
})
}
render() {
const { version, pyodide } = this.state
const { classes } = this.props
return (
<div className={classes.message}>
{version == null
? <Typography variant="h2">Loading Python</Typography>
: <MatrixMultiplication pyodide={pyodide} />
}
</div>
)
}
}
App.propTypes = {
classes: PropTypes.object
}
export default withStyles(styles)(App)

The only changes we made here were to import the “MatrixMultiplication” component, and render it with the “this.state.pyodide” object once Python had been loaded.

Now we can take it for a spin!

npm start

Clicking the exercise button runs the Python code and displays the matrix multiplication. The first time we click the button there is a delay while “numpy” is downloaded.

GitHub Pages

In this step we make the app functional. However, as the point of this is to show how to run Python in a browser we won’t go through the details of the JavaScript here. However, what we do want to see is how to use this as a “serverless” app running Python. One way we can do this is with GitHub pages. You can find the final project here.

Publishing The Site

First we set up the repository to use GitHub pages. Clicking the “settings” icon in the GitHub repository. There is a “Pages” tab on left of the screen. In the “pages” set the “source” to “main” or “master” and the “folder” to “docs”. You should get a message like:

Your site is published at https://rob-blackbourn.github.io/demo-react-pyodide/

The contents of the “docs” folder will now be available as a static web site at the “published at” url shown above.

To generate the web site we create a `.env` file in the root of the project with the following contents.

PUBLIC_URL=https://rob-blackbourn.github.io/demo-react-pyodide/
BUILD_PATH=docs

The “PUBLIC_URL” is the one provided by the GitHub Pages settings screen. The “BUILD_PATH” is the root “docs” folder where GitHub Pages will look for the static site. This is the folder where React will build the app.

Now we can build the app. Committing and pushing to GitHub will start the process of creating the site. In a few minutes we should be able to view our React/Python app running!

Next Steps

There’s a followup to this article addressing some of the scalability issues here.

--

--