Fail to improve site first time loading performance - javascript

I am creating a site using HTML5, JavaScript and React.
I am using Google Chrome Lighthouse to check the performance of the site first load.
This is the Lighthouse Runtime environment:
User agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Device Emulation Nexus 5X: Enabled
Network Throttling 562.5ms RTT, 1.4Mbps down, 0.7Mbps up: Enabled
CPU Throttling 4x slowdown: Enabled
After many iteration and fixes these are the scores I have on Lighthouse so far:
Performance 57
Progressive Web App 100
Accessibility 100
Best Practices 88
SEO 100
The best Practices is not at 100 because:
the host I have does not provide HTTP/2 (I might change if that's really an issue)
I have Google Ads script with has document.write() (can't change anything about that)
My big problem is with the Performance Metrics.
I have a white blank screen for more than 3.3s and the is fully showing after 8.3s!
The First meaningful paint is at 5,750ms, the first Interactive is at 6,720ms (same for Consistent Interactive)
The Perceptual Speed Index is 4,228 (score 64)
And the Estimated Input Latency is 16ms (score 100)
This is the Critical Chain Request:
This is the Network overview:
This is what I have done/tried so far:
Moved all components in the start page to async load using react-code-splitting
Optimized the images to use the new J2P, JXR and WEBP format, and made them the correct size so the browser does not have to resize them.
Moved the content of App.js into index.js (before the loading would get index.js and vendor.js in parallel, then a js file with all the css files inside and app.js, and after that it would load in parallel the components that are used inside app.js
Moved some css import to the components they are associated with.
Used webpack-bundle-analyzer to try to find common libraries and how they are splitted. As you can see in the following screenshot there are a lot of duplicated modules in my bundles that I cannot find how to extract for efficiency. (lodash, react-overlays,...)
This is my webpack.config.js
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
const WebpackChunkHash = require('webpack-chunk-hash');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
//var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const OfflinePlugin = require('offline-plugin');
// To handle the regeneratorRuntime exception
require('babel-polyfill');
require('file-loader');
require('css-loader');
require('style-loader');
require('html-webpack-template');
/* Shared Dev & Production */
const config = {
context: path.resolve(__dirname, 'src'),
entry: {
index: [
// To handle the regeneratorRuntime exception
'babel-polyfill',
'./index.js'
],
vendor: ['offline-plugin/runtime', 'react', 'react-dom', 'react-router', 'react-redux', 'history', 'react-router-dom', 'redux', 'react-router-redux', 'redux-form', 'lodash'],
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.(ico|jpg|png|gif|eot|otf|jp2|jxr|webp|svg|ttf|woff|woff2)(\?.*)?$/,
exclude: /\/favicon.ico$/,
use: [
{
loader: 'file-loader',
query: {
name: '[path][name][hash].[ext]',
publicPath: '/'
}
}
]
},
{
test: /\.css$/,
include: path.join(__dirname, 'src/style'),
use: [
'style-loader',
{
loader: 'css-loader',
options: {
minimize: true
}
}
]
},
{
test: /\.(ico)(\?.*)?$/,
exclude: /node_modules/,
use: {
loader: 'file-loader',
options: { name: '[name].[ext]' },
},
},
{
test: /\.xml/,
use: {
loader: 'file-loader',
options: { name: '[name].[ext]' },
},
},
],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
publicPath: '/',
},
resolve: {
extensions: ['.js'],
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
},
plugins: [
// New moment.js optimization
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: 2
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
appMountId: 'app-root',
inlineManifestWebpackName: 'webpackManifest',
template: 'templateIndex.html',
title: 'Our Site',
}),
new OfflinePlugin({
AppCache: false,
ServiceWorker: { events: true },
}),
new BundleAnalyzerPlugin({analyzerMode: 'static'}),
],
devServer: {
historyApiFallback: true,
},
};
//if (process.env.NODE_ENV === 'production') {
config.output.filename = '[name].[chunkhash].js';
config.plugins = [
...config.plugins,
new webpack.HashedModuleIdsPlugin(),
new WebpackChunkHash(),
/*new ChunkManifestPlugin({
filename: 'chunk-manifest.json',
manifestVariable: 'webpackManifest',
inlineManifest: true,
}),*/
];
//}
module.exports = config;
my .babelrc
{
"presets": [
[
"env",
{ "modules": false }
],
"react",
"stage-0",
"stage-2"
],
"plugins": [ "syntax-dynamic-import", "transform-react-jsx", "transform-decorators-legacy" ]
}
And my index.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import {Redirect, Route, Switch, withRouter} from 'react-router';
import { Provider } from 'react-redux';
import Async from 'react-code-splitting';
const TopBar = () => (<Async load={import('./containers/TopBar/TopBarAsync')} />);
const NavBar = () => (<Async load={import('./containers/NavBar/NavBarAsync')} />);
const BottomBar = () => (<Async load={import('./containers/BottomBar/BottomBarAsync')} />);
const UserHelpers = (props) => <Async load={import('./containers/UserHelpers')} componentProps={props} />
const Main = () => (<Async load={import('./containers/Main')} />);
const Blog = () => (<Async load={import('./containers/Blog')} />);
const PublicPage = () => (<Async load={import('./containers/PublicPage')} />);
import configureStore from './store/store';
const store = configureStore();
import './styles/font-awesome.min.css';
import './styles/app.css';
import './styles/TeamBrowser.css';
let googleTracking = null;
if(process.env.NODE_ENV==='production') {
// https://web-design-weekly.com/2016/07/08/adding-google-analytics-react-application/
ReactGA.initialize([my key here]);
googleTracking = function fireTracking() {
let page=window.location.pathname+window.location.search;
ReactGA.set({ page: page });
ReactGA.pageview(page);
}
const runtime=require('offline-plugin/runtime');
runtime.install({
onUpdateReady() {
runtime.applyUpdate();
},
onUpdated() {
window.location.reload();
},
});
}
render((
<Provider
store={store}>
<BrowserRouter
onUpdate={googleTracking}>
<div className="body">
<header>
<TopBar/>
<NavBar />
</header>
<main>
<Switch>
<Route path="/" exact component={Main} />
<Route path="/main" component={Main} />
<Route path="/Feed" component={Blog} />
<Route path="/Blog" component={Blog} />
<Route path="/:id" component={PublicPage} />
<Redirect to="/" />
</Switch>
</main>
<footer
className="footer">
<BottomBar />
</footer>
<UserHelpers />
</div>
</BrowserRouter>
</Provider>),
document.getElementById('app'));
Right now I am out of ideas about how to improve the loading speed to get a better Lighthouse score.
Can anyone spot anything that I do not know about?
To me it looks like chunk-manifest-webpack-plugin is not doing its job properly because it is not extracting all the common modules.
And second, the index.[hash].js bundle contains files that it should not (ex: utils.js, files under the 'actions' folder,... These are my own files, but they are not referenced in my index.js so why does it put them there?

Related

React Routing Issue (Without create-react-app)

I have created react app without using CRA command and also installed react router dom v6.
When I have 1 path in url, I don't get any error (like http://localhost:3000 /firstPath ). But, when I have 2 paths in URL (like http://localhost:3000 /firstPath/secondPath ), I get 404 firstPath/main.js not found error. It is searching for main.js inside a folder called firstPath.
In the following code, the index, add and test URLs are working. But the edit URL is not working.
Console Log Image
Sources Tab Image
rootRouter.js code: [BrowserRouter tag is given in the parent file]
import React from "react";
import { Route, Routes } from "react-router-dom";
import Home from "../views/Home";
import Add from "../views/Add";
import Edit from "../views/Edit";
const rootRouter = () => {
return (
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="add" element={<Add />} />
<Route path="edit/:id" element={<Edit />} />
<Route path="test" element={<div>test</div>} />
</Routes>
);
};
export default rootRouter;
webpack.config.js code:
const path = require("path");
const port = process.env.PORT || 3000;
module.exports = {
mode: "development",
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
target: "web",
devServer: {
host: "localhost",
port: port,
static: ["./public"],
open: true,
hot: true,
liveReload: true,
historyApiFallback: true,
},
resolve: {
extensions: [".js", ".jsx", ".json", ".ts", ".tsx"],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.png?$/,
use: ["file-loader"],
},
],
},
};

HMR not triggering sometimes for components

This has been baffling me for a few hours now & I can't seem to work out why. My project structure is below. Sometimes a change in views/Dashboard/index.jsx will trigger a hot reload & other times it will not. It's weird but when it works, after about an hour or so, it will just suddenly stop working...
index.js
import 'react-hot-loader'
import 'bootstrap/dist/css/bootstrap.min.css'
import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import { library } from '#fortawesome/fontawesome-svg-core'
import { faChartBar, faThList, faCogs, faClipboard, faFilter, faNotEqual } from "#fortawesome/free-solid-svg-icons"
import configureStore, { history } from './redux/store'
import App from './App'
library.add(faChartBar, faThList, faCogs, faClipboard, faFilter, faNotEqual)
let store
function render(Component) {
ReactDOM.render(
<AppContainer>
<Component store={store} />
</AppContainer>,
document.getElementById('root')
)
}
configureStore()
.then(_store => {
store = _store
render(App)
if (module.hot) {
// Reload components
module.hot.accept('./App', () => {
render(App)
})
}
})
.catch(err => {
console.log(err)
})
App.jsx
import { hot } from 'react-hot-loader/root'
import React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom'
import PropTypes from 'prop-types'
// Routes
import routes from './routes'
const App = ({ store }) => {
return (
<Provider store={store}>
<Router>{routes}</Router>
</Provider>
)
}
App.propTypes = {
history: PropTypes.object,
}
export default hot(module)(App)
routes/index.js
import React from 'react'
import { Route, Switch } from 'react-router'
// Components
import Dashboard from '../views/Dashboard'
// Views
import NoMatch from '../views/NoMatch'
export default (
<Switch>
<Route path="/" component={Dashboard} exact={true} />
<Route component={NoMatch} />
</Switch>
)
views/Dashboard/index.jsx
import React, { Component } from 'react'
export default class Dashboard extends Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
Hello World
</div>
)
}
}
If I make any change to the first 4 files listed (index.js, App.jsx, routes/index.js, containers/Dashboard.js) then the hot reloading works & the page is reloaded. However, if I attempt to make a change to views/Dashboard/index.jsx, for example, change "Hello World" to "Foo Bar" then the hot reloading is not honoured.
However I have now taken out the intermediary step of redux & changed the routers/index.js to use the direct component from views/Dashboard/index.jsx & it still does not reload.
How can this be? If I set the component on the Route to the container listed above, it hot reloads for that file but not this JSX one? Is this to do with the structure inside my main component views/Dashboard/index.jsx? If so, what? As it looks fine to me.
EDIT
My complete webpack config is:
webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const DotenvPlugin = require('dotenv-webpack')
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
filename: 'js/[name].bundle.js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader', // Using Babel to compile JS files
},
{
test: /\.(css|scss|sass)$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.(png|svg|jpg|gif|woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1000000,
name: 'img/[hash].[ext]',
},
},
],
},
{
test: /\.html$/,
loader: 'html-loader'
}
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public', 'index.html'),
filename: './index.html',
}),
new DotenvPlugin({
systemvars: true,
}),
],
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
}
webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const path = require('path')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
entry: path.resolve(__dirname, 'src', 'index.js'),
// https://github.com/gaearon/react-hot-loader#source-maps
devtool: 'eval',
devServer: {
contentBase: path.join(__dirname, 'dist'),
open: true,
compress: true,
hot: true,
historyApiFallback: true
},
resolve: {
alias: {
'react-dom': '#hot-loader/react-dom'
}
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
],
optimization: {
usedExports: true,
},
})
.babelrc
{
"presets": [
[
"#babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.0.0",
"targets": {
"esmodules": true
}
}
],
"#babel/preset-react"
],
"plugins": [
"react-hot-loader/babel"
]
}
I have updated the files listed to show what I currently have. This is a very weird error as sometimes the hot reloading will work & other times it will not. Hot reloading always works for files; index.js, App.jsx, & routes/index.jsx but sometimes it will work for views/Dashboard/index.jsx & sometimes it will not.
I was previously following the Getting Started guide but this thread helped me realise this was out of date. I have now just followed the README as is mentioned. I have also attempted to take snippets from this article as well on setting up HMR with Webpack 4.
Versions
webpack: 4.41.6
webpack-dev-server: 3.10.3
babel-loader: 8.0.6
#hot-loader/react-dom: 16.11.0
react: 16.12.0
react-hot-loader: 4.12.19
I have the below response in the console so HMR is clearly enabled
I am trying to replicate exactly what is in the examples as recommended

Why does isomorphic-style-loader throw a TypeError: Cannot read property 'apply' of undefined when being used in unison with CSS-Modules

I'm currently trying to render the application on the server, which works for the HTML and JS, but found that my styles (.less | .scss) would not load. I did some research and figured, not sure, that I was missing the isomorphic-style-loader in my Webpack configuration based on others running into the same issues. I set it up, at least how I understood it, but am now finding that when running the application I receive the following error:
TypeError: Cannot read property 'apply' of undefined at WithStyles.componentWillMount
I'm somewhat new to the whole React / Express thing but have been trying to follow along with tutorials and learning as I go, if anything seems out of place, please excuse me. I am hoping to see if anybody can explain what exactly causes this error, and provide me with some idea of what I could follow to resolve this error. Below is some example code that resembles the one I am having issues with, if it helps in any way.
(For reference I was following Tyler McGinnis React Router Server Rendering tutorial and tried to expand upon it to add styling - Link Here)
Thanks beforehand for any explanation provided as to what may be causing this error.
webpack.config.babel.js
import path from 'path'
import webpack from 'webpack'
import nodeExternals from 'webpack-node-externals'
const paths = {
browser: path.join(__dirname, './src/browser'),
server: path.join(__dirname, './src/server'),
build: path.resolve(__dirname, 'public')
}
let browserConfig = {
entry: `${paths.browser}/index.js`,
output: {
path: paths.build,
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]___[hash:base64:5]',
sourceMap: true
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.less$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]___[hash:base64:5]',
sourceMap: true
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true
}
},
'postcss-loader'
]
}, {
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
plugins: [
new webpack.DefinePlugin({
__isBrowser__: true
})
],
resolve: {
extensions: ['.js', '.jsx', '.json', '.css', '.scss', '.sass', '.less']
}
}
let serverConfig = {
entry: `${paths.server}/index.js`,
target: 'node',
externals: [nodeExternals()],
output: {
path: __dirname,
filename: 'server.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]___[hash:base64:5]',
sourceMap: true
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.less$/,
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]___[hash:base64:5]',
sourceMap: true
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true
}
},
'postcss-loader'
]
}, {
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
plugins: [
new webpack.DefinePlugin({
__isBrowser__: false
})
],
resolve: {
extensions: ['.js', '.jsx', '.json', '.css', '.scss', '.sass', '.less']
}
}
module.exports = [browserConfig, serverConfig]
server.js
import express from "express"
import cors from "cors"
import React from "react"
import bodyParser from 'body-parser'
import serialize from "serialize-javascript"
import { renderToString } from "react-dom/server"
import { StaticRouter, matchPath } from "react-router-dom"
import App from '../shared/App'
import routes from '../shared/routes'
const app = express()
const port = process.env.PORT || 3000
app.use(cors())
app.use(bodyParser.json()) // support json encoded bodies
app.use(bodyParser.urlencoded({extended: true})) // support encoded bodies
app.use(express.static("public"))
app.get("*", (req, res, next) => {
const activeRoute = routes.find((route) => matchPath(req.url, route)) || {}
const promise = activeRoute.fetchInitialData
? activeRoute.fetchInitialData(req.path)
: Promise.resolve()
promise.then((data) => {
const css = new Set()
const context = { data, insertCss: (...styles) => styles.forEach(style => css.add(style._getCss())) }
const markup = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>React on the Server!</title>
<script src="/bundle.js" defer></script>
<script>window.__INITIAL_DATA__ = ${serialize(data)}</script>
</head>
<body>
<div id="app">${markup}</div>
</body>
</html>
`)
}).catch(next)
})
app.listen(port, () => console.log(`Server is listening on port: ${port}`))
routes.js
import AboutMain from './components/About/AboutMain'
const routes = [
{
path: '/about',
component: AboutMain
}
]
export default routes
browser.js
// Import the neccessary modules for use in file
import React from 'react' // Main React module
import { hydrate } from 'react-dom' // render alternative for server rendering
import App from '../shared/App'
import { BrowserRouter } from 'react-router-dom' // React Router component for client side routing
import '../shared/components/global.scss' // Only has general rules, which do get applied
hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('app')
)
App.js
import React, { Component } from 'react'
import routes from './routes'
import { Route, Link, Redirect, Switch } from 'react-router-dom'
class App extends Component {
render() {
return (
<div>
<Switch>
{routes.map(({ path, exact, component: Component, ...rest }) => (
<Route key={path} path={path} exact={exact} render={(props) => (
<Component {...props} {...rest} />
)} />
))}
<Route render={(props) => <NoMatch {...props} /> } />
</Switch>
</div>
)
}
}
export default App
AboutMain.js
// Importing Required Modules
import React, {Component, Fragment} from 'react' // Importing React, Component, Fragment from "react"
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './about.scss'
class AboutMain extends Component {
state = {
phrase: 'We Made It!'
}
render() {
return (
<Fragment>
<header className={s.banner}>
<h1 className={s.heading}>{this.state.phrase}</h1>
</header>
</Fragment>
)
}
}
export default withStyles(s)(AboutMain) <-- Error Seems to occur here, at least I think.
about.scss
.banner {
margin: 0 auto;
padding: 15px;
border: 2px solid #000;
}
.heading {
text-transform: uppercase;
text-decoration: underline;
}
The problem went away simply because you removed isomorphic-style-loader. Please don't accept your own answer like that. The problem here is you did not provide a context so insertCss.apply(_context, styles) will complain because _context is undefined. To solve the problem, follow these steps:
Create a ContextProvider component in the same directory of App
ContextProvider.js
import React from 'react';
import PropTypes from 'prop-types'
import App from './App'
class ContextProvider extends React.Component {
static childContextTypes = {
insertCss: PropTypes.func,
}
getChildContext() {
return { ...this.props.context }
}
render () {
return <App { ...this.props } />
}
}
export default ContextProvider
Wrap the ContextProvider in BOTH your browser.js and server.js. Remember to declare the context in both files.
browser.js (in other apps, this would be root client code, i.e client.js or index.js)
// Import the neccessary modules for use in file
/* import statements */
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss());
return () => {
removeCss.forEach(f => f());
};
},
}
hydrate(
<BrowserRouter>
//ContextProvider wraps around and returns App so we can do this
<ContextProvider context={context} />
</BrowserRouter>,
document.getElementById('app')
)
server.js
//Additional code above
app.get("*", (req, res, next) => {
const activeRoute = routes.find((route) => matchPath(req.url, route)) || {}
const promise = activeRoute.fetchInitialData
? activeRoute.fetchInitialData(req.path)
: Promise.resolve()
promise.then((data) => {
const css = new Set()
const context = { insertCss: (...styles) =>
styles.forEach(style => css.add(style._getCss()))}
const markup = renderToString(
<StaticRouter location={req.url}>
<ContextProvider context={context}>
<App />
</ContextProvider>
</StaticRouter>
)
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>React on the Server!</title>
<script src="/bundle.js" defer></script>
<script>window.__INITIAL_DATA__ = ${serialize(data)}</script>
<style type="text/css">${[...css].join('')}</style>
</head>
<body>
<div id="app">${markup}</div>
</body>
</html>
`)
}).catch(next)
})
app.listen(port, () => console.log(`Server is listening on port: ${port}`))
I wrote an article explaining this in more detail here: https://medium.com/#danielnmai/load-css-in-react-server-side-rendering-with-isomorphic-style-loader-848c8140a096
After reviewing the code all night and endlessly searching Google. I found a fix that the main issue with my code was in the webpack.config.babel.js setup.
I changed the browser test for sass || scss to use style-loader, rather than the isomorphic-style-loader. I also removed all isomorphic-style-loader logic from my app (ie. withStyles, insertCss, etc.)
I'm not sure if this was the correct approach, but for the meantime, it seems to fix my problem and does not return any errors or network issues.

React-hot-loader doesn't work with React-router-dom

So I've finally setup a working project with:
Electron (2.0.2)
React (16.4.0)
React-router-dom (4.2.2)
Webpack (4.11.0)
React-hot-loader (4.2.0)
And just when I started to develop some react components I noticed my project won't hot reload correctly. If I adjust something on the base url (/) it is updated correctly, but if I update something on a secondary url, say /test the webpack compiles, but I get the message Cannot GET /test.
I've tried a lot and I cannot seem to figure out what I am doing wrong. I looked into react-router-dom, since hot-reloading was an issue back in version 3.x, but they say it should be resolved now (in 4.x --> It works here..). Also i've added <base href="/"/> in my index.html so that is not it.
Can anyone tell me what I am doing wrong?
Webpack.common.js (This is merged into Webpack.dev.js)
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
},
resolve: {
modules: [path.resolve(__dirname), 'node_modules']
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
cacheDirectory: true,
presets: ['env', 'react'],
plugins: ['transform-runtime'],
env: {
development: {
plugins: ['react-hot-loader/babel']
},
production: {
presets: ['react-optimize']
}
}
}
}
]
}
};
Webpack.dev.js
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
'app': [
'babel-polyfill',
'react-hot-loader/patch',
path.join(__dirname, 'src', 'index.js')
]
},
plugins: [
new webpack.HotModuleReplacementPlugin() // Enable hot module replacement
]
});
Index.js
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";
import { App } from "./app";
const render = Component => {
ReactDOM.render(
<AppContainer>
<Component/>
</AppContainer>,
document.getElementById("root")
);
};
render(App);
if (module.hot) {
module.hot.accept("./app", () => {
render(App);
});
}
App.js (my main entry point for my app, thus where I define my base routing)
import React, { Component } from 'react';
import { BrowserRouter, Route, NavLink } from 'react-router-dom';
import { Test } from './components/test';
import { Test2 } from './components/test2';
export class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<NavLink to="/">Home</NavLink>
<NavLink to="/test">Test</NavLink>
<div>
<Route exact path="/" component={Test}/>
<Route path="/test" component={Test2}/>
</div>
</div>
</BrowserRouter>
);
}
}
And the components 'test' and 'test2' are just plain simple react components with a 'hello world' text.
Anyone who sees anything that I am missing or doing wrong?
Thanks to this tutorial I found a way to adapt my project and get hot loading to work. It even made my code a bit cleaner and my build scripts simpeler.
Webpack.common.js
The first thing I needed to change was the babel-loader. I stole it from some tutorial, and it worked, but I did not know exactly what it did so I got rid of that code. I've also made the compilation of my code faster through the webpack.DllReferencePlugin.
Here is the updated webpack.common.js:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
entry: {
app: [
'babel-polyfill',
'./src/index.js',
],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
plugins: ['react-hot-loader/babel'],
cacheDirectory: true,
presets: ['env', 'react'],
},
}
],
},
plugins: [
new webpack.DllReferencePlugin({
context: path.join(__dirname),
manifest: require('../dist/vendor-manifest.json'),
}),
new HtmlWebpackPlugin({
title: '<my-app-name>',
filename: 'index.html',
template: './public/index.html',
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, '../dist/*.dll.js'),
includeSourcemap: false // add this parameter
})
],
};
The AddAssetHtmlPlugin is required since the index.html is dynamically created (by the HtmlWebpackPlugin) for the dev server and you cannot hardcode the correct bundle import for the vendor.dll and the app.bundle (more here).
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
const path = require('path');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
hot: true,
contentBase: path.resolve(__dirname, 'dist'),
historyApiFallback: true // Allow refreshing of the page
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // Enable hot reloading
]
});
What did I change:
I moved the entry point up to webpack.common.
I Removed the 'react-hot-loader/patch' from the entry
(optional) I've added some config options for the webpack-dev-server.
Index.js
This is the file that caused the hot-reload to fail. Especially the if(module.hot) part caused it to fail. So I've changed it to the following:
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { App } from './app';
const render = () => {
ReactDOM.render(
<AppContainer>
<App/>
</AppContainer>,
document.getElementById('app'),
);
};
render(App);
if (module.hot) {
module.hot.accept('./app', () => {
const NextApp = require('./app').default; // Get the updated code
render(NextApp);
});
}
The reason it works now is because now I fetch the new app and replace the old one, thus telling the hot-loader there has been a change. I could also just use module.hot.accept(), but that would make the react-hot-loader useless (you make use of the webpack hot-reloader) and this way I would also lose the state within my components every time I updated some code.
So there you go. I hope this will help anyone (other then myself).

Hot module replacement - Updating but not re-rendering

I am running an express server that will act as an API for my React app that is being bundled and served by webpack-dev-server.
I am trying to get hot module replacement to work, and am almost there, when I make changes to my files, I get this in the console:
But the app is never re-rendered, unless manually refreshed. Unaware if this is relevant, but when I update my .scss files, it refreshes without manually doing so, and updates as I would expect.
Versions:
"webpack": "2.1.0-beta.22"
"webpack-dev-server": "2.1.0-beta.8"
"react-hot-loader": "3.0.0-beta.5"
I tried the latest webpack but it gave me validation errors that could not be overcome.
I am running webpack via: "webpack": "webpack-dev-server --port 4000 --env.dev", and my express server is being ran on http://localhost:3000.
Here is my webpack.config.babel.js:
const webpack = require('webpack');
const { resolve, join } = require('path');
const { getIfUtils, removeEmpty } = require('webpack-config-utils')
const getEntry = (ifDev) => {
let entry
if (ifDev) {
entry = {
app: [
'react-hot-loader/patch',
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:4000/',
'./js/index.js'
],
vendor: ['react']
}
} else {
entry = {
bundle: './js/index.js',
vendor: ['react']
}
}
return entry
}
const config = env => {
const { ifProd, ifDev } = getIfUtils(env)
return {
entry: getEntry(ifDev),
output: {
path: resolve('./public/dist/'),
publicPath: 'http://localhost:4000/',
filename: '[name].bundle.js',
},
context: resolve(__dirname, 'assets'),
devtool: env.prod ? 'source-map' : 'eval',
devServer: {
contentBase: resolve('./public/dist/'),
headers: { 'Access-Control-Allow-Origin': '*' },
publicPath: 'http://localhost:4000/',
hot: true,
noInfo: true,
inline: true
},
bail: env.prod,
module: {
loaders: [
{ test: /\.scss$/, loaders: [ 'style', 'css', 'sass' ], exclude: /node_modules|lib/ },
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loaders: [ 'babel-loader' ] },
{ test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, loader: 'file-loader' }
]
},
resolve: {
extensions: ['.js', '.jsx']
},
plugins: removeEmpty([
ifDev(new webpack.NoErrorsPlugin()),
ifDev(new webpack.NamedModulesPlugin()),
ifDev(new webpack.HotModuleReplacementPlugin()),
new webpack.DefinePlugin({
'process.env': { NODE_ENV: JSON.stringify((env.prod) ? 'production' : 'development') }
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
filename: 'vendor.bundle.js'
}),
ifProd(new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
})),
ifProd(new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
output: { comments: false },
sourceMap: false
}))
]),
}
}
module.exports = config
Here is my .babelrc, where I call react-hot-loader
{
"presets": [["es2015", { modules: false }], "stage-0", "react"],
"plugins": ["react-hot-loader/babel"],
"env": {
"test": {
"plugins": ["istanbul"],
"presets": ["es2015", "stage-0", "react"]
}
},
"sourceMaps": "inline"
}
With React Hot Loader v3 and the Babel transform, you want to do this in the root of your component (where you do your rendering, or where you create your Redux provider):
render(
<AppContainer>
<Root
store={ store }
/>
</AppContainer>,
document.getElementById('root')
);
if (module.hot) {
module.hot.accept('./containers/Root', () => {
const RootContainer = require('./containers/Root').default;
render(
<AppContainer>
<RootContainer
store={ store }
/>
</AppContainer>,
document.getElementById('root')
);
});
}
With the new version of Hot Loader, you have to explicitly accept the hot update with module.hot.accept.
In a more complex Redux project (with routing and hot reloading reducers) you could do something like this:
/**
* Starts the React app with the Router, and renders it to the given DOM container
* #param {DOMElement} container
*/
export default function app(container) {
const store = createStore(
combineReducers({
...reducers,
routing: routerReducer,
form: formReducer,
}),
compose(
applyMiddleware(
routerMiddleware(hashHistory),
thunkMiddleware,
promiseMiddleware
),
process.env.NODE_ENV !== 'production' && window.devToolsExtension ? window.devToolsExtension() : (param) => param
)
);
if (module.hot) {
module.hot.accept('./reducers', () => {
const nextReducers = require('./reducers');
const nextRootReducer = combineReducers({
...nextReducers,
routing: routerReducer,
form: formReducer,
});
store.replaceReducer(nextRootReducer);
});
}
const history = syncHistoryWithStore(hashHistory, store);
render({ store, history, container });
store.dispatch(loadEventsWhenLoggedIn());
if (module.hot) {
module.hot.accept('./render', () => {
const newRender = require('./render').default;
newRender({ store, history, container });
});
}
}
(and render.js)
/**
* Starts the React app with the Router, and renders it to the given DOM container
* #param {DOMElement} container
*/
export default function render({ store, history, container }) {
ReactDOM.render(
<Provider store={store}>
<div className='container'>
<Routes history={history} store={store} />
</div>
</Provider>,
container
);
}
For more examples you should have a look at Dan Abramov's Redux devtools example repo, for example this file: https://github.com/gaearon/redux-devtools/blob/master/examples/todomvc/index.js
HMR works automatically for CSS because style-loader supports that out-of-the-box.
With React that's not the case. You need react-hot-loader.
Install it with npm, and change this:
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loaders: [ 'babel-loader' ] },
To:
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loaders: [ 'react-hot-loader', 'babel-loader' ] },
If you want to know more, I recommend to read "Webpack’s HMR & React-Hot-Loader — The Missing Manual".
I was having this problem, added the module.hot.accept() function and it still wasn't working.
hmr [connected]... but no hot replacement.
*deleted node_module, package-lock.json, dist/build folders...
*changed webpack.config.client.production.js --> added
plugins: [
new webpack.ProvidePlugin({
process: "process/browser"
})
],
resolve:{
alias:{
process: "process/browser"
}
}
then npm install --save-dev process,
added import process from 'process' to bundle entry point (main.js for me)
...
npm install
then run it.

Categories

Resources