Create a Static Site Using React Running Python

The Problem

Preparing The App

npx create-react-app --use-npm demo-react-pyodide
...
"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"
},
...
npm install
npm install prop-types
npm install @material-ui/core@^4.12 @fontsource/roboto@^4.5
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()
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)
npm start

Add Python

...
<head>
...
<script src="https://cdn.jsdelivr.net/pyodide/v0.17.0/full/pyodide.js"></script>
<head>
...
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)
npm start

Generate Matrices

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
}
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)
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)
npm start

GitHub Pages

Publishing The Site

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

Next Steps

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store