120

I'm creating an app that uses webpack-dev-server in development alongside react-router.

It seems that webpack-dev-server is built around the assumption that you will have a public entry point at one place (i.e. "/"), whereas react-router allows for an unlimited amount of entry points.

I want the benefits of the webpack-dev-server, especially the hot reloading feature that is great for productivity, but I still want to be able to load routes set in react-router.

How could one implement it such that they work together? Could you run an express server in front of webpack-dev-server in such a way to allow this?

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Nathan Wienert
  • 1,623
  • 3
  • 19
  • 25
  • I have an extremely hacky version of something here, but it is fragile and only allows simple routes to match: https://github.com/natew/react-base (see make-webpack-config) and (app/routes.js) – Nathan Wienert Oct 05 '14 at 14:50

9 Answers9

107

You should set historyApiFallback of WebpackDevServer as true for this to work. Here's a small example (tweak to fit your purposes):

var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');

var config = require('./webpack.config');


var port = 4000;
var ip = '0.0.0.0';
new WebpackDevServer(webpack(config), {
    publicPath: config.output.publicPath,
    historyApiFallback: true,
}).listen(port, ip, function (err) {
    if(err) {
        return console.log(err);
    }

    console.log('Listening at ' + ip + ':' + port);
});
Juho Vepsäläinen
  • 26,573
  • 12
  • 79
  • 105
  • You will miss the statusbar on top of your index.html, but this works great :) – swennemen Feb 22 '15 at 16:00
  • 8
    This should be the accepted answer. From the webpack dev server docs: _"If you are using the HTML5 history API you probably need to serve your index.html in place of 404 responses, which can be done by setting historyApiFallback: true"_ If I understand the question correctly this will solve the problem. – Sebastian Apr 14 '16 at 16:18
  • 1
    @smnbbrv No probs. It actually uses [connect-history-api-fallback](https://www.npmjs.com/package/connect-history-api-fallback) underneath and you can pass an object with the middleware specific options if you want instead of just `true`. – Juho Vepsäläinen Jun 17 '16 at 09:13
  • Any idea how to do this with webpack-dev-middleware? – Don P Mar 02 '17 at 07:53
  • @DonnyP webpack-dev-middleware doesn't do anything do anything with connect-history-api-fallback so you will have to connect it separately to your setup. – Juho Vepsäläinen Mar 02 '17 at 08:26
  • 1
    OR if you're using the cli, `webpack-dev-server --history-api-fallback` – Levi Oct 21 '18 at 14:51
  • The comment from @Levi comment, above, worked for me! – Robin Zimmermann Mar 12 '20 at 16:01
  • Awesome, thank you! For those also using webpack.config.js, just add the "historyApiFallback: true" to the "devServer" object – Zachary Raineri Sep 07 '20 at 12:53
69

I set up a proxy to achieve this:

You have a regular express webserver that serves the index.html on any route, except if its an asset route. if it is an asset, the request gets proxied to the web-dev-server

your react hot entrypoints will still point directly at the webpack dev server, so hot reloading still works.

Let's assume you run webpack-dev-server on 8081 and your proxy at 8080. Your server.js file will look like this:

"use strict";
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./make-webpack-config')('dev');

var express = require('express');
var proxy = require('proxy-middleware');
var url = require('url');

## --------your proxy----------------------
var app = express();
## proxy the request for static assets
app.use('/assets', proxy(url.parse('http://localhost:8081/assets')));

app.get('/*', function(req, res) {
    res.sendFile(__dirname + '/index.html');
});


# -----your-webpack-dev-server------------------
var server = new WebpackDevServer(webpack(config), {
    contentBase: __dirname,
    hot: true,
    quiet: false,
    noInfo: false,
    publicPath: "/assets/",

    stats: { colors: true }
});

## run the two servers
server.listen(8081, "localhost", function() {});
app.listen(8080);

now make your entrypoints in the webpack config like so:

 entry: [
     './src/main.js',
     'webpack/hot/dev-server',
     'webpack-dev-server/client?http://localhost:8081'
 ]

note the direct call to 8081 for hotreload

also make sure you pass an absolute url to the output.publicPath option:

 output: {
     publicPath: "http://localhost:8081/assets/",
     // ...
 }
ravibagul91
  • 20,072
  • 5
  • 36
  • 59
Retozi
  • 7,521
  • 1
  • 23
  • 16
  • 1
    One question, sort of unrelated so I can open a new question if need be but I notice that now the console output from the webpack dev server isn't streamed. Before, you could watch it compile and see the percents rise, now it just block outputs after compilation. – Nathan Wienert Oct 07 '14 at 19:09
  • Nice done. This is exactly how it should be done. I added a note about the `output.publicPath` option, which should be an absolute url too. – Tobias K. Oct 09 '14 at 09:00
  • Thanks, what do you have in assets? In my case the bundle.js dir is called `dist`. I'll keep it as that unless anything else is supposed to go in there? My app is in a folder called `app` - should they be in the same place? – Dominic Oct 16 '15 at 14:35
  • 7
    It would be easier just to use a built-in [webpack proxy](https://webpack.github.io/docs/webpack-dev-server.html#proxy) instead. Thus you don't interfere in the server itself, you leave the server _pure_. Instead, you just do a little (3-5 lines) addition to webpack config. Thanks to that you modify only dev scripts for dev purposes and leave the production code (server.js) in peace (unlike in your version) and imo that's the proper way to go. – jalooc Nov 11 '15 at 10:23
  • This fixes the issue: 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', found @ https://github.com/glenjamin/webpack-hot-middleware/issues/37 – krob Jan 13 '16 at 20:41
  • how do you make the `server.js` clean for production use? – harshes53 Jan 23 '16 at 04:57
  • @jalooc that is interesting to me. My understanding is that from a `package.json` script command you run `webpack-dev-server` using some `webpack.dev.config` where you specify the `devServer.proxy` option to point to your original local express server. But how do you start both webpack-dev-server **and** your express server from package.json script command? – superjos Feb 17 '16 at 19:21
  • @superjos in this case the express server is started independently of webpack. But of course you can join the two commands with something like `&&` and put it to package.json. – jalooc Feb 17 '16 at 21:09
  • thanks for quick reply. In the meantime I found other answers mentioning [parallelshell](https://www.npmjs.com/package/parallelshell). `&&` as it is would not work in that case. – superjos Feb 17 '16 at 21:20
  • @jalooc that sounds interesting, why don't you offer it as an answer rather than a comment? – BotNet Jun 26 '16 at 02:30
  • This is the best approach I've seen. Thanks for sharing. Also, what are the last two entries in the entry array for? I left them out and everything seems to work fine – cramhead Oct 27 '16 at 22:59
  • 4
    This answer is still correct though a bit dated. More straightforward ways are available now, look for `historyApiFallback`. – Eugene Kulabuhov May 29 '17 at 13:47
27

For anyone else that may still be looking for this answer. I put together a simple proxy bypass which achieves this without much hassle and the config goes into the webpack.config.js

I am sure there are much more elegant ways to test for local content using regex, but this works for my needs.

devServer: {
  proxy: { 
    '/**': {  //catch all requests
      target: '/index.html',  //default target
      secure: false,
      bypass: function(req, res, opt){
        //your custom code to check for any exceptions
        //console.log('bypass check', {req: req, res:res, opt: opt});
        if(req.path.indexOf('/img/') !== -1 || req.path.indexOf('/public/') !== -1){
          return '/'
        }

        if (req.headers.accept.indexOf('html') !== -1) {
          return '/index.html';
        }
      }
    }
  }
} 
Haroen Viaene
  • 1,329
  • 18
  • 33
Werner Weber
  • 379
  • 3
  • 3
  • Thanks for the code! Is there a difference between this and just adding "historyApiFallback: true" to the same devServer object? (outside from the obvious of being able to further customize it). Just curious – Zachary Raineri Sep 07 '20 at 12:57
15

If you're running webpack-dev-server using CLI, you can configure it through webpack.config.js passing devServer object and using the historyApiFallback option:

module.exports = {
  entry: "index.js",
  output: {
    filename: "bundle.js"
  },
  devServer: {
    historyApiFallback: true
  }
}

This will redirect to index.html everytime it 404 is encountered.

NOTE: If you're using publicPath, you'll need to pass it to devServer too:

module.exports = {
  entry: "index.js",
  output: {
    filename: "bundle.js",
    publicPath: "admin/dashboard"
  },
  devServer: {
    historyApiFallback: {
      index: "admin/dashboard"
    }
  }
}

You can verify that everything is setup correctly by looking at the first few lines of the output (the part with "404s will fallback to: path").

enter image description here

Liam
  • 27,717
  • 28
  • 128
  • 190
Eugene Kulabuhov
  • 2,349
  • 1
  • 26
  • 25
12

For a more recent answer, the current version of webpack (4.1.1) you can just set this in your webpack.config.js like such:

const webpack = require('webpack');

module.exports = {
    entry: [
      'react-hot-loader/patch',
      './src/index.js'
    ],
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.css$/,
                exclude: /node_modules/,
                use: ['style-loader','css-loader']
            }
        ]
    },
    resolve: {
      extensions: ['*', '.js', '.jsx']  
    },
    output: {
      path: __dirname + '/dist',
      publicPath: '/',
      filename: 'bundle.js'
    },
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
      contentBase: './dist',
      hot: true,
      historyApiFallback: true
    }
  };

The important part is historyApiFallback: true. No need to run a custom server, just use the cli:

"scripts": {
    "start": "webpack-dev-server --config ./webpack.config.js --mode development"
  },
Michael Brown
  • 1,585
  • 1
  • 22
  • 36
2

I'd like to add to the answer for the case when you run an isomorphic app (i.e. rendering React component server-side.)

In this case you probably also want to automatically reload the server when you change one of your React components. You do this with the piping package. All you have to do is install it and add require("piping")({hook: true}) somewhere in the beginning of you server.js. That's it. The server will restart after you change any component used by it.

This rises another problem though - if you run webpack server from the same process as your express server (as in the accepted answer above), the webpack server will also restart and will recompile your bundle every time. To avoid this you should run your main server and webpack server in different processes so that piping would restart only your express server and won't touch webpack. You can do this with concurrently package. You can find an example of this in react-isomorphic-starterkit. In the package.json he has:

"scripts": {
    ...
    "watch": "node ./node_modules/concurrently/src/main.js --kill-others 'npm run watch-client' 'npm run start'"
  },

which runs both servers simultaneously but in separate processes.

Viacheslav
  • 3,876
  • 2
  • 21
  • 28
1

historyApiFallback can also be an object instead of a Boolean, containing the routes.

historyApiFallback: navData && {
  rewrites: [
      { from: /route-1-regex/, to: 'route-1-example.html' }
  ]
}
Tom Roggero
  • 5,777
  • 1
  • 32
  • 39
1

May be not in all cases, but seems the publicPath: '/' option in the devServer is easiest solution to fix deep routes issue, see: https://github.com/ReactTraining/react-router/issues/676

Alex
  • 1,297
  • 1
  • 16
  • 12
-1

This worked for me: just simply add the webpack middlewares first and the app.get('*'... index.html resolver later,

so express will first check if the request matches one of the routes provided by webpack (like: /dist/bundle.js or /__webpack_hmr_) and if not, then it will move to the index.html with the * resolver.

ie:

app.use(require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
}))
app.use(require('webpack-hot-middleware')(compiler))
app.get('*', function(req, res) {
  sendSomeHtml(res)
})