Python and the Browser - Revisited
Tags: python, react, tutorial, webdev
Published: 2021-04-10 01:57:20

A while back, I posted about my initial foray into using Python to develop front-end web applications with React by using the [Transcrypt](https://transcrypt.org) transpiler.  
{% medium https://jennasys.medium.com/python-and-the-browser-4d0fa659722d %}  
Part of the initial learning process I went through was doing the official [React tutorial](https://reactjs.org/tutorial/tutorial.html), 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](https://github.com/JennaSys/tictacreact) 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*](https://github.com/JennaSys/tictacreact2/blob/master/index.html)
```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](https://www.npmjs.com/package/parcel-plugin-transcrypt) 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:
```python
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*](https://github.com/JennaSys/tictacreact2/blob/master/pyreact.py)
```python
# __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*](https://github.com/JennaSys/tictacreact2/blob/master/tictacreact.py)
```python
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:
```python
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:
```python
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:
```python
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:
```python
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](http://localhost:8000/index.html)

&nbsp;
![Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/qzas7d5ktnr1bw1p507f.png)

You can find all of the source code for this application here:  
[https://github.com/JennaSys/tictacreact2](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/](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](https://pyreact.com)