Skip to content

Instantly share code, notes, and snippets.

@Kanaderu
Forked from genomics-geek/README.md
Created November 10, 2019 19:16
Show Gist options
  • Save Kanaderu/a279dc06a6344f1c279b6ba682b4a23d to your computer and use it in GitHub Desktop.
Save Kanaderu/a279dc06a6344f1c279b6ba682b4a23d to your computer and use it in GitHub Desktop.
Setup ReactJS, Redux, Webpack with React Hot Reloading on an existing Django Project

Setting up ReactJS/Redux using Webpack for an existing Django project

This guide will help set up your django project to use ReactJS

1. Install Python dependencies

Add pip requirements to our django project:

  • django-webpack-loader==0.4.1 ( Connects Django project with Webpack)
  • django-cors-headers==2.0.2 (Allows us to easily customize CORS settings)

Add new dependencies to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'corsheaders',
    'webpack_loader',
    ...
]

Adjust settings for new apps

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

2. Adjust STATICFILES

This where our ReactJS project will live.

Create a frontend directory

mkdir -p frontend

Add new path to STATICFILES_DIRS

STATICFILES_DIRS = [
     str(APPS_DIR.path('static')),
     str(ROOT_DIR.path('frontend')),
]

3. Install Node dependencies

This will install all the Javascript libraries we will use

Setup Node

Run npm init and follow the instructions. Just fill in the information for your project.

Install webpack packages

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

Install babel compiler and plugins

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

Install additionaly helpful libs

  • 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

Install ReactJS and Redux and associated plugins

  • ReactJS is a JS lib for building UIs
  • Redux is a predictable state container for JS apps

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

Install ESLint packages

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

Install Unit Testing packages

  • Karma - Test runner
  • Mocha - Testing framework
  • expect - lets you write better assertions
npm install --save-dev karma mocha expect deepfreeze karma-mocha karma-webpack karma-sourcemap-loader karma-chrome-launcher karma-babel-preprocessor enzyme

4. Setup Webpack module bundler

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.

Create our webpack dir

mkdir -p frontend/webpack/

Create our base webpack config

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;

Create the entry point for our front-end project

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

Create Node shortcuts

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"
  }

5. Wireup Django/ReactJS

This will create a django template where ReactJS can inject into.

Create django templates

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>

Adjust main URLConf in Django

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')),

6. Create your React/Redux app

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 :).

Create ReactJS project structure

  • 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 Redux Store

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 React Root

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;

Create Redux combined reducers

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;

Create React project entry point

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); });
}

Create client-side routing

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;

7. Setup JS Unit testing

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);
  });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment