Django, React, and Universal JavaScript

There has been a lot of talk lately about "Universal JavaScript." In short, this term describes the practice of pre-rendering the initial markup of a JavaScript application and serving it alongside the application logic. Doing so reduces the load time of your application since the user can interact with the page while the browser downloads and compiles the necessary files. On top of that, people who have JavaScript turned off (mostly Google) actually have a page to look at, rather than a blank screen, increasing overall SEO.

This post will cover how I accomplish this in my Django applications. For more information on so-called "isomorphic javascript," you can read this article over at Airbnb.

Plan of Attack

In order to be able to render React components, our django server needs a process to talk to which speaks JavaScript. The scalable way to do this in production is to spin up a small express server which will handle the rendering. Our django server will send the path of the component to render to this server which will reply with the rendered content. If you're looking to reduce the overhead associated with this process, you could install a reverse proxy to cache the request.

Installing Dependencies

We could write a wrapper around the network calls ourselves, but we can just use this handy package called python-react. Let's begin by downloading it using pip:

pip install react

We also need to download the node dependencies required by the javascript server:

npm install --save react react-render http express body-parse babel

The Javascript Server

With these packages installed, there are only two more things we need to do before we can render React components in our django app. First, we need to create a small javascript server that we will use to render the actual component. Copy the following code into a file in your django project:

// javascriptServer.js

var http = require('http')
var express = require('express')
var bodyParser = require('body-parser')
var reactRender = require('react-render')

// the address to run the server at
var ADDRESS = '127.0.0.1'
// the port to listen to
var PORT = 8001

// create an express app
var app = express()
// and a server that hosts the app
var server = http.Server(app)

// use the body parser middleware so that the post comes in as test
app.use(bodyParser.json())

/// url config

// the root url
app.get('/', function(req, res) {
    // end the response with a 404 error message and no other process
    res.end('this server renders react components!')
})

// posts to the react render url
app.post('/render', function(req, res) {
    // run the body of the response through react render 
    reactRender(req.body, function(err, renderedComponent) {
        var error = null
        if (err) {
            error = {
                type: err.constructor.name,
                message: err.message,
                stack: err.stack
            }
        } 
        // send the rendered component back to the django server
        res.json({
            error: error,
            markup: renderedComponent
        })
    })
})

// start the server at the designated address/port
server.listen(PORT, ADDRESS, function() {
    console.log('react render server listening at http://' + ADDRESS + ':' + PORT);
});

To start the server, run:

node javascriptServer.js

Setting Up Django

Now that that's done, we have to configure django to interface with the JavaScript server. To do this, add react to your INSTALLED_APPS variable as well as the following settings:

# make sure to add these apps to your settings file
INSTALLED_APPS = (
    ...
    'react',
 )

# python-react settings
REACT = {
    'RENDER': not DEBUG,
    'RENDER_URL': 'http://127.0.0.1:8001/render'
}

And we are good to go.

Let's Render Us a Component

With the JavaScript server running, the render_component provided by python-react allows us to easily render a component relative to the static root. To use it, simply pass the path to the component you want to render in a python script. To test out the server, run the following script in a python shell:

from react.render import render_component
# the component to be rendered
path_to_component = 'foo/bar/component.js'
# bundle the component up using webpack
component = render_component(path_to_component)

# print the initial markup 
print(component)

Setting Initial Data

Most of the time the component that is being rendered relies on some initial data that is grabbed from the server after it has loaded. Since we are pre-rendering our components, it would make sense for us to render the components with this data so that we no longer have to wait for $(document).ready().

Conveniently, python-react allows us to pass props to a component when we render it:

component = render_components (
    path_to_component,
    props = {
        # specify the initial data here by passing it as a prop
    }
)

Back to Django

Now that we know how to use python-react to pre-render our components, hooking it into a django view is only a few lines of code. For example, pre-rendering the root component of a Todo application that is located at scripts/todo.js relative to our STATIC_ROOT setting, the view would look like:

class TodoView(TemplateView):
    """
    render the index template
    """
    # the template we'll use to render the component
    template_name = 'component.html'

    def get_context_data(self, **kwargs):
        """"    Add the rendered component to the template context    """
        # grab the parent context
        context = super().get_context_data()
        # add the rendered component to the context
        context['component'] = render_component('scripts/todo.js', 
            translate=True ,
            props = {
                # pass the initial list of entries as a prop to the root component
                entries: serialize_initial_entries()
            }
        )
        # return the modified context
        return context

And its associated template:

<html>
    <body>
        {{ component }}
        <script src="{% static 'main.js' %}"></script>
    </body>
</html>

Don't forget to include your application logic!

So... Much... Boilerplate.

Yea, I know. So let's use django's class based views to help us out a bit:

class ComponentView(TemplateView):
    """ Pre-render the component designated by `self.component_path` """
    template_name = 'component.html' 
    component_path = ''   # the location of the component
    initial_data = {}   # initial data for the view passed as a props to the component

    def get_context_data(self, **kwargs):
        # grab the parent context
        context = super().get_context_data()
        # add the rendered component to the context
        context['component'] = render_component(self.component_path, translate=True, props=self.initial_data)
        # return the modified context
        return context


class AppView(ComponentView):
    """ Render the application component. """
    component_path = 'scripts/todo.js'
    # pass the initial data as a prop to the component
    initial_data = {
        entries: serialize_initial_entries()
    }

Not too shabby, huh?

Conclusion

In this post I showed you how I achieve "Universal" Django applications using python-react. We then DRY'd up the logic using a class based view. By the way, the general technique illustrated here (spinning up a separate JavaScript server) achieves pre-rendered javascript in any language - not just Python/Django.

Do you have another way of achieving "universal" apps in django? Let me know in the comments!

blog comments powered by Disqus