Going Isomorphic with Python and React

Isomorphic web applications are applications whose part or all the code is executed both on server and client. The server and the browser share the same code.

One of greatest goals that isomorphic solutions can achieve is improving SEO for Single Page Applications, but so far such features were mostly available for JavaScript developers who could run same code on Browser and NodeJS.

As Browsers only supports JavaScript (unless esoteric technologies are involved) all the widespread libraries and frameworks available to create isomorphic applications expect to run on JavaScript and as a Python developer we were pretty much out of choices.

Being dissatisfied with this situation is what lead me to work on DukPy, a Python library that aimed at removing the need for NodeJS from my work toolchain (hope it will for other people too).

One of the most widespread isomorphic web frameworks is React, which allows to render react components server and attach dynamic behaviours to them once they reach the browser.

In this article I’m going to show how DukPy and React can be used to make an isomorphic web application in Pure Python without even installing NodeJS.

If you have not read it yet, make sure to take a look at previous ES2015 and React in pure Python environment post as this one will take for granted the concepts explained there are already known.


I’ll take for granted that you already have all the required dependencies installed as showcased in the previous post

$ pip install TurboGears2
$ pip install Kajiki
$ pip install tgext.webassets
$ pip install dukpy

Once all the required pieces are in place we can start by making an application that renders a React Component on client side, then we will make the same component render on server and have the browser take over from there.

To do so we will create an statics/js directory where we will put all our Javascript

$ mkdir statics
$ mkdir statics/js

Make sure to download react and react-dom into that directory so that they are available to our web app

$ cd statics/js
$ curl -O 'https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js'
$ curl -O 'https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js'

Last but not least we need the Component itself, which will be a simple HelloWorld component.

Our component will be declared inside a statics/js/HelloWorld.jsx file:

export class HelloWorld extends React.Component {
render() {
return (
<div className="helloworld">
Hello {this.props.name}
</div>
);
}
}

Then we will create an app.py file where we will put the Python code that will start the webserver, create the web application, compile JSX to plain js and serve the index page which will render the component itself:

import tg
from tg import AppConfig
from tg import TGController
from tg import expose
import kajiki

page = kajiki.XMLTemplate(u'''<html>
<head></head>
<body>
<div id="isomor"></div>

<script py:for="m in g.webassets['bundle.js'].urls()"
src="$m">
</script>
<script>
ReactDOM.render(
React.createElement(HelloWorld.HelloWorld, { name: "World" }),
document.getElementById('isomor')
);
</script>
</body>
</html>
''', mode='html5')


class RootController(TGController):
@expose()
def index(self):
return page(dict(
g=tg.app_globals
)).render()


config = AppConfig(minimal=True, root_controller=RootController())
config.renderers = ['kajiki']
config.serve_static = True
config.paths['static_files'] = 'statics'

from webassets.filter import register_filter
from dukpy.webassets import BabelJSX
register_filter(BabelJSX)

import tgext.webassets as wa
wa.plugme(
config,
options={
'babel_modules_loader': 'umd'
},
bundles={
'bundle.js': wa.Bundle(
'js/react.js',
'js/react-dom.js',
wa.Bundle(
'js/HelloWorld.jsx',
filters='babeljsx',
),
output='assets/bundle.js'
)
}
)

application = config.make_wsgi_app()

from wsgiref.simple_server import make_server
print("Serving on port 8080...")
httpd = make_server('', 8080, application)
httpd.serve_forever()

If you try to run the saved app.py file with such content and all the dependencies are correctly in place you should see something like:

$ python app.py
No handlers could be found for logger "tg.configuration.app_config"
Serving on port 8080...

Heading your browser to http://localhost:8080 should greet you with an “Hello World”

If anything is unclear, make sure you start from the React in Pure Python post, as that will explain step by step what happened so far.


Now that our app is in place we can start introducing server side rendering for React.

This requires one additional component we will have to download into our statics/js directory, the react-dom-server library which allows server side rendering of React

$ cd statics/js
$ curl -O 'https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom-server.js'

Then we are going to render our component into our Python web application, to do so we are going to rely on DukPy for the actual rendering and WebAssets for providing the required dependencies

import json
from dukpy import JSInterpreter, jsx_compile
from markupsafe import Markup


class ReactRenderer(object):
def __init__(self, jspath):
self.jspath = jspath
self.jsi = JSInterpreter()
self.jsi.loader.register_path(self.jspath)
self.components = {}
self.initialized = False

def _init(self):
if self.initialized:
return

bundle_js = tg.app_globals.webassets['bundle.js']
self.jsi.evaljs(
[f.data() for f in bundle_js.build()] +
["var ReactDOM = require('react-dom-server');"]
)
self.initialized = True

def render(self, component, **kwargs):
self._init()
code = "ReactDOM.renderToString(React.createElement({component}, {args}), null);".format(component=component, args=json.dumps(kwargs))
return Markup(self.jsi.evaljs(code))

The ReactRenderer is a convenience class that will create a DukPy interpreter with React and our HelloWorld component preloaded (through the bundle.js WebAssets bundle we already declared) and react-dom-server loaded through require

In fact the class consists of a single render() method which will initialise the interpreter (if it’s not already initialised) and will then render the specified React Component. So we can use this class to render any Component that was available into our bundle.js including the HelloWorld one.

Only part left is we need to create it and provide it to our index() action so that it can use it to render the component. For convenience and as usually I’ll need the ReactRenderer object available everywhere I’ll make it available into the configuration of my application

import os
config.react_renderer = ReactRenderer(
os.path.join(os.path.dirname(__file__), 'statics', 'js')
)

Make sure you add this line before creating the TurboGears application (so before make_wsgi_app). The argument provided to ReactRenderer is the path where it can find any additional javascript module that will be loaded through require, in this case as we downloaded react-dom-server in statics/js that’s the specified path.

Now that our ReactRenderer is in place we can edit our index action and provide the react renderer to our HTML template

class RootController(TGController):
@expose()
def index(self):
return page(dict(
render_react=tg.config['react_renderer'].render,
g=tg.app_globals
)).render()

If you properly added the render_react value to the ones the controller action provides to the page template we can now change the template itself to render the component.

If you remember we previously had an empty isomor div

<div id="isomor"></div>

that div acted only as a target for our ReactDOM.render call which rendered the component and placed it into the div.

This was pretty clear through the fact that our page when loaded was empty for a moment and then the content appeared a little later when React was able to render it.

What we are going to do is replacing that empty div with one with the component pre-rendered inside:

<div id="isomor">${render_react('HelloWorld.HelloWorld', name='World')}</div>

The render_react callable is in fact the ReactRender.render method we provided from the action. If you remember the first argument is the Component that should be rendered (in this case HelloWorld from the HelloWorld module) and any additional keyword argument is passed as a property of the component. In this case we are providing the name=World property (same as we did in the React.createElement call).

Note that it is really important that any property provided to the Component when rendering it from python matches those provided to the React.createElement call in JS or React will complain and will replace the div content instead of reusing it (same will happen if you wrongly put empty spaces before or after the rendered component).


If everything worked as expected, the slight moment where your page was empty should have disappeared. The component is now server pre-rendered by Python and React will just kick in on the browser continuing where python left from.

Congratulations! We achieved our first Isomorphic Application in pure Python! :D

Show your support

Clapping shows how much you appreciated Alessandro Molina’s story.