Having been very wary of the whole of the JavaScript world for the last couple of years (the constant churn of frameworks is one big joke right?), I was pretty wary of the world of Webpack. But I must say Webpack is pretty cool. Especially when you can transpile JavaScript from ES6 to ES5 into the final output bundle. JavaScript has finally become a good language to code in. Most Webpack tutorials online tend to focus on how to setup Webpack within a node.js environment, but there were very few resources on how to get Webpack / Hot reloading / React working with a server other than a node.js / Express server.

I spent a while time figuring out how to get Webpack setup with a barebones Sinatra server. My general proof of concept for the application architecture was that I would run a simple Sinatra server using Rackup and spawn a new webpack-dev-server instance to run the Webpack / React app on a separate process / port, and proxy requests to the webpack-dev-server port through my Sinatra app. 

Setting up the skeleton application

First off we need to do some setting up. This will include setting up the Ruby environment, the Rack config file (`config.ru`) and various other things. I'll focus on the server-side element first, as this is considerably easier to get your head around!

First off, we need to make a new directory to house our application:

mkdir sinatra-webpack && cd sinatra-webpack

Once we have done this, we need to create a couple of config files in the root of the directory (run the commands below consecutively):

touch Gemfile
touch index.html
touch server.rb
touch webpack.config.js
touch config.ru
touch .babelrc

Don't worry about the contents of each of these files for the meantime. I will guide you through the population of each one by one. But for now, we need to setup a simple Sinatra server which in turn will launch a separate webpack-dev-server process and also proxy requests through to the React app.

The general folder structure of the application will eventually look like the below:

The server

In our Gemfile, we need to specify these gems:

source 'https://rubygems.org'
ruby '2.3.1'

gem 'rack-proxy'
gem 'sinatra'
gem 'sinatra-contrib'

rack-proxy will help us proxy requests to the relevant route through to our React app and Sinatra is just a simple web server (way easier than Rails). Finally on this one, we need to  run bundle install to install the relevant gems.

Next up, we need to configure our config.ru file so that we can run the Sinatra app using the Rackup command line tool. The config.ru file is very simple, it basically does these things:

  • Configures a new AppProxy class which rewrites the HTTP_HOST value to localhost:8081 (which just happens to be the port my webpack-dev-server runs on)
  • Uses the URLMap feature to run the Sinatra server and the Webpack-dev-server instances simulataneously on the same port.

What this effectively means is that our Sinatra routes (e.g. /companies.json) will be namespaced to the /api route whereas the Webpack bundle will sit at /

Our config.ru file will now look like below:

require './server'
require 'rack-proxy'

class AppProxy < Rack::Proxy
  def rewrite_env(env)
    env['HTTP_HOST'] = 'localhost:8081'
    env
  end
end

run Rack::URLMap.new(
    '/api' => Server,
    '/' => AppProxy.new
)

Setting up server.rb

As you may have seen in the previous step detailing the contents of the config.ru file, we are require-ing ./server.rb. The Sinatra server is very simple, and only offers one endpoint which will return a JSON payload of "companies". The server also spawns a new process when Rackup is first initialized to run our webpack-dev-server instance. I use the port 8081 as this is what I have configured in my webpack.config.js file (we will go over this later).

require 'sinatra/base'
require 'sinatra/json'

class Server < Sinatra::Base
    pid = Process.spawn('./node_modules/.bin/webpack-dev-server')
    Process.detach(pid)
    puts "webpack dev server pid: #{pid}"

    get '/companies.json' do
        companies = []
        1.upto(50) do |i|
            companies << {
                name: "Company #{i}",
                id: i
            }
        end
        json :companies => companies
    end
end

At this point you should probably test that all of the moving parts are working, by running the rackup command and visiting http://localhost:9292/api/companies.json in your browser. A big lump of JSON should be served up. That is it for our server-side code! Now onto the client-side stuff...

Setting up Webpack

I guess the first thing we should do is populate index.html. It is very simple, and merely references the bundled version of the React app as bundled by Webpack:

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="root"></div>
        <script type="text/javascript" src="bundle.js" charset="utf-8"></script>
    </body>
</html>

Easy so far. Now we need to setup our package.json which will hold all of our JavaScript dependencies. You can use the npm init command here, or create the package.json file manually at the root of the application folder. My package.json has various dev dependencies, all of which should go in the devDependencies section (I would copy my dependencies exactly including version numbers to ensure you don't run into any problems). I only have two production dependencies (React and Reqwest):

{
  "name": "sinatra-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "dependencies": {
    "react": "^15.4.2",
    "reqwest": "^2.0.5"
  },
  "devDependencies": {
    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.22.0",
    "jsx-loader": "^0.13.2",
    "react-dom": "^15.4.2",
    "react-hot-loader": "^3.0.0-beta.6",
    "webpack": "^2.2.1",
    "webpack-dev-server": "^2.3.0",
    "webpack-module-hot-accept": "^1.0.4"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Adam Bull",
  "license": "ISC"
}

Once you have done this, make sure you have npm installed, and run npm install (or npm i for short) in the directory where the package.json file is.

The webpack.config.js file

Now, onto the beast. The webpack.config.js file is very complex and I personally don't think configuration of Webpack is very well documented. Anyway the general gist is that you configure an entry file which represents the entry point to your application's JavaScript / React code, and you configure an output directory which is where your bundled / minified / transpiled JavaScript will go every time you save a file and the app recompiles.

Webpack provides us with the ability to configure special "loaders" for certain file extensions. These loaders have special abilities to parse file contents, such as being able to transpile ES6 to ES5, parse React JSX syntax etc etc. You can specify which loaders you want to enact on which file types in the module.loaders section. 

The entry section also has two extra entries before the entry.js file is specified; these enable hot reloading of JavaScript modules (i.e. React components) as and when they are edited and changes are saved.

const webpack = require('webpack');

module.exports = {
    context: __dirname,
    entry: [
      'webpack-dev-server/client?http://0.0.0.0:8081',
      'webpack/hot/only-dev-server',
      './app/entry.js'
    ],
    output: {
        path: __dirname + '/dist',
        filename: 'bundle.js'
    },
    resolve: {
      extensions: ['.js', '.jsx'],
    },
    module: {
      loaders: [
        { 
          test: /\.js$/,
          exclude: /(node_modules|bower_components)/,
          loaders: ["jsx-loader", "babel-loader"]
        },
        { 
          test: /\.jsx$/,
          exclude: /(node_modules|bower_components)/,
          loaders: ["babel-loader"]
        }
      ]
    },
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ]
};

Configuring your .babelrc file

Finally, for a little bit of sugar, we need to populate our .babelrc file which will tell the Babel transpiling tool what JavaScript variants we are expecting to process when compiling the Webpack bundle:

{
  "presets": ["es2015", "react"],
  "plugins": ["react-hot-loader/babel"]
}

The last piece of the puzzle: React

Finally, I guess we should make a bit of a toy React app inside of the app/ folder. This application is going to be very simple (no SCSS compilation or anything fancy like that), and will consist of two files: entry.js (the entry point to the application in the webpack.config.js file) and app.jsx (our top level React component written in ES6 nomenclature).

The entry.js file looks like so:

import App from './app';
import React from 'react';
import { render } from 'react-dom';

render(<App />, document.getElementById('root'));

We are simply importing the app.jsx file from within the same folder (as this is what we want rendered), importing React from our node_modules directory and importing the render method from React-DOM (this allows us to communicate with the DOM).

The React component

The component I created for the purposes of this tutorial simply retrieves the list of companies from the companies.json endpoint on my Sinatra server and renders it as a list on the page. I use the lightweight reqwest library to do this:

import React from 'react';
import reqwest from 'reqwest';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
        companies: []
    };
  }

  componentDidMount() {
      let context = this;
      reqwest('/api/companies.json', function (res) {
      context.setState({
        companies: res.companies          
      })
    })
  }

  render() {
      let companies = this.state.companies.map((item) => {
          return (
              <div className="company" key={item.id}>
                  {item.name}
              </div>
          );
      });

    return (
      <div className="companies">
          {companies}
      </div>
    );
  }
}

export default App;

That should be it! You have a fairly nice setup with Webpack and you can write sweet ES6 React components, and you can use Sinatra.

You should be able to access the Sinatra / Webpack application by visiting http://localhost:9292 where 9292 is the port of the Sinatra instance, and the page should look like this:

The full source code can be found at https://bitbucket.org/adaam2/sinatra-webpack. If you want to clone this repository via git, you can do so with the following command:

git clone https://[email protected]/adaam2/sinatra-webpack.git my-webpack-project

Addendum

I have recently updated the Bitbucket repository with a basic implementation of SCSS compilation. You can see this commit here.