Reputation: 3402
I try to setup nested routes for my react app like this
/
-> Home Page/about
-> About Page/protected
-> protected default page/protected/page1
-> protected page 1It 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
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
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
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
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
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