- Introduction
- Installation
- Configuration
- Development
- Production
- React in Django templates
- The Payoff Revealed
django-cra-helper is the missing link between Django and create-react-app. By adding this to your Django project, you can almost effortlessly inject your React components into your Django templates and initialize component props via Django context variables.
The ultimate goal of this package is to integrate these two projects with minimal changes to workflows that are typically used with either during development. From npm start
to python manage.py collectstatic
, your commands should work as expected so you can forget about implementation and get back to development!
Note: For the purposes of this README, the abbreviation CRA will be used to refer to create-react-app.
This package is available for installation via pip
:
pip install django-cra-helper
Once django-cra-helper is installed, cra_helper
will need to be added to INSTALLED_APPS
in settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'cra_helper',
'django.contrib.staticfiles',
]
Note:
cra_helper
must be placed abovedjango.contrib.staticfiles
in the list!
Add cra_helper.context_processors.static
to TEMPLATES['OPTIONS']['context_processors']
:
TEMPLATES = [
{
# ...snip...
'OPTIONS': {
'context_processors': [
# ...snip...
'cra_helper.context_processors.static',
],
},
},
]
Additionally, the following STATICFILES_FINDERS
list must be added to settings.py
:
STATICFILES_FINDERS = [
# Required for CRAManifestFinder below to work
'django.contrib.staticfiles.finders.FileSystemFinder',
# A finder to pull in asset-manifest.json
'cra_helper.finders.CRAManifestFinder',
]
The last necessary setting is the name of the folder containing the CRA project files, relative to the base directory of the Django project (the folder containing manage.py
):
CRA_APP_NAME = 'cra-app'
If for some reason the CRA liveserver does not serve on localhost or port 3000, the following settings can be added to settings.py
to specify its actual host and port:
CRA_HOST = '0.0.0.0' # defaults to 'localhost'
CRA_PORT = 9999 # defaults to 3000
Hot-reloading support can be enabled by first adding the following to your project or app's urls.py file:
# import Django settings
from django.conf import settings
# add `re_path` import here
from django.urls import path, re_path
# ...other imports...
from cra_helper.views import proxy_cra_requests
# other existing urls
urlpatterns = [...]
# add a reverse-proxy view to help React in the Django view talk to Create-React-App
if settings.DEBUG:
proxy_urls = [
re_path(r'^__webpack_dev_server__/(?P<path>.*)$', proxy_cra_requests),
re_path(r'^(?P<path>.+\.hot-update\.(js|json|js\.map))$', proxy_cra_requests),
]
urlpatterns.extend(proxy_urls)
Next, follow the instructions below that correspond to your project's version of react-scripts
:
For projects using react-scripts@<3.3.0
Add one more url to proxy_urls
above:
proxy_urls = [
# ...snip...
re_path(r'^sockjs-node/(?P<path>.*)$', proxy_cra_requests),
]
For projects using react-scripts@>=3.3.0
Create an .env file in the root of your Create-React-App folder with the following environment variable:
WDS_SOCKET_PORT=3000
Finally, run CRA's npm run build
command once to generate a build/
directory. django-cra-helper requires the build/asset-manifest.json file contained within to help load non-JS and non-CSS assets that might be used in any React components. This command should be re-run any time a new non-JS or non-CSS asset is added to the project.
If the CRA project's liveserver is started via npm start
prior to starting Django's development server via python manage.py runserver
, code changes in the React codebase will be updated immediately within Django views as well.
When the CRA liveserver is running, django-cra-helper adds a bundle_js
array template variable that can be inserted into the Django view's template to load the liveserver's various files containing all of the current JS and CSS. These files are recompiled on-the-fly by the liveserver whenever edits are made to the React code. This file can be added to a Django template as follows:
{% if bundle_js %}
{% for file_url in bundle_js %}
<script type="text/javascript" src="{{ file_url }}"></script>
{% endfor %}
{% endif %}
Note: Don't use the
static
template tag here! This file needs to be loaded from the CRA liveserver instead.
django-cra-helper also takes care of ensuring that Django's collectstatic
command pulls in production-ready bundles built by CRA's npm run build
command.
First, prepare React files for production with the typical CRA npm
build command:
npm run build
This will output bundled, minified JavaScript and CSS, and assets to the /build/
folder within the CRA project folder.
Once this command is complete, run the following Django command to gather static files, including the compiled React assets:
python manage.py collectstatic --no-input
React assets will be included with the other static assets in the settings.STATIC_ROOT
directory, to be served as is usual in a Django production environment. An asset-manifest.json
file will also get pulled in. The contents of this CRA-generated file are required by django-cra-helper to help reference React files that have had a unique hash added to their filenames during the build process.
Similar to the bundle_js
template variable mentioned earlier, django-cra-helper includes numerous other template variables when the CRA liveserver is not running:
For projects using react-scripts@>=3.2.0
Starting with react-scripts@3.2.0
, a new entrypoints
property can be found in asset-manifest.json. This contains an array of files that django-cra-helper makes available in templates to more easily inject these files via new entrypoints.css
and entrypoints.js
arrays:
{% for file in entrypoints.css %}
<link href="{% static file %}" rel="stylesheet">
{% endfor %}
{% for file in entrypoints.js %}
<script type="text/javascript" src="{% static file %}"></script>
{% endfor %}
NOTE: These JavaScript and CSS files should be arranged in an order required for the site to load; the ultimate order is derived from the order present in asset-manifest.json.
For projects using react-scripts@>=3.0.0 to react-scripts@<3.2.0
Code-splitting was introduced in later versions of react-scripts
that split up main_js
into multiple files. Additional <script>
tags are required to enable the React project to load. Depending on the size of your project, in addition to main_js
mentioned above you'll need to add at least two more in a specific order:
{% if main_js %}
<script type="text/javascript" src="{% static runtime_main_js %}"></script>
<script type="text/javascript" src="{% static static_js_2_9a95e042_chunk_js %}"></script>
<script type="text/javascript" src="{% static main_js %}"></script>
{% endif %}
The naming of static_js_2_9a95e042_chunk_js
above will differ from project to project. Unfortunately you'll have to manually confirm this value in your project's asset-manifest.json and update accordingly. It doesn't seem to change between builds, though, so it may not be a value you need to regularly update...
For older projects using react-scripts@<=2.1.8
The two most important variables are main_js
and main_css
. These can be injected into the page via a typical call to {% static %}
in the template:
{% if main_css %}
<link href="{% static main_css %}" rel="stylesheet">
{% endif %}
{% if main_js %}
<script type="text/javascript" src="{% static main_js %}"></script>
{% endif %}
NOTE: Recent attempts at building a fresh CRA project with
react-scripts@2.1.8
were unsuccessful in recreating SPAs that allowed for just a singlemain_js
.npm run build
-produced artifacts functioned almost identically to artifacts generated the same asreact-scripts@3.1.2
, detailed below.There may be child dependencies of
react-scripts
that make it no longer possible to start apps that will function with the above instructions. In these cases, please try the instructions in the next section.
CRA allows developers to specify a relative sub-folder for their site to be hosted from via the "homepage"
property in package.json:
{
"name": "cra-app",
"version": "0.1.0",
"homepage": "/frontend",
...
}
When this value is set, npm run build
will output assets and an asset-manifest.json with paths prepended with the path prefix:
Before: /static/js/main.319f1c51.chunk.js
After: /frontend/static/js/main.319f1c51.chunk.js
To make sure the React imports/assets/etc... can be found even when hosted through Django, you'll also need to update STATIC_URL
in Django's settings.py to include the path prefix:
STATIC_URL = '/frontend/static/'
CRA_PACKAGE_JSON_HOMEPAGE = '/frontend'
The value set to CRA_PACKAGE_JSON_HOMEPAGE
above should match the value of "homepage"
in package.json so that django-cra-helper can find the CRA liveserver and redirect appropriately:
Once these changes are made then the React app should be able to find everything it needs to function.
The CRA project will need to undergo a small bit of re-architecture to prepare it to accept input values via context when Django serves the view. The following is an example of how a couple of small tweaks to a CRA project's src/index.js file will establish a simple API for Django to communicate with the bundled React codebase:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
/**
* Maintain a simple map of React components to make it easier
* for Django to reference individual components
*/
const pages = {
App,
}
/**
* If Django hasn't injected these properties into the HTML
* template that's loading this script then we're viewing it
* via the create-react-app liveserver
*/
window.component = window.component || 'App';
window.props = window.props || { env: 'Create-React-App' };
window.reactRoot = window.reactRoot || document.getElementById('root');
/**
* React the component as usual
*/
ReactDOM.render(
React.createElement(pages[window.component], window.props),
window.reactRoot,
);
Basically, index.js
is updated to read values set to window.component
, window.props
, and window.reactRoot
and use these to render a component. Each of these three "inputs" will allow Django to easily specify which component to initialize on a per-view basis:
window.component
: A string that points to a Component entry inpages
window.props
: An Object containing props to get passed into the Componentwindow.reactRoot
: an instance ofdocument.getElementById
Note: Settings these values is optional. The defaults specified in the template above enable components to render as expected when viewed from the CRA liveserver.
Now that the "API" is in place, Django Views can include values for these inputs via the context they pass to their template:
def index(request):
context = {
'component': 'App',
'props': {
'env': 'Django',
},
}
return render(request, 'index.html', context)
Below is the Django app view's index.html template that can render across multiple versions of react-scripts
(intended only for demo purposes with a fresh CRA app):
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{% if entrypoints %}
{% for file in entrypoints.css %}
<link href="{% static file %}" rel="stylesheet">
{% endfor %}
{% elif main_css %}
<link href="{% static main_css %}" rel="stylesheet">
{% endif %}
<title>Django + React Project</title>
</head>
<body>
<div id="react">Loading...</div>
{{ props | json_script:"react-props" }}
<script>
window.component = '{{ component }}';
window.props = JSON.parse(
document.getElementById('react-props').textContent
);
window.reactRoot = document.getElementById('react');
</script>
{% if bundle_js %}
{% for file in bundle_js %}
<script type="text/javascript" src="{{ file }}"></script>
{% endfor %}
{% elif entrypoints %}
{% for file in entrypoints.js %}
<script type="text/javascript" src="{% static file %}"></script>
{% endfor %}
{% elif main_js %}
<script type="text/javascript" src="{% static runtime_main_js %}"></script>
{# make sure to update this accordingly according to asset-manifest.json #}
<script type="text/javascript" src="{% static static_js_2_597565cd_chunk_js %}"></script>
<script type="text/javascript" src="{% static main_js %}"></script>
{% endif %}
</body>
</html>
The context's component
and props
are bound to window.component
and window.props
respectively.
Note the use of the json_script
filter when setting windows.props
. Django provides this filter as a way to easily sanitize and convert a Python dict
to a Javascript Object
. The contents of the injected <script>
tag can be run through JSON.parse()
to safely assign it to a variable.
Finally, window.reactRoot
specifies the container element that the React component should be rendered into. Setting a value for this is only required if the container's id
is not "root" (the same ID assigned to the container <div>
in the CRA project's index.html
.)
In some scenarios it may be desirable to leverage React client-side routes alongside typical Django routes. Fortunately it's simple to define an additional "catch-all" Django route that will gracefully handle URLs that should be handled by routing in the React app.
In a typical client-side routing setup a React app may have the following routes defined:
import { BrowserRouter as Router, Switch, Route, NavLink, Redirect } from 'react-router-dom';
const App = () => (
<Router>
<ul>
<li><NavLink to='/'>To Home</NavLink></li>
<li><NavLink to='/foo'>To Foo</NavLink></li>
<li><NavLink to='/bar'>To Bar</NavLink></li>
</ul>
<Switch>
<Route path='/foo'>foo</Route>
<Route path='/bar'>bar</Route>
<Route exact path='/'>home</Route>
<Route path='*'>
<Redirect to='/' />
</Route>
</Switch>
</Router>
);
On the Django side of things, the following routes can be defined in a frontend
app's frontend/urls.py that both point to an index
view that renders frontend/templates/index.html:
from django.urls import path, re_path, include
from .views import index
urlpatterns = [
# Default index template to render index.html
path('', index),
# Helps Django pass unknown routes to the client-side router
re_path(r'^.*/$', index)
]
These URLs can then be include
'd in the project's root urls.py:
from django.urls import path, include
urlpatterns = [
# ...other routes...
path('', include('frontend.urls')),
]
Note: If your routes aren't working, make sure
'frontend'
has been added toINSTALLED_APPS
in settings.py
With both paths in place Django will now load the React app when it attempts to handle a path that is only defined as a client-side route.
Other assets bundled by CRA, including image assets, can be accessed in templates by substituting /
, .
, ~
, and -
in their filepaths with _
. django-cra-helper adds every entry in asset-manifest.json to the base context, using these substitution rules to accomodate Django's static
tag.
For example, a logo.svg file in the CRA project can be included in a Django template as follows:
<!-- This file is located at `/build/static/media/logo.svg` -->
<img src="{% static static_media_logo_svg %}" height="40" width="40">
Note: This is optional! Static assets can still be included in the Django app's
/static/
directory and loaded as usual. The special substitution mentioned above is only needed when reusing React assets outside of specific components.
When all is said and done, React components should now render and be viewable in both the CRA liveserver and when served via Django. Here's an example of a slightly-modified CRA App
component displayed in CRA (left) and Django (right):