This guide will help set up your django project to use ReactJS
- django-webpack-loader==0.4.1 ( Connects Django project with Webpack)
- django-cors-headers==2.0.2 (Allows us to easily customize CORS settings)
INSTALLED_APPS = [
...
'corsheaders',
'webpack_loader',
...
]
Add CORS settings in your base settings:
# CORS CONFIGURATION
# ------------------------------------------------------------------------------
# https://github.com/ottoyiu/django-cors-headers#configuration
CORS_ORIGIN_ALLOW_ALL = True
Add Webpack loader settings for local and production settings.
Local:
# Webpack Loader by Owais Lane
# ------------------------------------------------------------------------------
# https://github.com/owais/django-webpack-loader
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'builds-dev/',
'STATS_FILE': os.path.join(str(ROOT_DIR), 'frontend', 'webpack', 'webpack-stats.dev.json')
}
}
Production:
# Webpack Loader by Owais Lane
# ------------------------------------------------------------------------------
# https://github.com/owais/django-webpack-loader
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'builds/',
'STATS_FILE': os.path.join(ROOT_DIR, 'frontend', 'webpack', 'webpack-stats.production.json')
}
}
Add CORS middleware according to these instructions. I personally split them up like below. This allows us to meet the criteria of both CORS and Whitenoise :).
In Base settings:
# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
SECURITY_MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
]
# This is required to go first! See: https://github.com/ottoyiu/django-cors-headers#setup
CORS_MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
DJANGO_MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
MIDDLEWARE = SECURITY_MIDDLEWARE + CORS_MIDDLEWARE + DJANGO_MIDDLEWARE
In Production settings:
# Use Whitenoise to serve static files
# See: https://whitenoise.readthedocs.io/
WHITENOISE_MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware', ]
# CORS Needs to go first! See: https://github.com/ottoyiu/django-cors-headers#setup
MIDDLEWARE = SECURITY_MIDDLEWARE + CORS_MIDDLEWARE + WHITENOISE_MIDDLEWARE + DJANGO_MIDDLEWARE
This where our ReactJS project will live.
mkdir -p frontend
STATICFILES_DIRS = [
str(APPS_DIR.path('static')),
str(ROOT_DIR.path('frontend')),
]
This will install all the Javascript libraries we will use
Run npm init
and follow the instructions. Just fill in the information for your project.
These are webpack packages. To read more about Webpack visit here. For now, I am using Webpack v1 because Webpack v2 seems a bit unstable and has changed a bit.
npm install --save-dev webpack webpack-dev-server webpack-bundle-tracker
These are packages that allow us to write our code using new ES6 JS. To read more, visit here.
npm install --save-dev babel-cli babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 css-loader style-loader
- axios - Helpful to interact with an API
- lodash - Helpful extra methods that are missing in JS
- victory - Amazing library for charting
npm install --save-dev axios lodash victory
Main ReactJS and Redux:
npm install --save-dev react react-dom redux
ReactJS and Redux plugins:
NOTE: Sticking with react-router@3 and react-router-redux@4 because new versions are in beta and unstable.
npm install --save-dev prop-types react-bootstrap react-fontawesome react-router@3 react-router-redux@4 react-cookie redux-logger redux-thunk react-redux semantic-ui-react
To allow for hot reloading of React components:
npm install --save-dev react-hot-loader@next redux-devtools redux-devtools-dock-monitor redux-devtools-log-monitor
I choose to use the airbnb JS standards, since they have clearly stated it here
npm install --save-dev eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-config-airbnb babel-eslint
npm install --save-dev karma mocha expect deepfreeze karma-mocha karma-webpack karma-sourcemap-loader karma-chrome-launcher karma-babel-preprocessor enzyme
This will help us bundle and compile all of our front-end stuff. To read more, visit here. In short, it bundles all JavaScript, JSX, etc. code for our project and manages our codebase to be split into bundles to be loaded in in our different environments.
mkdir -p frontend/webpack/
Create frontend/webpack/webpack.base.config.js
. The contents should be:
var path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [['es2015', { modules: false }], 'stage-2', 'react']
}
}
]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
resolve: {
modules: [
path.join(__dirname, 'frontend/js/src'),
'node_modules'
],
extensions: ['.js', '.jsx']
}
};
Create frontend/webpack/webpack.local.config.js
. The contents should be:
var path = require('path');
var BundleTracker = require('webpack-bundle-tracker');
var webpack = require('webpack');
var config = require('./webpack.base.config.js');
config.entry = {
main: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://0.0.0.0:3000',
'webpack/hot/only-dev-server',
path.join(__dirname, '../js/src/main/index')
]
};
config.devtool = 'eval';
config.output = {
path: path.join(__dirname, '../js/builds-dev/'),
filename: '[name]-[hash].js',
publicPath: 'http://0.0.0.0:3000/js/builds/',
};
config.module.rules[0].use[0].options.plugins = ['react-hot-loader/babel'];
config.plugins = [
new webpack.HotModuleReplacementPlugin(),
new BundleTracker({ filename: './frontend/webpack/webpack-stats.dev.json' }),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('development'),
BASE_URL: JSON.stringify('http://0.0.0.0:8000/'),
}
})
];
config.devServer = {
inline: true,
hot: true,
historyApiFallback: true,
host: '0.0.0.0',
port: 3000,
headers: { 'Access-Control-Allow-Origin': '*' }
};
module.exports = config;
Create frontend/webpack/webpack.production.config.js
. The contents should be:
var path = require('path');
var webpack = require('webpack');
var BundleTracker = require('webpack-bundle-tracker');
var config = require('./webpack.base.config.js');
config.entry = {
main: [
path.join(__dirname, '../js/src/main/index')
]
};
config.output = {
path: path.join(__dirname, '../js/builds/'),
filename: '[name]-[hash].min.js',
publicPath: '/js/builds/'
};
config.plugins = [
new BundleTracker({ filename: './frontend/webpack/webpack-stats.production.json' }),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production'),
BASE_URL: JSON.stringify('http://0.0.0.0/'),
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
}),
new webpack.optimize.UglifyJsPlugin({
mangle: false,
sourcemap: true,
compress: {
warnings: true
}
})
];
module.exports = config;
mkdir -p frontend/js/src/main
mkdir -p frontend/builds
mkdir -p frontend/builds-dev
touch frontend/js/src/main/index.jsx
touch frontend/builds/.gitkeep
touch frontend/builds-dev/.gitkeep
In package.json
add the following to scripts:
"scripts": {
"build-development": "webpack --config frontend/webpack/webpack.local.config.js --progress --colors",
"build-production": "webpack --config frontend/webpack/webpack.production.config.js --progress --colors",
"watch": "webpack-dev-server --config frontend/webpack/webpack.local.config.js",
"test": "./node_modules/karma/bin/karma start frontend/webpack/karma.config.js --log-level debug"
}
This will create a django template where ReactJS can inject into.
Create file <project-name>/templates/index.html
and add the following contents:
{% extends 'react-base.html' %}
{% load staticfiles %}
{% load render_bundle from webpack_loader %}
{% block body %}
<div id="react-root"></div>
{% endblock body %}
{% block javascript %}
{% render_bundle 'main' %}
{% endblock javascript %}
Create file <project-name>/templates/react-base.html
and add the following contents:
NOTE This includes some goodies like google fonts, bootstrap, and font-awesome :). Feel free to remove them.
{% load staticfiles %}
<html class="{% block html_class %}{% endblock html_class %}" lang="en">
<head>
<!-- Allows you to inject head elements here -->
{% block head %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="author" content="">
<meta name="description" content="">
<meta name="keywords" content="">
<title>{% block head_title %}{% endblock head_title %}</title>
<!-- Allows you to inject CCS here -->
{% block stylesheets %}
<!-- Third-party CSS libraries go here -->
<!-- Latest compiled and minified Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional Bootstrap theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- FontAwesome http://fontawesome.io/ -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<!-- Animate CSS https://github.com/daneden/animate.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">
<!-- Google Fonts https://fonts.google.com/ -->
<link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet">
<!-- Semantic UI for React https://react.semantic-ui.com/usage -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.2/semantic.min.css">
<!-- Project specific Stylesheets -->
{% endblock stylesheets %}
<!-- Title Icon -->
<!-- <link rel="shortcut icon" href=""> -->
{% endblock head %}
</head>
<body class="{% block body_class %}{% endblock body_class %}">
<!-- Allows you to inject body content here -->
{% block body %}
{% endblock body %}
<!-- Project specific JavaScript -->
<!-- Allows you to inject JavaScript here -->
{% block javascript %}
{% endblock javascript %}
<!-- Google Analytics goes here -->
{% block google_analytics %}
{% endblock google_analytics %}
</body>
</html>
We will adjust our Django urls to allow client-side routing. Add the following routes to your URLs:
url(r'^$', TemplateView.as_view(template_name='index.html')),
url(r'^app/(?P<route>.*)$', TemplateView.as_view(template_name='index.html')),
url(r'^pages/(?P<route>.*)$', TemplateView.as_view(template_name='index.html')),
This will set up your ReactJS project using Redux store to contain your application state. In addition, it will set up Hot Reloading and Redux DevTools :).
- main - Where the main app will live
- Root - Where we will set up the main App component
- Store - Where we will configure the Redux store
- reducers.js - Where we will combine all application reducers for the Redux store
- routes.jsx - Where we will establish the client side routing
mkdir -p frontend/js/src/main/
mkdir -p frontend/js/src/main/Root
mkdir -p frontend/js/src/main/Store
mkdir -p frontend/js/src/main/utils
touch frontend/js/src/main/reducers.js
touch frontend/js/src/main/routes.jsx
Create files:
touch frontend/js/src/main/Store/index.js
touch frontend/js/src/main/Store/ConfigureStore.development.js
touch frontend/js/src/main/Store/ConfigureStore.production.js
Contents of index.js
:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./ConfigureStore.production');
} else {
module.exports = require('./ConfigureStore.development')
}
Contents of ConfigureStore.development.js
:
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import DevTools from '../Root/DevTools';
import rootReducer from '../reducers';
const enhancer = compose(
// Middleware you want to use in development
applyMiddleware(thunk, createLogger()),
applyMiddleware(routerMiddleware(browserHistory)),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
);
// Function to call to configure Redux store
const configureStore = (initialState) => {
// Note: only Redux >= 3.1.0 supports passing enhancer as third argument
// See: https://github.com/rackt/redux/releases/tag/v3.1.0
const store = createStore(rootReducer, initialState, enhancer);
// Hot Reload reducers
// Note: Requires Webpack or Browserify HMR to be enabled
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers').default)
);
}
return store;
};
export default configureStore;
Contents of ConfigureStore.production.js
:
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const enhancer = compose(
// Middleware you want to use in production
applyMiddleware(thunk),
applyMiddleware(routerMiddleware(browserHistory)),
);
// Function to call to configure Redux store
const configureStore = (initialState) => {
// Note: only Redux >= 3.1.0 supports passing enhancer as third argument
// See: https://github.com/rackt/redux/releases/tag/v3.1.0
return createStore(rootReducer, initialState, enhancer);
};
export default configureStore;
Create files:
touch frontend/js/src/main/Root/index.js
touch frontend/js/src/main/Root/DevTools.jsx
touch frontend/js/src/main/Root/Root.development.jsx
touch frontend/js/src/main/Root/Root.production.js
Contents of index.js
:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.production');
} else {
module.exports = require('./Root.development')
}
Contents of DevTools.jsx
:
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
const DevTools = createDevTools(
<DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-w">
<LogMonitor />
</DockMonitor>
);
export default DevTools;
Contents of Root.development.jsx
:
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import PropTypes from 'prop-types';
import DevTools from './DevTools';
import routes from '../routes';
const Root = ({ store, history }) => {
return (
<Provider store={store}>
<div>
<Router history={history} routes={routes} />
<DevTools />
</div>
</Provider>
);
};
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default Root;
Contents of Root.production.jsx
:
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import PropTypes from 'prop-types';
import routes from '../routes';
const Root = ({ store, history }) => {
return (
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>
);
};
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default Root;
In frontend/js/src/main/reducers.js
, add the contents:
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
const rootReducer = combineReducers({
routing: routerReducer
});
export default rootReducer;
In frontend/js/src/main/index.js
, add the contents:
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import Root from './Root';
import configureStore from './Store';
const store = configureStore();
const history = syncHistoryWithStore(browserHistory, store);
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component store={store} history={history} />
</AppContainer>,
document.getElementById('react-root')
);
};
render(Root);
if (module.hot) {
module.hot.accept('./Root', () => { render(Root); });
}
In frontend/js/src/main/routes.jsx
, add the contents:
NOTE: This will have a Hello World place holder. NOTE: When running the application, edit Hello World and see it update in the browser automagically.
import React from 'react';
import { Route } from 'react-router';
const HelloWorld = () =>
<div>
Hellow World!
</div>;
const routes = (
<div>
<Route path="/" component={HelloWorld} />
</div>
);
export default routes;
This will setup karma as the test runner.
Create file frontend/webpack/karma.config.js
. It's contents should be:
var webpackConfig = require('./webpack.local.config.js');
webpackConfig.entry = {};
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha'],
// list of files / patterns to load in the browser
files: [
'../js/src/test_index.js'
],
// list of files to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'../js/src/test_index.js': ['webpack', 'sourcemap'],
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
autoWatchBatchDelay: 300,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
// Webpack
webpack: webpackConfig,
webpackServer: {
noInfo: true
}
});
};
Create test index frontend/js/src/test_index.js
. Contents:
var testsContext = require.context('.', true, /.spec$/);
testsContext.keys().forEach(testsContext);
Create example test frontend/js/src/example.spec.js
. Contents:
import expect from 'expect';
describe('Something abstract', () => {
it('works', () => {
expect(1).toEqual(1);
});
});