I am developing React app and I have some problems while implementing routing.
<BrowserRouter>
<Layout>
<Route path="/" component={Home} exact={true} />
<Route path="/signin" component={Signin} />
<Route path="/register" component={Register} />
<Route path="/about" component={About} />
<Route path="/class/:id" component={Class} />
<Route path="/class" component={Classes} exact={true} />
</Layout>
</BrowserRouter>
When I navigate home page on local , i.e localhost:8000 , home page is displayed.
And when I click class link, browser navigates to /class and classes page is displayed.
And when I click specific class item , browser navigate to /class/:class-id and specific class detail page is displayed.
But when I manually type in /class/:class-id in browser, class detail page is not displayed.
When I inspect the browser, bundle.js was referred to localhost:8000/class/bundle.js.
But bundle.js is referred to localhost:8000/bundle.js.
I am using webpack and here's webpack configuration.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InterpolateHtmlPlugin = require('interpolate-html-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: "./src/index.js",
// Here the application starts executing
// and webpack starts bundling
output: {
// options related to how webpack emits results
path: path.resolve(__dirname, "dist"), // string
// the target directory for all output files
// must be an absolute path (use the Node.js path module)
filename: "bundle.js", // string,
// the filename template for entry chunks,
assetModuleFilename: "assets/[hash][ext][query]"
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader'
},
{
test: /\.less$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" },
{ loader: "less-loader" }
]
},
{
test: /\.(jpe?g|png|gif|svg)$/,
type: "asset/resource"
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'#': path.resolve(__dirname, 'src/'),
}
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
}),
new InterpolateHtmlPlugin({
PUBLIC_URL: 'http://localhost:8000/public'
})
],
devServer: {
historyApiFallback: true,
port: 8000,
open: true
},
externals: {
// global app config object
config: JSON.stringify({
apiUrl: 'http://localhost:4000'
})
}
}
I found a simple solution.
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
publicPath: '/'
}),
new InterpolateHtmlPlugin({
PUBLIC_URL: 'http://localhost:8000/public'
})
]
I added publicPath: '/' to HtmlWebpackPlugin so that bundle.js can be referred to absolute path, not relative path. localhost:8000/bundle.js
So it works perfect now.
On the other hand, I researched isomorphic application but found that its main benefits are to improve performance & SEO implementation.
This idea appeared to improve performance & SEO implementation after modern JS frameworks which have client rendering ability.
Since Google announced and supported SEO regarding javascript, this idea lost enthusiast , but still be used for its benefits.
I am sure isomorphic and catch-all method can resolve my issue with perfect.
But for now, I prefer my current solution, it's simple.
Related
I have an app that uses react-router-dom and I have a a basic setup like so:
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Box, Flex } from '#chakra-ui/react';
import CodeEntry from './pages/CodeEntry';
import Register from './pages/Register';
// import Login from './pages/Login';
import { useUI } from '../state/context/UIContext';
import { GlobalStyle } from '../styles/GlobalStyles';
import Header from './Header';
import Footer from './Footer';
import ErrorMessage from '../components/ErrorMessage';
import routes from '../config/routes';
const App = () => {
const {
state: { errorMessage },
} = useUI();
return (
<Router>
<GlobalStyle />
<Flex flexDir="column" justifyContent="space-between" minH="100vh">
<Box>
<Header />
{errorMessage.length > 0 && <ErrorMessage message={errorMessage} />}
<Switch>
<Route exact path={routes.APP.REGISTER}>
<Register />
</Route>
<Route exact path={routes.APP.DASHBOARD}>
<>Dashboard</>
</Route>
<Route exact path={routes.APP.HOME}>
<CodeEntry />
</Route>
<Route exact path={routes.APP.LOGIN}>
<>Login</>
</Route>
</Switch>
</Box>
<Box>
<Footer />
</Box>
</Flex>
</Router>
);
};
export default App;
When I run my app locally with npm run dev it uses the following command to spin up a local server:
webpack serve --config webpack.dev.js
the webpack.dev.js file looks like so:
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const Dotenv = require('dotenv-webpack');
const plugins = [];
//if (process.env.CI !== 'true') plugins.push(new Dotenv({ systemvars: true, path: path.resolve(__dirname, '.env') }));
module.exports = merge(common, {
mode: 'development',
devServer: {
host: 'localhost',
port: 3000,
historyApiFallback: true,
open: true,
},
infrastructureLogging: {
level: 'error',
},
stats: 'none',
output: {
publicPath: '/',
},
plugins,
});
and the webpack.common.js like so:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
require('babel-polyfill');
module.exports = {
entry: {
main: ['babel-polyfill', './src/index.tsx'],
},
devtool: 'source-map',
output: {
filename: '[name].[fullhash].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
{
test: /\.js$/,
exclude: [path.resolve(__dirname, 'node_modules')],
use: [{ loader: 'babel-loader' }],
},
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
outputPath: 'assets/img/',
},
},
],
},
{
test: /\.(woff(2)?|ttf|eot|otf)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'assets/fonts/',
},
},
],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebPackPlugin({
template: 'index.html',
}),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ['dist'],
}),
new webpack.DefinePlugin({
__REACT_DEVTOOLS_GLOBAL_HOOK__: '({ isDisabled: true })',
}),
],
};
Everything in dev works as it should but when I run a build for production non of my routes apart from the root route ('/') work. Here is how I run a production build:
npm run build && webpack --config webpack.prod.js
and here is how I serve that build locally using the npm package serve:
serve dist --no-clipboard --listen ${PORT:-3000}"
My webpack.prod.js file is quite simple and adopts a lot of the webpack config from the webpack.common.js config file. Here is my prod config:
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
//
module.exports = merge(common, {
mode: 'production',
});
I have tried adding a <Link> component like so:
<Link to='/register'>
The link works fine when I click on it in prod as well as dev, it is just when I navigate directly to the page where I get a 404
Solution was simple, in the end it was an issue with the serve package. All I had to do was a a -s flag to the following command:
serve -s dist --no-clipboard --listen ${PORT:-3000}"
I'm using react router with component lazy loading and using Webpack as a bundler, When I access to the home page / I can see in the network tab that the bundle.js is loaded and also when I click on a specific item in the sidebar the correspondent component is loaded successfully with its file name for example 0.bundle.js, However when I navigate directly from the search bar to a nested route (example http://127.0.0.1:8080/forms/select) i get an error like the following:
GET http://127.0.0.1:8080/forms/bundle.js net::ERR_ABORTED 404 (Not Found)
This error indicates that the bundle.js is not loaded which means that it cannot load the other chunks.
webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
module: {
rules: [],
},
resolve: {
extensions: ['*', '.js', '.jsx'],
},
output: {
path: __dirname + '/dist',
publicPath: '/',
filename: 'bundle.js',
},
plugins: [new webpack.HotModuleReplacementPlugin()],
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
hot: true,
historyApiFallback: true,
},
};
.babelrc
{
"presets": [
"#babel/preset-env",
"#babel/preset-react"
],
"plugins": ["#babel/plugin-syntax-dynamic-import"]
}
routes.js
import { lazy } from 'react';
const Forms = lazy(() => import('../components/uiViews/Forms'));
const SelectForm = lazy(() => import('../components/uiViews/Forms/SelectForm'));
const FormValidation = lazy(() => import('../components/uiViews/Forms/FormValidation'));
const routes = [
{
icon: 'form',
label: 'forms',
path: '/forms',
component: Forms,
children: [
{
icon: 'select',
label: 'selectInput',
path: '/forms/select',
component: SelectForm,
},
{ icon: 'issues-close', label: 'formValidation', path: '/forms/validation', component: FormValidation },
{
icon: 'form',
label: 'wizardForm',
path: '/forms/wizard',
component: WizardForm,
}],
},
];
export default routes;
routes rendering
<Suspense fallback={<div className="spin-loading"> <Spin size="large" /></div>}>
{routes.map((route, i) => {
return route.component ? RouteWithSubRoutes( {...route},`r${i}`) : null;
})}
</Suspense>
....
function RouteWithSubRoutes(route,key) {
return route.children ? (
route.children.map((subRoute,j) => {
return RouteWithSubRoutes(subRoute,`sr${j}`);
})
) : (
<Route key={key} path={route.path} exact component={() =>route.component? <route.component />:<ComingSoon/>} />
);
}
After some days of trying out different solutions, finally i found this one that saves my day :
... I finally figured out the actual issue and it is not directly related to either Webpack or React Hot Loader or React Router or any other library at least for now at least for me. When using HTML5 push state to manipulate browsers history WE MUST PROVIDE tag in our html head section. After providing to the head section of my html, HMR works like a charm even in nested routes.
<!DOCTYPE html>
<html>
<head>
<base href="/" /> <!-- THIS TINY LITTLE THING -->
<meta charset="UTF-8" />
<title>Hello React!</title>
</head>
<body>
<div id="root"></div>
<script src="/main.bundle.js"></script>
</body>
</html>
I'm developing a project using React/Redux and Node.js.
I added react-router-dom as a dependency into my project and configured the router like this:
import ...
const Router = () => (
<main>
<Switch>
<Route exact path='/' component={components.main}/>
<Route path='/caseDetail/:id' component={components.caseDetail}/>
<Route component={components.notFound}/>
</Switch>
</main>
)
export default Router
And setup my webpack like this
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const env = require('./src/env.js')
const TARGET = process.env.npm_lifecycle_event
process.env.BABEL_ENV = TARGET
const PATHS = {
app: path.join(__dirname, 'src'),
build: path.join(__dirname, 'build'),
assets: path.join(__dirname, 'assets')
}
const common = {
entry: [PATHS.app],
module: {
rules: [
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
include: PATHS.app,
loaders: ['babel-loader']
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: true } }],
include: /flexboxgrid/
},
{
test: /\.scss$/,
exclude: /flexboxgrid/,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { modules: true } },
'sass-loader'
]
},
{
test: /\.jpe?g$|\.gif$|\.png$/i,
loader: 'url-loader?name=[name].[ext]'
},
{
test: /\.otf$|\.eot$|\.svg$|\.ttf|\.woff|\.woff2$/,
loader: 'url-loader?name=[name].[ext]'
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
modules: ['node_modules', path.resolve(__dirname, './node_modules')],
mainFields: ['browser', 'web', 'browserify', 'main', 'style']
}
}
if (TARGET === 'start' || !TARGET) {
module.exports = merge(common, {
module: {
rules: [
{
test: /\.jsx?$/,
use: ['source-map-loader'],
enforce: 'pre'
}
]
},
devtool: 'inline-source-map',
devServer: {
contentBase: PATHS.build,
historyApiFallback: true,
hot: true,
inline: true,
progress: true,
stats: 'errors-only',
https: true,
host: env.host,
port: env.port,
overlay: {
errors: true
},
watchOptions: {
watch: true
}
},
plugins: [
new MiniCssExtractPlugin({ filename: 'assets/style.css' }),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: PATHS.app + '/index.html',
inject: 'body'
})
],
output: {
path: PATHS.build,
filename: 'bundle.js'
}
})
}
if (TARGET === 'production') {
module.exports = merge(common, {
plugins: [
new MiniCssExtractPlugin({ filename: 'style.css' }),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: "'production'"
}
})
],
output: {
path: '/build',
filename: 'bundle.js'
}
})
}
My problem is
When I call this route /caseDetail/1 it works by using Link:
<Link to={{ pathname: `/caseDetail/` + caseStudy.id }}>
BUT, if I call directly by the browser, like https://localhost:3000/caseDetail/1
It fails, I get this message on console
Refused to apply style from 'https://localhost:3000/caseDetail/assets/style.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
0:12 GET https://localhost:3000/caseDetail/bundle.js net::ERR_ABORTED 404
0:1 Refused to execute script from 'https://localhost:3000/caseDetail/bundle.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
SOLVED
How to fix. Thanks to #soroush-chehresa
I added into my router file, the BrowserRouter like this:
import React from 'react'
import { Switch, Route, BrowserRouter } from 'react-router-dom'
import { components } from './loader'
const Router = () => (
<main>
<BrowserRouter>
<Switch>
<Route exact path='/' component={components.main}/>
<Route exact path='/caseDetail' component={components.caseDetail}/>
<Route path='/caseDetail/:id' component={components.caseDetail}/>
<Route component={components.notFound}/>
</Switch>
</BrowserRouter>
</main>
)
export default Router
And also added into my webpack.config.js on output
publicPath: '/'
If you are using react-router-redux wrap Router with ConnectedRouter:
import { ConnectedRouter } from 'react-router-redux';
import { createBrowserHistory } from 'history';
const Router = () => (
<main>
<ConnectedRouter history={createBrowserHistory()}>
<Switch>
<Route exact path='/' component={components.main}/>
<Route path='/caseDetail/:id' component= {components.caseDetail}/>
<Route component={components.notFound}/>
</Switch>
</ConnectedRouter>
</main>
);
Otherwise if you are using react-router-dom wrap Router with BrowserRouter:
import { BrowserRouter } from 'react-router-dom';
const Router = () => (
<main>
<BrowserRouter>
<Switch>
<Route exact path='/' component={components.main}/>
<Route path='/caseDetail/:id' component={components.caseDetail}/>
<Route component={components.notFound}/>
</Switch>
</BrowserRouter>
</main>
);
It seems that when webpack builds the file the output can only see the maincard div and none of the contents therein. I'm not sure what's missing as when this is run as npm react-scripts start it works fine. I'm not sure what i'm missing from webpack for this to render correctly. I'm trying to load this into an S3 bucket so it has to be packed with the webpack.
import React from 'react';
import { connect } from 'react-redux';
import { Route, withRouter } from 'react-router-dom';
import { fetchUserList } from "../actions/UserActions";
import { fetchSkillList } from "../actions/SkillActions";
import WelcomeCard from "./WelcomeCard";
import UserSearchCard from "./UserSearchCard";
import AddUserCard from './AddUserCard';
import '../styles/MainCard.css';
class MainCard extends React.Component {
componentDidMount() {
this.props.fetchUserList();
this.props.fetchSkillList();
}
render() {
return (
<div className="main_card">
<Route exact path='/' component={WelcomeCard}/>
<Route path='/list' component={UserSearchCard}/>
<Route path='/new' component={AddUserCard}/>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return {
fetchUserList: () => dispatch(fetchUserList()),
fetchSkillList: () => dispatch(fetchSkillList())
}
};
export default withRouter( connect(undefined, mapDispatchToProps)(MainCard) );
Webpack Config:
let path = require('path');
let webpack = require('webpack');
const publicPath = '/dist/build/';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
//Content
entry: './src/index.js',
mode: 'development',
// A SourceMap without column-mappings ignoring loaded Source Maps.
devtool: 'cheap-module-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('development')
}
}),
//simplifies creation of HTML files to serve your webpack bundles. This is especially useful for webpack bundles that include a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you, supply your own template using lodash templates or use your own loader.
new HtmlWebpackPlugin({
title: 'Talent Identification Manager'
}),
//Auto replacement of page when i save some file, even css
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
],
output: {
path: path.join(__dirname, publicPath),
filename: 'main.bundle-0.0.1.js',
publicPath: "/",
sourceMapFilename: 'main.map',
},
devServer: {
port: 3000,
host: 'localhost',
//Be possible go back pressing the "back" button at chrome
historyApiFallback: true,
noInfo: false,
stats: 'minimal',
publicPath: publicPath,
contentBase: path.join(__dirname, publicPath),
//hotmodulereplacementeplugin
hot: true
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules(?!\/webpack-dev-server)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015', 'stage-2'],
plugins: ['syntax-decorators']
}
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
}
]
}
}
React Router doesn't know that you want to treat /talentidbucket as the base of your site, so you have explicitly tell it so by passing the base path as the basename prop of the BrowserRouter component in production.
class App extends React.Component {
render() {
return (
<BrowserRouter basename="/talentidbucket"> {/* ... */} </BrowserRouter>
);
}
}
When using hash history code splitting works with react router but now i'm about to go into production and i want to switch to browser-history it gives an error when i try to change route, example if i try going to the login route 127.0.0.1:8080/auth/login :
Refused to execute script from
'http://127.0.0.1:8080/auth/3.bundle.js' because its MIME type
('text/html') is not executable, and strict MIME type checking is
enabled.
and
Uncaught (in promise) Error: Loading chunk 3 failed. (error:
http://127.0.0.1:8080/auth/3.bundle.js)
at HTMLScriptElement.onScriptComplete (bootstrap:108)
This is my router
<Router history={history}>
<ConnectApp />
</Router>
Connect app:
<Switch>
{/* Front end */}
<Route path="/" component={AsyncHome} exact />
<Route path="/post/:id" component={AsyncPost} exact />
{/* authentication */}
<Route path="/auth/:section" component={AsyncLogin} exact />
{/* Backend */}
<PrivateRoute path="/admin/:path" component={AsyncAdmin} exact />
<PrivateRoute
path="/admin/edit-post/:id"
component={AsyncEditPost}
exact
/>
<Route component={Err404} />
</Switch>
history.js:
import { createBrowserHistory } from 'history';
export default createBrowserHistory({
// add configurations here
});
webpack.dev.config.js
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
historyApiFallback: true
},
plugins: [
new BundleAnalyzerPlugin()
]
}
*If there is any more code to add please indicate in the comment
Thank you
Add publicPath:"/" to the config:
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
historyApiFallback: true
},
plugins: [
new BundleAnalyzerPlugin()
],
output: {
path: path.resolve('dist'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
publicPath: '/' // Add this line
},
}