A while back, I posted about my initial foray into using Python to develop front-end web applications with React by using the Transcrypt transpiler.
Python and the Browser. I had to find out: Was it actually… | by JennaSys | Medium
JennaSys ・ ・
jennasys.Medium
Part of the initial learning process I went through was doing the official React tutorial, but using Python for the code instead of JavaScript. When I did that, I adhered to the structure of the application that was used in the tutorial pretty closely. Since then, I have been working with Transcrypt and React quite a bit more and have formed some of my own standards of practice for producing clean Python code in my React applications. In this post, I'll show you a few of those practices as I take the original class-based version of that program I did (which is what the React tutorial is based on), and convert it to use functional components and React hooks instead (which is all I use now).
Overview
The premise of the React tutorial is a Tic-Tac-Toe game that maintains a history of moves, where you can reset the board back to any previous point. The design consists of a Game component that manages the state of the game and holds the history of moves. Then there is a Board component that handles the rendering of the board. And Lastly, there is a Square component that renders a single square in the game.
My original version of the application has four files:
- index.html (the application entry point and DOM root)
- game.css (CSS selectors for the application)
- tictacreact.py (the application code in Python)
- pyreact.py (Python wrappers for the React.Component class and miscellaneous JavaScript functions)
Support Files
For this makeover, the CSS file and the index.html file will remain pretty much unchanged:
Listing 1: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Tic Tac React!</title>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="game.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="__target__/tictacreact.js"></script>
</body>
</html>
Since we will no longer be using React class components, we can clean up the pyreact.py file quite a bit. I use this Python module to hold all of the Python-to-JavaScript mappings, that I can then import into other Python modules. This approach facilitates keeping any JavaScript messiness in one place and allows all of the other Python modules to remain pure Python, which keeps the Python linter happy.
For most projects, I've been using the Parcel bundler which has a Transcrypt plug-in available for it. With that, I would normally have a few lines in the pyreact.py module to load the React libraries with a JavaScript ES5 style import that uses the Node require()
function like this:
React = require('react')
ReactDOM = require('react-dom')
In this case, since we are loading the React libraries in the HTML header, the React and ReactDOM namespaces will be global, so I instead just stubbed out those libraries in the Python file.
Listing 2: pyreact.py
# __pragma__ ('skip')
"""
These JavaScript object stubs are just to
quiet the Python linter and are ignored by transcrypt as long
as they are imported inside of pragma skip/noskip lines.
"""
class React:
createElement = None
useState = None
useEffect = None
createContext = None
useContext = None
class ReactDOM:
render = None
class document:
getElementById = None
addEventListener = None
# __pragma__ ('noskip')
# Map React javaScript objects to Python identifiers
createElement = React.createElement
useState = React.useState
useEffect = React.useEffect
createContext = React.createContext
useContext = React.useContext
# Wrap the ReactDOM.render method to hide JavaScript details
def render(root_component, props, container):
def main():
ReactDOM.render(
React.createElement(root_component, props),
document.getElementById(container)
)
document.addEventListener("DOMContentLoaded", main)
The section between the skip/noskip pragma
lines isn't really needed other than to quiet the Python linter not being able to resolve the JavaScript object names. They are just Python stub declarations that will ultimately be ignored by Transcrypt thanks to the compiler directives.
The mappings in this file are where Transcrypt does a lot of its magic. I'm basically assigning a JavaScript object to a Python variable. From there, it can be used just like any other Python object. It can be imported into other Python modules, and its methods can be called. Even though I'm using JavaScript libraries, I only need to know the library's API to code to it using Python.
The render()
function doesn't change from before, and is just a wrapper around the ReactDOM.render()
method that lets us encapsulate the JavaScript calls that go along with it.
The Refactoring
Most of the actual refactoring we did in this version of the application was in the tictacreact.py module. Beyond just turning the class components into functional components, we also changed how some of the state gets updated. While it didn't save us many lines of code, it is now a bit more modularized and (hopefully) more readable than what was there before.
Listing 3: tictacreact.py
from pyreact import render, useState, createElement as el
from pyreact import createContext, useContext
Ctx = createContext()
def Square(props):
idx = props['idx']
ctx = useContext(Ctx)
squares = ctx['squares']
onClick = ctx['onClick']
return el('button', {'className': 'square',
'onClick': lambda: onClick(idx)
}, squares[idx])
def Row(props):
rowNum = props['rowNum']
row = [el(Square, {'idx': (rowNum * 3) + col_num}) for col_num in range(3)]
return el('div', {'className': 'board-row'}, row)
def Board():
rows = [el(Row, {'rowNum': row_num}) for row_num in range(3)]
return el('div', None, rows)
def Moves(props):
numMoves = props['numMoves']
setStepNumber = props['setStepNumber']
def get_move(move):
desc = ('Go to move #' + str(move)) if move > 0 else 'Go to game start'
return el('li', {'key': move},
el('button', {'className': 'move-history',
'onClick': lambda: setStepNumber(move)
}, desc)
)
return [get_move(move) for move in range(numMoves)]
def Game():
history, setHistory = useState([{'squares': [None for _ in range(9)]}])
stepNumber, setStepNumber = useState(0)
board = history[stepNumber]
xIsNext = (stepNumber % 2) == 0
winner = calculate_winner(board['squares'])
if winner is not None:
status = f"Winner: {winner}"
elif stepNumber == 9:
status = "No Winner"
else:
status = f"Next player: {'X' if xIsNext else 'O'}"
def handle_click(i):
new_squares = list(board['squares'])
if winner or new_squares[i]: # Already winner or square not empty
return # Nothing to do
new_squares[i] = 'X' if xIsNext else 'O'
tmp_history = history[:stepNumber + 1] # Slice in case step changed
new_history = [{'squares': move['squares']} for move in tmp_history]
new_history.append({'squares': new_squares})
setHistory(new_history)
setStepNumber(len(new_history) - 1)
return el(Ctx.Provider, {'value': {'squares': board['squares'],
'onClick': handle_click}
},
el('div', {'className': 'game'},
el('div', {'className': 'game-board'},
el(Board, None),
el('div', {'className': 'game-status'}, status),
),
el('div', {'className': 'game-info'}, 'Move History',
el('ol', None,
el(Moves, {'numMoves': len(history),
'setStepNumber': setStepNumber}
)
)
)
)
)
# Render the component in a 'container' div
render(Game, None, 'root')
def calculate_winner(squares):
lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
]
for line in lines:
a, b, c = line
if squares[a] and (squares[a] == squares[b]) and (squares[a] == squares[c]):
return squares[a]
return None
In the main Game
component, we made several changes starting with converting the class-based state
object to individual useState()
hooks instead. So the history
and stepNumber
state variables now have their own companion update functions.
Since the xIsNext
state variable that was being used before is just calculated based on the value of another state variable, I changed it to be a local variable instead. It will get recalculated if a re-render happens due to a change in the state that it is based on.
To clarify what is being displayed at any given time, I also added the local variable board
to hold the current board values as a convenience. Now, as determined by the stepNumber
, we pull it out of the history list just once instead of every time we need to use it as we were doing before. This value also gets recalculated when a re-render happens.
The handle_click()
function gets cleaned up a little, but it is still a bit busy since we need to make copies of the history
and board
squares in order to update them since we are working with immutable objects. Until Transcrypt adds the Python copy
standard library to what it supports, you either have to use a JavaScript function to do that, or do it manually as we did here for history
where we used a list comprehension:
new_history = [{'squares': move['squares']} for move in tmp_history]
For the list of previous moves that get displayed in the UI, instead of generating the list item elements in the Game
component, we moved that functionality into its own Moves
component. This change cleans up the Game
component and makes the overall application structure a little more readable.
One of the practices I started doing was to deconstruct the values held in the props
object into local variables rather than directly referencing them from props
just when they are needed:
def Moves(props):
numMoves = props['numMoves']
setStepNumber = props['setStepNumber']
This practice accomplishes two things. First, by deconstructing all of the values right at the top of the function definition, I know exactly what props that component is expecting without having to search the entire function to figure it out. Second, it cleans up the code in the function where I actually use those values by not having to do the dictionary lookups in place.
The last somewhat subtle change we made is to put the handle_click()
function and the board
squares into a context variable:
el(Ctx.Provider, {'value': {'squares': board['squares'],
'onClick': handle_click}
},
Using this context variable saves us from having to pass these values down through several other layers of components that don't need them, just so that we can use them in the Square
component.
In the Board
component, we really cleaned it up in that it now just returns a div
element with three Row
components. And since we are now using the context variable, we no longer need to pass any props into it.
The Row
component is something new we added with this refactor that clarifies conceptually what is being generated. Similar to the Board
component, the Row
component returns a div
element containing just three Square
components.
The Square
component is now a bonafide React component instead of just an imperative function. Functionally it is the same as before, but we did add in the React useContext()
hook to pull out the values we needed to use here:
ctx = useContext(Ctx)
squares = ctx['squares']
onClick = ctx['onClick']
Finally, we just made some minor optimizations to the calculate_winner()
function from the earlier version.
Transpile & Run
Right now, Transcrypt version 3.7.16 only works with Python 3.6 or 3.7, so in setting up a virtual environment, I'll use this:
$ python3.7 -m venv venv
then activate it:
$ source ./venv/bin/activate
(for Windows use venv\Scripts\activate
)
and then install Transcrypt:
(venv) $ pip install transcrypt
To build the application, you just need to give Transcrypt the entry point of your application, and it will walk the dependency tree to transpile any other related modules:
(venv) $ transcrypt --nomin --build --map tictacreact
We also gave it a few CLI options:
- nomin - tells it not to minify the generated JavaScript (Note: The Java runtime is needed for this to work)
- build - tells it to start from scratch
- map - tells it to generate a JavaScript-to-Python source code map file
Once that is done, we need to serve up the generated files before we can open up the application in a web browser. A quick way to do this is using the HTTP server that comes with Python:
(venv) $ python -m http.server
Then open the application:
http://localhost:8000/index.html
You can find all of the source code for this application here:
https://github.com/JennaSys/tictacreact2
A live demo of this code (with source maps) is also hosted on GitHub Pages:
https://jennasys.github.io/tictacreact2/
Conclusion
As someone that really likes Python and isn't a big fan of JavaScript, using Transcrypt to develop React applications with Python has been working out decidedly well for me so far. To share how I was doing it, I had started putting together an outline for a talk I was going to give at my Python meetup group. As it turned out, that outline kept growing, and I ended up writing an entire book about it instead. If you're interested, you can find out more about the "React to Python" book here: https://pyreact.com
Top comments (0)