Django Templates with React
I'm relatively new to the Django world, and I love it for the amount of ready-to-use solutions. The only thing I don't quite like is Django Templates. After using React for a while, these templates feels a bit cumbersome and inelegant.
I found a few articles recommending creating full-blown React SPA and using Django just as a REST API. But there are few issues with this solution, which makes it not that attractive.
If you create full-blown React SPA, you will have code duplication right away, because there is routing here and there, there is authentication and authorization, there are error pages, there are states & data models... For some applications / teams / companies it is an acceptable and even desirable outcome, but not for me and my small application.
So, how do we use React just as a template engine?
Idea
There is a nice thing about React, it doesn't just mount to the DOM body
or somewhere random, it mounts to the exact element you specify.
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
So the idea is to try to implement routing using one mounting point just for one page.
At this point, an attentive reader will notice that for each page we will actually load JavaScript for every other page because it's the same bundle, but please bear with me for a while.
Routing
So, we can specify multiple mounting points in index.js:
ReactDOM.render(
<h1>Page 1!</h1>,
document.getElementById('page-1')
);
ReactDOM.render(
<h1>Page 2!</h1>,
document.getElementById('page 2')
);
If there is <div id="page-1"/>
on the page, then the user will see the first page, but if there is <div id="page-2"/>
, then the user will see the second page.
On the Django side, you will just need to render <div>
block with the right ID:
{% extends "base.html" %}
{% load static %}
{% block content %}
<div id="{{ element_id }}"></div>
<script src="{% static 'index.js' %}"></script>
{% endblock %}
def index_view(request):
if request.method == 'GET':
return render(
request,
'react-page.html',
{ 'element_id': 'todos-index' }
)
Bundle size
Now it's the time to figure out how not to load JavaScript for pages which aren't used right now.
There is actually a built-in React feature, which is called Code Splitting. You can load pages only when you actually need them, which will improve user experience.
So our pages routing will start looking something like this:
let Page1 = React.lazy(() => import('./page1'))
let Page2 = React.lazy(() => import('./page2'))
ReactDOM.render(
<Suspense fallback={<></>}>
<Page1/>
</Suspense>,
document.getElementById('page-1')
);
ReactDOM.render(
<Suspense fallback={<></>}>
<Page2/>
</Suspense>,
document.getElementById('page-2')
);
Context data
At this point we can implement guards and routing on Django side and render static pages using React. Now it's time to figure out how to pass data.
We can use Django built-in called json-script which takes some data, escapes dangerous characters and dumps it to the JSON, which is readable by JavaScript. Let's try to leverage it.
{% extends "base.html" %}
{% load static %}
{% block content %}
<div id="{{ element_id }}"></div>
{{ page_context | json_script:'page-context' }}
<script src="{% static 'index.js' %}"></script>
{% endblock %}
And get this data on React side
//custom hook
export let usePageContext = <T = any>() => {
let [pageContext, setPageContext] = useState<T | undefined>(undefined)
useEffect(() => {
let pageContext = document.getElementById('page-context').textContent
setPageContext(JSON.parse(pageContext))
}, [])
return pageContext as T
}
interface Todo {
id: number
title: string
}
const TodosIndexPage = memo(() => {
let pageContext = usePageContext<{ todos: Todo[] }>()
let todos = pageContext?.todos
return <>
<h1>React todos page</h1>
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
</>
})
export default TodosIndexPage
Final notes
That's basically it, there are only few steps to make it work:
- Separate mounting points for React.
- (Optional, but good for performance) Setup code splitting for React.
- Return target mounting point on Django side.
- Send page context on Django side.
- Get page context on React side.
You can find a working example on my GitHub github.com/kozlovzxc/djangoReactTemplates, but I encourage you to play with it yourself, because you probably will come up with something better!
p.s.: You can also try to duplicate Django routes using React Router instead of multiple mounting point, but I'm not sure about benefits though.
Btw, let's be friends here and on twitter 👋
Top comments (7)
Great work thanks. This has really helped with my project.
exactly what I was looking for without extra unecesary libraries or complexity
And then you can manipulate context as a react state? How do you send data to views? Also there is project that integrates react and Django but I forgot it's name
In my setup I just used normal Django flow with forms and POST requests for data updates and React mostly for read-only templates, so once page will be reloaded by POST request, there will be a new data in your hook.
To speed things up, you can either enforce caching policy for JS files, or update state locally and send new data to the API.
Is there a way to do this as single page. For example when you goes to another page music doesn't get restarted or paused?
Haven't researched into that, maybe you can start with rendering and then transition to PWA, but this way you won't be able to pass data in templates 🤷
You sir, are a genius.