Django - GraphQL - React Integration Tutorial: Part-3 + Image
Pixly Logo
Django - GraphQL - React Integration Tutorial: Part-3

Django - GraphQL - React Integration Tutorial: Part-3

2019-11-27

 

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 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 file, 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 ultimate bundle process with webpack. 

Visual representation of webpack process

 

 

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 use 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

Index file is the root file of our app meaning that all our code will 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 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 requesting address will be the address of our Django server.

 

After than 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 open our page, switch component from react-router-dom will look the url. Then try to match path of route components with this url, if any, then the matched component in the route will be rendered.

In ideal scenario, when a user open our homepage, switch function will match the main page component. Then the query in the main page will make request to the server. If query will be successful, main page will render the data and 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, movie page render 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 different page for components. However, because of this project is small, writing in them in 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!!!

Opening page: list of movies

 

 

When we click any of the movies, you will see that the url address will be changed. Let's click

Movie page

 

We created a simple single-page-application. Now, the last part of this tutorial will be make this app works seamlessly with our django project.

Now you can stop the webpack server from corresponding terminal screen.

 

 

 

Step-10: Build production environment

 

Now, We can build our app for 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, and index.html file in the templates directory. 

Django usage of webpack outputs

 


 


In the beginning of this series, I said that we will develop this project on two server, 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.