# 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 ```python INSTALLED_APPS = [ ... 'corsheaders', 'webpack_loader', ... ] ``` ### Adjust settings for new apps Add CORS settings in your base settings: ```python # 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: ```python # 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: ```python # 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](https://github.com/ottoyiu/django-cors-headers#setup). I personally split them up like below. This allows us to meet the criteria of both CORS and Whitenoise :). In Base settings: ```python # 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: ```python # 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 ```python 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](https://webpack.github.io/). 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](http://es6-features.org/#Constants). ``` 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](https://github.com/mzabriskie/axios) - Helpful to interact with an API + [lodash](https://lodash.com/) - Helpful extra methods that are missing in JS + [victory](http://formidable.com/open-source/victory/) - Amazing library for charting ``` npm install --save-dev axios lodash victory ``` ### Install ReactJS and Redux and associated plugins + [ReactJS](https://facebook.github.io/react/) is a JS lib for building UIs + [Redux](http://redux.js.org/) 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](https://github.com/airbnb/javascript) ``` 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](https://karma-runner.github.io/1.0/index.html) - Test runner + [Mocha](https://mochajs.org/) - Testing framework + [expect](https://github.com/mjackson/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](https://webpack.github.io/). 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: ```javascript 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: ```javascript 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: ```javascript 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: ```json "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 `/templates/index.html` and add the following contents: ```html {% extends 'react-base.html' %} {% load staticfiles %} {% load render_bundle from webpack_loader %} {% block body %}
{% endblock body %} {% block javascript %} {% render_bundle 'main' %} {% endblock javascript %} ``` Create file `/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. ```html {% load staticfiles %} {% block head %} {% block head_title %}{% endblock head_title %} {% block stylesheets %} {% endblock stylesheets %} {% endblock head %} {% block body %} {% endblock body %} {% block javascript %} {% endblock javascript %} {% block google_analytics %} {% endblock google_analytics %} ``` ### Adjust main URLConf in Django We will adjust our Django urls to allow client-side routing. Add the following routes to your URLs: ```python url(r'^$', TemplateView.as_view(template_name='index.html')), url(r'^app/(?P.*)$', TemplateView.as_view(template_name='index.html')), url(r'^pages/(?P.*)$', 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`: ```javascript if (process.env.NODE_ENV === 'production') { module.exports = require('./ConfigureStore.production'); } else { module.exports = require('./ConfigureStore.development') } ``` Contents of `ConfigureStore.development.js`: ```javascript 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`: ```javascript 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`: ```javascript if (process.env.NODE_ENV === 'production') { module.exports = require('./Root.production'); } else { module.exports = require('./Root.development') } ``` Contents of `DevTools.jsx`: ```javascript 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( ); export default DevTools; ``` Contents of `Root.development.jsx`: ```javascript 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 (
); }; Root.propTypes = { store: PropTypes.object.isRequired, history: PropTypes.object.isRequired }; export default Root; ``` Contents of `Root.production.jsx`: ```javascript 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 ( ); }; 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: ```javascript 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: ```javascript 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( , 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. ```javascript import React from 'react'; import { Route } from 'react-router'; const HelloWorld = () =>
Hellow World!
; const routes = (
); 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: ```javascript 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: ```javascript var testsContext = require.context('.', true, /.spec$/); testsContext.keys().forEach(testsContext); ``` Create example test `frontend/js/src/example.spec.js`. Contents: ```javascript import expect from 'expect'; describe('Something abstract', () => { it('works', () => { expect(1).toEqual(1); }); }); ```