Bing Lu
Bing Lu

Reputation: 3402

React Router v4 nested routes not work with webpack-dev-server

I try to setup nested routes for my react app like this

It works fine in codesandbox (https://codesandbox.io/s/react-router-nested-route-utqy7) React 16.8.1 React Router 4.3.1

But when I set the same thing up with webpack-dev-server (3.7.1), it can only reach / and can't reach to the rest routes.

My file structure is like

├── package.json
├── src
│   ├── index.jsx
│   └── index.html
├── webpack
│   ├── paths.js
│   ├── webpack.common.js
│   └── webpack.dev.js
└── webpack.config.js

paths.js

const path = require('path');

module.exports = {
  outputPath: path.resolve(__dirname, '../', 'build'),
  entryPath: path.resolve(__dirname, '../', 'src/index.jsx'),
  templatePath: path.resolve(__dirname, '../', 'src/index.html'),
};

webpack.common.js

const webpack = require('webpack');
const convert = require('koa-connect');
const history = require('connect-history-api-fallback');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const commonPaths = require('./paths');

module.exports = {
  entry: commonPaths.entryPath,
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        loader: 'babel-loader',
        exclude: /(node_modules)/,
      },
    ],
  },
  serve: {
    add: app => {
      app.use(convert(history()));
    },
    content: commonPaths.entryPath,
    dev: {
      publicPath: commonPaths.outputPath,
    },
    open: true,
  },
  resolve: {
    modules: ['src', 'node_modules'],
    extensions: ['*', '.js', '.jsx', '.css', '.scss'],
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({
      template: commonPaths.templatePath,
    }),
    new ScriptExtHtmlWebpackPlugin({
      defaultAttribute: 'async',
    }),
  ],
};

webpack.dev.js

const webpack = require('webpack');
const commonPaths = require('./paths');

module.exports = {
  mode: 'development',
  output: {
    filename: '[name].js',
    path: commonPaths.outputPath,
    chunkFilename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.(css|scss)$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
          },
          'sass-loader',
        ],
      },
    ],
  },
  devServer: {
    contentBase: commonPaths.outputPath,
    compress: true,
    hot: true,
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
};

webpack.config.js

const webpackMerge = require('webpack-merge');
const common = require('./webpack/webpack.common');

const devConfig = require(`./webpack/webpack.dev.js`);
module.exports = webpackMerge(common, devConfig);

index.jsx

import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Route } from "react-router-dom";

const Homepage = () => (
  <div>
    <h1>Home Page</h1>
  </div>
);
const AboutPage = () => (
  <div>
    <h1>About</h1>
  </div>
);
const Protected = () => (
  <div>
    <h1>Protected default page</h1>
  </div>
);
const ProtectedPage1 = () => (
  <div>
    <h1>ProtectedPage1</h1>
  </div>
);

render(
  <BrowserRouter>
    <div>
      <Route path="/" component={Homepage} exact />
      <Route path="/about" component={AboutPage} />

      <Route
        path="/protected"
        render={({ match: { url } }) => (
          <div>
            <Route path={`${url}/`} component={Protected} exact />
            <Route path={`${url}/page1`} component={ProtectedPage1} />
          </div>
        )}
      />
    </div>
  </BrowserRouter>,
  document.getElementById('app')
);

I think some paths are incorrect in my config, I just can't figure out where is wrong.

Upvotes: 8

Views: 7440

Answers (5)

Zach Smith
Zach Smith

Reputation: 8961

To summarized the answer by @Bing Lu, in your webpack.config.js file:

module.exports = () => ({
  mode: 'development',
  entry: ...,
  ...,
  output: {
    ...
    publicPath: '/' // <- this is the important line along with historyApiFallback = true in the dev server config
  },
  ...,
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    historyApiFallback: true,
    compress: true,
    ...
  },
})

Upvotes: 3

l30.4l3x
l30.4l3x

Reputation: 573

I was having the same problem described in the question (webpack-dev-server not serving nested routes, top level ones working fine). Sadly, neither historyApiFallback: true nor publicPath: '/' were working. Actually, the problem was inside index.html, more precisely inside <script src="bundle.js"></script>. Changing to

<script src="/bundle.js"></script>       <!-- do include slash before the file name -->

was enough to end the pain.

Upvotes: 4

SIRANJEEVI.K
SIRANJEEVI.K

Reputation: 1

basically wrap, your react app using <HashRouter> instead of <BrowserRouter> working fine without any webpack config modification, if you don't want to use HashRouter then you can free to use historyApiFallback: true in web pack dev server config on bottom of webpack.config file

like so
 const config = {

........
    devServer: {
        compress: true,
        port: 3000,
        https: false,
        historyApiFallback:true
    }
}

Upvotes: 0

oskario
oskario

Reputation: 251

Try adding:

<base href="/" />

to the <head> tag of your index.html. This way it'll always look for /main.js bundle, even for nested routes.

Upvotes: 19

Bing Lu
Bing Lu

Reputation: 3402

I finally figured out the reason that webpack-dev-server couldn't serve nested routes.

As a single page application, when you visit /somepath of your react app, it actually fallback to the / and pass the pathname to react router. React router will navigate you to /somepath by the using browser's history API.

webpack-dev-server, for some unknown reason, doesn't enable this "fallback to history API" behaviour by default.

So, we need to add historyApiFallback: true, to the devServer of webpack config.

Now, all top level routes, like /somepath should work, but for nested routes, like /somepath/morepath, it's not enough.

With default webpack-dev-server setting, the compiled html template will point to the bundled js like <script type="text/javascript" src="main.js"></script>. Pay attention to the src="main.js" which assumes the main.js is under the same path as the index.html. The assumption is true for top level path /somepath but for nested routes, /somepath/morepath, this assumption will lead html file to access main.js like /somepath/main.js.

So, we end up with looking for a way to specify a certain place for html file when it's going to access the bundled js. And, it's the job of publicPath. Adding publicPath: '/', to the output block of webpack config. It will tell html to always access main.js from / folder and the compiled html will be <script type="text/javascript" src="/main.js"></script>. That's exactly what we're looking for.

Upvotes: 47

Related Questions