The Lobby Page…with React
If you’re unfamiliar with React, it’s essentially a front-end library that acts as the “view” layer for web applications (and native mobile with React Native). With it, developers can create components that can manage their own state, actions and UI elements. Combining small React components together can allow you to build fast, reactive, yet complex user interfaces. Using React with Django isn’t as straightforward as just using the standard Django templates with a little javascript, though. We’ll need to bring in a few libraries and tools to get the job done.
Node – NPM
We’ll start with Node and it’s package manager, npm. If you’ve only worked with Django in the past, you may not have had a chance to play with npm. It can be compared to pip in some ways, as it installs the requested versions of libraries and frameworks. Similar to the requirements.txt file that is commonly used to track and install python packages in pip, npm uses a packages.json file in much the same way.
If you’re not sure if you have Node and npm installed, you can check the versions with in your terminal by running node -v (should be at least v0.10.32)and npm -v (should be at least v2.1.8). If you don’t have either of these, you can get the installation info here.
So, once you have confirmed that you have those and they’re relatively up-to-date, let’s move on to create the package.json file in our project.
In your terminal, at the root of the project, type:
1 |
npm init |
Just accept the defaults for now and then you should have a shiny new package.json file. Next, we’ll install a few packages:
- React
- Webpack – packages up all of the React JSX files into a single file in browser-readable JS format.
- Babel – Used by webpack and does the actual converting of the JSX to JS
- Babel-Loader – webpack plugin to allow Babel
- React-Websocket – Websockets made easy for React
- Babel Presets: presets for es2015 and React
In your terminal:
1 |
npm install --save-dev react react-dom webpack webpack-bundle-tracker babel-core babel babel-loader babel-preset-es2015 react-websocket babel-preset-react jquery |
With webpack installed now, we need to create config file. Create a webpack.config.js file in the root of your project (same directory as manage.py). Add the following to that file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// load the needed node modules var path = require("path"); var webpack = require('webpack'); var BundleTracker = require('webpack-bundle-tracker'); // webpack project settings module.exports = { context: __dirname, entry: { lobby: './templates/components/lobby/index', }, output: { path: path.resolve('./static/bundles/'), filename: "[name]-[hash].js" }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), // don't reload if there is an error new BundleTracker({path: __dirname, filename: './webpack-stats.json'}) ], module: { loaders: [ { test: /\.jsx$/, exclude: /(node_modules)/, loader: 'babel', // 'babel-loader' is also a legal name to reference query: { presets: ['es2015', 'react'] } }, ] }, resolve: { modulesDirectories: ['node_modules'], extensions: ['', '.js', '.jsx'] }, } |
What we’ve added there are the instructions for what webpack should do when run. Key bits to note there are:
- Line 9-11: We’re telling it that we want a “lobby” bundle to be created from the JS and JSX files that will soon be in the url we’ve provided.
- Line 12-15: This is setting where we want the bundle to be created and how to name it.
- Line 25-36: This is the array (with only one item) that specifies the loader we’ll be using with webpack. We’re using babel, telling it to exclude files in node_modules, and the presets so babel know’s what type of files its processing.
Lobby Template and React Components
React is a flexible library when it comes to how you structure your project. Many devs will create a react component from which all other components are rendered, so the entire project has a single entry point. This works really well for single page applications (SPAs) for obvious reasons. In Django, this pattern doesn’t really fit because Django is all about having multiple pages being served up by their specific URL & View. So, we’re going to keep using that pattern: URL -> VIEW -> TEMPLATE, but we’ll then have the template load the React components it needs.
If you refer back to line 10 in webpack.config.js, you’ll see that we’re specifying our first bundle – lobby. We want to include all of our lobby front-end code in that location, which will include the Django html template that is loaded from the view we already created. That directory will additionally hold to all of the React component files that are lobby specific.
So, webpack will pack all of those lobby JSX into a single JS bundle file we can use. To easily choose the bundle we want to work with in a particular Django template, we can use the django-webpack-loader project. Let’s add that now:
1 |
pip install django-webpack-loader |
And add ‘webpack_loader’ to your INSTALLED_APPS (settings.py) after ‘channels’ in addition to some settings for the loader:
1 2 3 4 5 6 7 8 9 10 11 |
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'game', 'channels', 'webpack_loader' ] |
1 2 3 4 5 6 7 |
WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': '/static/bundles/', # end with slash 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json') } } |
Create a components directory inside templates. Inside it, create a lobby directory. In that directory, create the Django template file lobby.html. Your templates folder should now look like this:
Now, fill the new lobby.html file with this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{% extends "base.html" %} {% load render_bundle from webpack_loader %} {% block page_javascript %} {% render_bundle 'lobby' %} {% endblock %} {% block main_content %} <h3>Let's play Obstruction, <span class="text-primary" >{{request.user.username}}</span>!</h3> <hr> <div id="lobby_component"></div> {% endblock %} |
If you’ve worked with Django before, this all should look very standard. Some things to note:
- Line 2: Loading the render_bundle template tag from the webpack_loader Django app we installed earlier.
- Line 7: Using that tag to drop the lobby bundled up JS that webpack will create.
- Line 12: Regular old Django… React is just supplementing the template, we can always just use standard Django template features around those components.
- Line 14: The div element that React will fill with the components.
That’s it for our lobby component’s root template. Now, we’re going to work with React.
Again looking at line 10 in the webpack.config.js file, you’ll see that the lobby bundle entry points to an index file. Each component folder and bundle needs this entry file, which handles the React imports and then renders the main component into the lobby_component div. This file also will be our first .jsx file.
JSX?
JSX, a React syntax that extends Javascript, is what most devs use when working with React. JS would work just fine, but you get additional benefits with JSX including: ES6 features and syntax and, most importantly, the ability to cleanly embed HTML directly in the script. It looks odd the first time you see it, but it really makes component building clean and easy.
Create the index.jsx file in the lobby directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React from 'react'; import LobbyBase from './LobbyBase.jsx' import ReactDOM from 'react-dom' import $ from 'jquery' // lobby socket url var lobby_sock = 'ws://' + window.location.host + "/lobby/" // preset the current_user var current_user = null // renders out the base component function render_component(){ ReactDOM.render(<LobbyBase current_user={current_user} socket={lobby_sock}/>, document.getElementById('lobby_component')) } render_component() |
While not a React component, we still used JSX syntax to take advantage of those features we mentioned above. Lines of note:
- Lines 1-4: ES6 imports instead of Node’s require for module importing
- Line 7: Websock url we’ll use to connect to the Channels lobby route we created earlier
- Line 9: Default the current_user to null until we do something to retrieve that a little later
- Line 12-14: Our first bit of React syntax. ReactDOM.render will render the base component on the page.
- Line 18: Calls the function that renders the component.
Let’s take a look at that what that line is rendering:
1 |
<LobbyBase current_user={current_user} socket={lobby_sock}/> |
This is the React syntax to display a component. The component must be imported into the current context, which we did on line 2. This syntax should be familiar as it’s basically just HTML with a tag name (the component) and attributes which are React props. You can think of React components as Javascript functions that take in parameters – the props. So the syntax above is a HTML-ish way to call the LobbyBase function, passing current_user and socket as parameters.
The {current_user} and {lobby_sock} look similar to the double curly braces of Django template variables, but in React the single curly braces allow you to embed javascript expressions or variables.
Lobby Base Component
Create a LobbyBase.jsx file in the lobby directory and add this as its content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import React from 'react'; import ReactDOM from 'react-dom'; import Websocket from 'react-websocket' import $ from 'jquery' class LobbyBase extends React.Component { constructor(props) { super(props); this.state = { player_game_list: [], available_game_list: [] } // bind button click this.sendSocketMessage = this.sendSocketMessage.bind(this); } componentDidMount() {} componentWillUnmount() { this.serverRequest.abort(); } handleData(data) { //receives messages from the connected websocket let result = JSON.parse(data) // we've received an updated list of available games this.setState({available_game_list: result}) } sendSocketMessage(message){ // sends message to channels back-end const socket = this.refs.socket socket.state.ws.send(JSON.stringify(message)) } render() { return ( <div className="row"> <Websocket ref="socket" url={this.props.socket} onMessage={this.handleData.bind(this)} reconnect={true}/> <span>Lobby Components will go here....</span> </div> ) } } LobbyBase.propTypes = { socket: React.PropTypes.string }; export default LobbyBase; |
There’s a lot there, so let’s break down this into pieces…
At the top, we’re importing our React component, including two we will create in a moment.
1 |
class LobbyBase extends React.Component |
The above line shows how to define a component as an ES6 class. There is also a createClass method for ES5. Check out this excellent article for the differences, if you’re interested. In this case, we’ll go ES6.
1 2 3 4 5 6 7 8 9 10 |
constructor(props) { super(props); this.state = { player_game_list: [], available_game_list: [] } // bind button click this.sendSocketMessage = this.sendSocketMessage.bind(this); } |
The constructor method runs when the component is instantiated. Typically, this is where you handle defaulting state from the props in React.
State is very much like props in React, but it is local to, and controlled by, the component. State should hold data that can change in the component, not static values.
We’ll discuss state a little more later, but for now just note that we’ll default the state values in the constructor.
But the constructor is also useful for binding the class methods to this, the class instance. You’ll see this on line 16: this.sendSocketMessage = this.sendSocketMessage.bind(this) Here we’re binding the instance method sendSocketMessage to the class instance, this. This isn’t JSX specific, but just how Javascript works. To avoid manual binding, you could use the ES5 createClass method to build your components. The binding is automatic using that method.
The React team recommends using the constructor as the spot to do your binding. This ensures that binding won’t unnecessarily run multiple times. But you don’t have to bind there, and as I’ll do often in this tutorial, I’ll break from the recommended to show other ways to achieve things on purpose. If you take a look at Line 45, you’ll see we bind this to a class method.
Speaking of class methods, you’ll see there are several in our component’s class alongside a couple of custom methods. React has “lifecycle methods” that we can use to tap into spots of the component’s lifecycle, just as we did with the contructor. The others we use in this component are:
- componentDidMount – invoked immediately before mounting occurs and it runs before render
- componentWillUnmount – not invoked when first instantiated, but will be immediately after the component is updated.
- render – a required method, returns the component
The custom methods we have currently are: sendSocketMessage and handleData. These are our handlers for sending and receiving messages over the websocket, which you can see will be rendered by the render method as a React component.
Also, you’ll find below the class that we’re setting a propTypes property on the component class. This helps with type-checking to ensure that the props coming in are the types expected by the component. Our only prop we’ve defined here is the socket url, which needs to be a string.
And finally, we’re exporting the module at the bottom of the file, which allows it to be imported in index.jsx
Webpack It Up
Webpack is now configured to look at our lobby component directory, pack up those files into a bundle and deliver it to our static files directory. We now have files to pack in that folder, so let’s do that now with:
1 |
./node_modules/.bin/webpack --config webpack.config.js |
If all goes well, you should have some details of the webpack run in your terminal with no errors. You should also now see a file in your /static/bundles/ directory. If you’re interested, view the contents of this file and you’ll see how Webpack and babel put it all together.
Watch Mode – a nice feature of webpack is the watch mode you can trigger. This will allow webpack to continually watch for changes in any of your entry folders and compile on the fly. To do this, add --watch at the end of the command we ran above. If you don’t use –watch, you’ll need to manually run the command each time you make a change to any JSX file.
If everything goes well with webpack, run your project again with: python manage.py runserver 8080 and then log into your app. If it doesn’t send you to the lobby page, you can click “Lobby” in the top menu. If everything worked, you should see something like:
Here you can see the mix of Django template and React: The template (lobby.html) renders out the “Let’s play…” line using the {{request.user.username}} from Django. Under that, you’ll see the “Lobby Components will go here…” line, which you’ll find in your LobbyBase component.
Note: You will need to be logged in to see the lobby.
hello,
when I run your code, but the web don’t show the game, what’s happend?
Are you going through the tutorial? Or are you running the example project from github?
Do you have a similar project but with channels 2.0? Do you plan to update this one?
Yes, I’m actually working on one now. Hopefully I’ll have it ready in October, but my free time is tight right now.
I am getting an error, “ImportError: cannot import name ‘Group'”, while trying to import Group from channels in models.py. I am using channels 2.0.2. Please help.
Yeah, this tutorial is based on a very early version of channels – 0.17, I believe. Version 2.0.2 has quite of few changes from that old version, including requiring Python3. Here is an issue on the official github that deals with the error you’re seeing: https://github.com/django/channels/issues/989
I will try to get the tutorial updated to Django 2.0, Python 3, and the latest channels version soon.
Hey,
I’m trying to follow your tutorial, but I keep running into the following error:
OSError at /lobby/
Error reading [PATH]\webpack-stats.json. Are you sure webpack has generated the file and the path is correct?
Any idea how to fix it?
Sorry, I didn’t reply sooner. If the file is being generated, check your STATS_FILE setting under WEBPACK_LOADER in settings.py. It’s possible that it isn’t pointing to the correct location for the file.
If the file isn’t being generated, webpack may not be running properly. Are you seeing errors when you run wepback?
That is very good, I was making similar stuff as a video on my language and this sharing is very useful.
Power of sharing, thanks again.
When will the index.jsx be called ?
The index.jsx files are bundled with webpack (set on lines 8 and 9 in webpack.config.js). Those are the bundles that are loaded with the Django Webpack Loader and rendered on the related html pages (lobby.html and game.html).
I got a few questions to make the game a little bit more complex
Is it possible to allow to the user give a name for the room?
Is it possible to allow more than 2 players? (obviously adding the correct img stuff)
If the answer from above is yes, can the user choose a “limit” for ppl to connect before starting the game?
Cheers!
Yes, each of those suggestions would work just fine:
Thanks for this django/react tutorial. It is awesome helping me get my head around React and putting all the pieces together!!
Great to hear! I’m glad you found it useful. Thanks for letting me know.
Thank you so much for writing this tutorial!
From your description, it seems like moves and player messages should update automatically, however when I run it, they only update after a manual refresh of the page. Am I misunderstanding how the website works?
Thanks
The log should update when new chat messages are created or moves are made. I would check to make sure the game.send_game_update() method is being called at the appropriate times. Also, check the browser console for any errors on the client-side. That may give you more of a clue to the issue as well.
Thanks for writing this tutorial! However, some things that may be confusing to a beginner:
startapp game
is going to create a game/views.py by default, and anyone that forgets to move views.py is going to have a package conflict. Also,from views import *
inside __init__.py should befrom .views import *
just to avoid any namespace mismatches. I recommend you keep the default views.py.Thanks for your comments, Daniel. Yes, I thought that splitting the views could be confusing, but it’s something I like to do to organize views of different types. So here, I didn’t want to combine the DRF API views with the “standard” Django views. Also, the tutorial wasn’t exactly intended for beginners, but maybe I can clarify the views split a little more in the post. Also, thanks for catching the import fix. I had already updated the imports in the git project, but missed it in the post.
Hello! Great tutorial! I have a question, I want to use foundation-sites in my project. I am following you tutorial and instead of using bootstrap precompiled css I would like to install foundation with bower maybe? I know how to use the “foundation new” command to create a new project but I would like not to create a new project but integrate foundation sites with mine!
Hey David, sorry I haven’t worked with Foundation yet. But it looks like you can just use the CSS itself and avoid the CLI site generation: http://foundation.zurb.com/sites/download/
You could just include this as you would any other CSS, and my guess is that if you install the full Foundation package with NPM, you could just reference the CSS there as well.
not sure if it’s a django versioning thing or what, but on page 1 of this tutorial you are no longer allowed to specify views with strings and they must be callable, suggested edit follows:
original:
from django.conf.urls import url
from django.contrib import admin
from game.views import *
urlpatterns = [
url(r’^admin/’, admin.site.urls),
url(r’^register/’, CreateUserView.as_view()),
url(r’^login/$’, ‘django.contrib.auth.views.login’, {‘template_name’: ‘login.html’}),
url(r’^logout/$’, ‘django.contrib.auth.views.logout’, {‘next_page’: ‘/’}),
url(r’^$’, HomeView.as_view())
]
edit:
from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth.views import login, logout
from game.views import *
urlpatterns = [
url(r’^admin/’, admin.site.urls),
url(r’^register/’, CreateUserView.as_view()),
url(r’^login/$’, login, {‘template_name’: ‘login.html’}),
url(r’^logout/$’, logout, {‘next_page’: ‘/’}),
url(r’^$’, HomeView.as_view())
]
Yep – you’re right, thanks for letting me know! I’ve updated the urls.py code.
Thank you so much, one of the most complete tutorials I have seen. Not all persons are willing to teach this things together, and the complexity of the scenario gives us good bases. This types of tutorials (even paid) are hard to find. Again, thank you.
Thank you for the nice comments! I’m glad you found the tutorial helpful.
Can you please show one example on how I can make api post call from react to django drf?
Is there any reason why all “post” calls are done via sockets not api in this tutorial?
You can see a few examples of calls from React to the DRF backend in my post. For example, take a look at the getGame() method on the GameBoard.jsx component. That method calls the DRF SingleGameViewSet endpoint to get game details.
And as I mentioned in the post, I tried to mix up different ways of getting data from the Django backend to the React frontend. I wanted to show different ways of achieving the same thing: sending data through the standard Django response via context, DRF calls to the backend, and Django Channels websocket calls. In reality, this isn’t how I would structure a production app, but I was hoping it would be informative. Hopefully not confusing at the same time.
Wow! Thanks again for sharing this tutorial. I am amazed by your generosity. The tutorial is intense.
Let me give some suggesting for how you can improve it. I found that very often the flow of tutorial is going from big concepts (code snippets) to a smaller ones. For example several times you are first putting together some views, react components or api_views and then go to show some serializers, consumers, routers, urls and so on. This can be sometimes confusing, since a student can receive error messages that those small parts are not yet existed. I think going from small concepts to bigger would be more easily to understand. Also, please check your github code. I think it does not work if you just download it and want to use. Several imports are configured improperly (signal in apps.py, for instance)
Thank you again!
Thanks for suggestions. Yes this was my first large tutorial so it definitely could be optimized and improved. I did the GitHub project well before the post, but it worked for me when I last tried it. It could be a python2/python3 import issue. I’ll update that tonight and get it working.
not sure if the instruction on page 8 is correct
class ClaimSquareView(APIView):
def get_object(self, pk):
try:
return Game.objects.get(pk=pk)
except Game.DoesNotExist:
raise Http404
def put(self, request, pk):
game = self.get_object(pk)
# update the owner
print(game)
return Response(serializer.errors)
– no import for Http404
– serializer is not defined
You’re right! That view isn’t even needed… I think I started going that direction to claim a square, but moved it to a Channels call using the consumer instead. I’ve removed that reference and the url reference.
Thank you very much for your suggestions and bug reports! I’ve added you to the “Thank you” section at the bottom of the post.
on page 7 views.py also should import
from django.contrib import messages
Added it, thank you.
Also, in my setup in view/__init__.py instead of
from views import *
from api_views import *
I need to enter
from .views import *
from .api_views import *
Yes this is probably because you’re on Python 3 and implicit relative imports like that won’t work. I’m on 2.7 and they work with it. Thanks for pointing that out. I’ll update the post to note this.
on the page 2, it is very important to highlights this setting in the settings.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, “static”),
]
it is not something that is added by default if start project with django-admin tools
Thanks, yes when I first talk about the settings file, I recommended overwriting all of the default code with what I show in the post. I’ll make sure that it’s more clear.
Thanks, confirm MIDDLEWARE_CLASSES fixed the issue with ‘AsgiRequest’ object has no attribute ‘session’
after following all instruction on the page 4, cannot login, getting this error
AttributeError at /login/
‘AsgiRequest’ object has no attribute ‘session’
Request Method: POST
Request URL: http://127.0.0.1:8080/login/
Django Version: 1.9.12
Exception Type: AttributeError
Exception Value:
‘AsgiRequest’ object has no attribute ‘session’
Exception Location: /home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/django/contrib/auth/__init__.py in login, line 101
Python Executable: /home/gideon/virtualenvs/vEnv_my_obstruct_game/bin/python
Python Version: 3.4.3
Python Path:
[‘/home/gideon/PycharmProjects/my_obstruct_game’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/setuptools-18.1-py3.4.egg’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/pip-7.1.0-py3.4.egg’,
‘/home/gideon/PycharmProjects/my_obstruct_game’,
‘/usr/lib/python3.4’,
‘/usr/lib/python3.4/plat-x86_64-linux-gnu’,
‘/usr/lib/python3.4/lib-dynload’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages’]
Server time: Mon, 6 Feb 2017 15:53:42 +0000
Traceback Switch to copy-and-paste view
/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/django/core/handlers/base.py in get_response
response = self.process_exception_by_middleware(e, request) …
▶ Local vars
/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/channels/handler.py in process_exception_by_middleware
return super(AsgiHandler, self).process_exception_by_middleware(exception, request)
One thing that could cause this with Django 1.9+ is if you have MIDDLEWARE instead of MIDDLEWARE_CLASSES in your settings.py file. Can you check that?
It seems there is an error in this instruction
npm install –save-dev react react-dom webpack webpack-bundle-tracker babel-core babel babel-loadernpm babel-preset-es2015 react-websocket babel-preset-es2015 babel-preset-react jquery
“babel-loadernpm” should read “babel-loader”
Yep, you’re right – a little copy-paste issue on my part. It’s fixed now. Thanks for letting me know!
I have not yet finished your tutorial, but for what I see I can tell you huge THANK YOU!
Thank you, I hope it you find it useful. Please let me know if you have any issues!