In the previous part, we build an API that is responsible for communication of Django project and react app. The third part of the tutorial is creating a single page application with React.
Here the links of all parts of this tutorial:
Create React App from Scratch
Step-1: Configuring Environment
(Note: if you already installed the node, you can skip this part)
We will use Node backend for the development environment. Therefore we need to install Node and Node package manager npm. In order to prevent possible dependency problems, I need to create a clean node environment. I will use NVM which is Node version manager and it allows us to create isolated Node environments.
# install node version manager
wget -qO- <https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh> | bash
# check installation
command -v nvm #should prints nvm
# install node
nvm install node #"node" is an alias for the latest version
# use the installed version
nvm use node
# prints Now using node v13.1.0 (npm v6.12.1)
# Note:versions can be different
Step-2: Create frontend director
# go django root directory
# create frontend directory
mkdir FRONTEND
cd FRONTEND
# create a node project
npm init
# you may fill the rest
Step-3: Install dependencies
# djr/FRONTEND
# add core react library
npm install react react-dom
# add graphql client-side framework of Apollo and parser
npm install apollo-boost @apollo/react-hooks graphql
# add routing library for single page app
npm install react-router-dom
# DEVELOPMENT PACKAGES
# add babel transpiler
npm install -D @babel/core @babel/preset-env @babel/preset-react
# add webpack bundler
npm install -D webpack webpack-cli webpack-dev-server
# add webpack loaders and plugins
npm install -D babel-loader css-loader style-loader html-webpack-plugin mini-css-extract-plugin postcss-loader postcss-preset-env
Step-4: Create necessary files
# djr/FRONTEND
# create source folder
mkdir src
#create webpack config file
touch webpack.config.js
# get into src folder
cd src
# djr/FRONTEND
# create html file for developing with react
touch index.html
# our react app's root file
touch index.js
# our app file and styling
touch App.js
touch App.css
# query file
touch query.js
Step-4: Package.json file
Your package.json file should look like this.
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --open --hot --mode development",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
},
"postcss": {
"plugins": {
"postcss-preset-env": {}
}
},
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
"apollo-boost": "^0.4.4",
"graphql": "^14.5.8",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2"
},
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^1.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
}
}
Step-4: Webpack configuration
What is webpack?
Webpack is a module bundler and a task runner. We will bundle all our javascript application including CSS styling into two javascript files, if you prefer you can output only one file. Due to the rich plugins, you can also do many things with webpack like compressing with different algorithms of your file, eliminate unused CSS code, extracting your CSS to different files, uploading your bundle to cloud etc...
This image below is a visual representation of the ultimate bundle process with webpack.
We decided to make two different webpack setting in single file; one for production and for development. This is the minimal configuration of webpack and it is not optimized.
const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");
// checks if it is production bundling or development bundling
const isEnvProduction = process.argv.includes("production")
// our root file
const entrypoint = './src/index.js'
const productionSettings = {
mode: "production",
entry: entrypoint,
output: {
// output directory will be the root directory of django
path: path.resolve(__dirname, '../'),
// this is the bundled code we wrote
filename: 'static/js/[name].js',
// this is the bundled library code
chunkFilename: 'static/js/[name].chunk.js'
},
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
name: true,
},
runtimeChunk: false,
},
devServer: {
historyApiFallback: true,
stats: 'normal',
},
module: {
rules: [
{
// for bundling transpiled javascript
test: /\\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
}
},
{
test: /\\.css$/i,
use: [
// IMPORTANT => don't forget `injectType` option
// in some cases some styles can be missing due to
// inline styling.
{ loader: 'style-loader', options: { injectType: 'styleTag' } },
"css-loader"
],
},
]
},
plugins: [
new HtmlWebPackPlugin({
// this is where webpack read our app for bundling
template: "./src/index.html",
// this is emitted bundle html file
// django will use this as template after bundling
filename:"./templates/index.html"
}),
]
};
const devSettings = {
mode: "development",
entry: entrypoint,
output: {
path: path.resolve(__dirname, './build'),
publicPath: "/",
filename: 'static/js/bundle.js',
chunkFilename: 'static/js/[name].chunk.js',
},
devtool: 'inline',
devServer: {
historyApiFallback: true,
contentBase: './dist',
stats: 'minimal',
},
module: {
rules: [
{ // using transpiled javascript
test: /\\.m?js$/,
exclude: /(node_modules|bower_components)/,
include: path.resolve(__dirname, 'src'),
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-proposal-object-rest-spread"],
// for fast development environment
// enable caching transpilation
cacheDirectory: true
},
}
},
{
test: /\\.css$/i,
use: [
// IMPORTANT => don't forget `injectType` option
// in some cases some styles can be missing due to
// inline styling.
{ loader: 'style-loader', options: { injectType: 'styleTag' } },
"css-loader",
'postcss-loader'
//{ loader: 'sass-loader' },
],
},
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
})
]
};
module.exports = isEnvProduction ? productionSettings : devSettings;
Step-5: Create Index Html file
When we are developing frontend, our react app render all our javascript code to this HTML file located in the src folder. Also when we build our code for production (bundling), webpack will be using this HTML as a template.
However, It is important to say that Django will not use this HTML file as a template. This is the HTML entry point of webpack and Django will use the output of bundle.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Django-React Integration Tutorial"/>
<title>Django React Integration</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Step-6: Create the root file of react application: index.js
The index file is the root file of our app meaning that all our code will be connected to this root file. The other tutorials or react boilerplates generally use this file for only render function of ReactDOM and leave it very small and clear. Writing this index file as it is is a totally a personal choice.
What we will do is create an Init component that will initialize the API framework and routing library.
We will wrap our App file with API framework so that all our components will be in the context of our API. The Apollo Provider expects an Apollo client which has the information of requested address will be the address of our Django server.
After then we will wrap our App file again with the router component namely Browser Router. This will allows us routing without rendering all the page when the URL of the address bar changes.
At the end of the file, you will see the render function of ReactDOM which accepts our root component, which is Init component in our case, and the DOM element that our app will be rendered in there.
// djr/FRONTEND/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter } from "react-router-dom"
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
/*
our api client will make request to thils adress.
at ~/Blog/djr/djr/urls.py
*/
const apiclient = new ApolloClient({
uri: '<http://127.0.0.1:8000/graphql>',
});
const Init = () => (
<ApolloProvider client={apiclient}>
<BrowserRouter>
<App />
</BrowserRouter>
</ApolloProvider>
)
ReactDOM.render( <Init />, document.getElementById('root'))
Information about the application
Now, we are ready to create our simple movie app.
Our app has two different screens;The main page which lists all movies in the database with small information and the movie page will show specific movie with more information.
Technical explanation
When a user first opens our page, switch component from react-router-dom will look the URL. Then try to match the path of route components with this URL, if any, then the matched component in the route will be rendered.
In the ideal scenario, when a user opens our homepage, switch function will match the main page component. Then the query in the main page will make a request to the server. If the query will be successful, the main page will render the data and the user will see the small movie cards. When the user click any of these cards, link component from react-router-dom will redirect user to the movie page of this specific movie. The url will be changed. Then switch function looks and match this URL with the movie page component. This time query in the movie page will request to the server with the given slug argument that was captured from URL. The server will look at this argument and check its database, if any match, then the movie information will be sent back to the client. Finally, the movie page renders the movie information with this data.
Note: It is better to load all information at first then render movie page with this data. It is not a good option to make a second request with this small data. Because of the need for explanation, this approach was selected.
Step-7: Create App.js file
// djr/FRONTEND/src/App.js
import React from "react";
import { Route, Switch, Link } from "react-router-dom"
import "./App.css"
const App = () => {
return (
<div className="App">
<Switch>
<Route exact path="/" component={MainPage} />
// colon before slug means it is a dynamic value
// that makes slug parameter anything
// like: /movie/the-matrix-1999 or /movie/anything
<Route exact path="/movie/:slug" component={MoviePage} />
</Switch>
</div>
)
}
export default App
Step-7: Write client-side queries
Before creating our main page and movie page components, we should first create our API queries.
// djr/FRONTEND/src/query.js
//import our graph query parser
import gql from "graphql-tag";
// our first query will requests all movies
// with only given fields
// note the usage of gql with jsvascript string literal
export const MOVIE_LIST_QUERY = gql`
query movieList{
movieList{
name, posterUrl, slug
}
}
`
// Note the usage of argument.
// the exclamation mark makes the slug argument as required
// without it , argument will be optional
export const MOVIE_QUERY = gql`
query movie($slug:String!){
movie(slug:$slug){
id, name, year, summary, posterUrl, slug
}
}
`
Step-7: Creation of page components
Normally, it is better to create a different page for components. However, because this project is small, writing in them in the App file will be no problem.
Import Apollo query hook and our queries to App file
// djr/FRONTEND/src/App.js
// import Apollo framework query hook
import { useQuery } from '@apollo/react-hooks'; // New
// import our queries previously defined
import { MOVIE_QUERY, MOVIE_LIST_QUERY } from "./query" //New
Main page component
// djr/FRONTEND/src/App.js
const MainPage = (props) => {
const { loading, error, data } = useQuery(MOVIE_LIST_QUERY);
// when query starts, loading will be true until the response will back.
// At this time this will be rendered on screen
if (loading) return <div>Loading</div>
// if response fail, this will be rendered
if (error) return <div>Unexpected Error: {error.message}</div>
//if query succeed, data will be available and render the data
return(
<div className="main-page">
{data && data.movieList &&
data.movieList.map(movie => (
<div className="movie-card" key={movie.slug}>
<img
className="movie-card-image"
src={movie.posterUrl}
alt={movie.name + " poster"}
title={movie.name + " poster"}
/>
<p className="movie-card-name">{movie.name}</p>
<Link to={`/movie/${movie.slug}`} className="movie-card-link" />
</div>
))
}
</div>
)
}
Movie page component
The Browser router component that we define in the index.js set some routing properties on components. You can see them with printing props.
// djr/FRONTEND/src/App.js
const MoviePage = (props) => {
// uncomment to see which props are passed from router
//console.log(props)
// due to we make slug parameter dynamic in route component,
// urlParameters will look like this { slug: 'slug-of-the-selected-movie' }
const urlParameters = props.match.params
const { loading, error, data } = useQuery(MOVIE_QUERY, {
variables:{slug:urlParameters.slug}
});
if (loading) return <div>Loading</div>
if (error) return <div>Unexpected Error: {error.message}</div>
return (
<div className="movie-page">
<Link to="/" className="back-button" >Main Page</Link>
{data && data.movie &&
<div className="movie-page-box">
<img
className="movie-page-image"
src={data.movie.posterUrl}
alt={data.movie.name + " poster"}
title={data.movie.name + " poster"}
/>
<div className="movie-page-info">
<h1>{data.movie.name}</h1>
<p>Year: {data.movie.year}</p>
<br />
<p>{data.movie.summary}</p>
</div>
</div>
}
</div>
)
}
Step-8: Add styles
You can copy them to App.css
/* djr/FRONTEND/src/App.css */
html, body {
width:100vw;
overflow-x: hidden;
height:auto;
min-height: 100vh;
margin:0;
}
.App {
position: absolute;
left:0;
right:0;
display: flex;
min-width: 100%;
min-height: 100vh;
flex-direction: column;
background-color: #181818;
/*font-family: "Open Sans", sans-serif;*/
font-size: 16px;
font-family: sans-serif;
}
/* MAIN PAGE */
.main-page {
position: relative;
display: flex;
flex-wrap: wrap;
min-height: 80vh;
background-color: #3f3e3e;
margin:10vh 5vw;
border-radius: 6px;
}
/* MOVIE CARD */
.movie-card {
position: relative;
width:168px;
height:auto;
background: #f1f1f1;
border-radius: 6px;
margin:16px;
box-shadow: 0 12px 12px -4px rgba(0,0,0, 0.4);
}
.movie-card:hover {
box-shadow: 0 12px 18px 4px rgba(0,0,0, 0.8);
}
.movie-card-image {
width:168px;
height:264px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.movie-card-name {
text-align: center;
margin: 0;
padding: 8px;
font-weight: bold;
}
.movie-card-link {
position: absolute;
top:0;
left:0;
right: 0;
bottom: 0;
}
/* MOVIE PAGE */
.back-button {
position: absolute;
left:10px;
top:10px;
width:120px;
padding: 8px 16px;
text-align: center;
background: #f1f1f1;
color:black;
font-weight: bold;
cursor:pointer;
}
.movie-page {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
min-height: 80vh;
margin:10vh 10vw;
border-radius: 6px;
}
.movie-page-box {
position: relative;
display: flex;
height:352px;
background-color: #f1f1f1;
}
.movie-page-image {
width:280px;
height:352px;
}
.movie-page-info {
position: relative;
display: flex;
flex-direction: column;
height:352px;
width: auto;
max-width: 400px;
padding: 16px 32px;
}
Step-9: Start development environment
Open two different terminal screen.
# in root directory of django project djr/
# make ready server for client requests.
python manage.py runserver
# in FRONTEND directory ~/Blog/djr/FRONTEND
# run react dev environment
npm run start
# this will probably open a browser page
# <http://localhost:8080/>
Voila!!!
When we click any of the movies, you will see that the URL address will be changed. Let's click
We created a simple single-page-application. Now, the last part of this tutorial will be making this app works seamlessly with our Django project.
Now you can stop the webpack server from the corresponding terminal screen.
Step-10: Build a production environment
Now, We can build our app for a production environment.
# in djr/FRONTEND
npm run build
When the bundling process is over, Django will use the new index.html file as a template which contains our client-side app. If you successfully build, you'll have two bundled javascript file in static folder of the root directory, an index.html file in the templates directory.
(Edit: The last line in the urls.py should start with re_path instead of path)
At the beginning of this series, I said that we will develop this project on two servers, but in the production environment, there will only one server.
Now Let's test it.
Please close open terminal sessions and re-open the Django server.
# in root directory
python manage.py runserver
# then open <http://127.0.0.1:8000/> on your browser.
Then It is working.
This tutorial series ended. I hope it will be useful for someone. Criticisms, feedbacks, and questions are welcome.
Finally, You can find all the code of this tutorial from here.