webpacksasswebpack-style-loaderhot-module-replacement

Page does full reload on scss changes, even with HMR enabled


I would like my workflow to update styles without refreshing the browser. I see in the console that HMR is enabled. However, updating a scss file causes the browser to fully refresh. This is specially inconvenient in development when working on a view that loads data from an API.

I'm using webpack 4.42.0, webpack-dev-server 3.11.0. Full package.json:

{
  "name": "example",
  "version": "1.0.0",
  "description": "example",
  "main": "index.js",
  "scripts": {
    "webpack": "webpack",
    "start": "webpack-dev-server --mode development",
    "build": "webpack --mode production",
    "serve": "node server/index.js",
    "test": "jest --watch",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  "babel": {
    "presets": [
      "@babel/preset-env",
      "@babel/preset-react"
    ],
    "plugins": [
      "@babel/plugin-proposal-class-properties"
    ]
  },
  "browserslist": [
    "last 4 version",
    "ie 10",
    "ie 11",
    "Safari >= 8",
    "iOS >= 8",
    "Android >= 4.4"
  ],
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.9.0",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/polyfill": "^7.8.7",
    "@babel/preset-env": "^7.9.0",
    "@babel/preset-react": "^7.9.1",
    "@storybook/addon-actions": "^5.3.17",
    "@storybook/addon-links": "^5.3.17",
    "@storybook/addons": "^5.3.17",
    "@storybook/react": "^5.3.17",
    "autoprefixer": "^9.7.4",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.4.2",
    "dotenv-webpack": "^1.8.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.2",
    "file-loader": "^6.0.0",
    "html-webpack-plugin": "^4.3.0",
    "jest": "^25.3.0",
    "jest-enzyme": "^7.1.2",
    "node-sass": "^4.14.1",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^8.0.2",
    "storybook-react-router": "^1.0.8",
    "style-loader": "^1.1.3",
    "svg-spritemap-webpack-plugin": "^3.5.4",
    "url-loader": "^4.1.0",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.28",
    "@fortawesome/pro-light-svg-icons": "^5.13.0",
    "@fortawesome/pro-regular-svg-icons": "^5.13.0",
    "@fortawesome/pro-solid-svg-icons": "^5.13.0",
    "@fortawesome/react-fontawesome": "^0.1.9",
    "axios": "^0.19.2",
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "firebase": "^7.15.0",
    "prop-types": "^15.7.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2"
  }
}

webpack.config.js:

const path = require('path');
const SVGSpritemapPlugin = require('svg-spritemap-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Dotenv = require('dotenv-webpack');
const env = process.env.NODE_ENV;

module.exports = {
    entry: './app/src/js/index.js',
    output: {
        path: path.resolve( __dirname, 'app/dist'),
        filename: 'js/main.js',
    },
    resolve: {
        extensions: ['.js', '.jsx', '.json'],
    },
    devtool: 'source-map',
    devServer: {
        publicPath: '/',
        contentBase: './app/src', // './app/dist/js',
        watchContentBase: true,
        historyApiFallback: true,
        open: true,
        hotOnly: true,
        inline: true,
        proxy: {
            '/api': 'http://localhost:4001',
        },
    },
    module: {
        rules: [
            { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
            { 
                test: /\.s(a|c)ss$/, 
                exclude: /node_modules/, 
                use: [
                    'style-loader',
                    'css-loader', 
                    'postcss-loader', 
                    'sass-loader'
                ] 
            },
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                loader: 'url-loader'
            }
        ],
    },
    plugins: [
        new Dotenv(),
        new SVGSpritemapPlugin('./app/src/svg/*.svg', {
            output: {
                filename: 'svg/icons.svg',
            },
            sprite: {
                prefix: false,
                generate: {
                    title: false,
                },
            },
        }),
        new HtmlWebpackPlugin({
            template: './index.html',
            filename: 'index.html',
        }),
    ]
};

Project structure:

- root
- - /app
- - - /dist
- - - /src
- - - - /js
- - - - - /components
- - - - - index.js
- - - - /sass
- - - - - /components
- - - - - /globals
- - - - - style.scss
- - /server

index.js imports style.scss


Solution

  • In case someone stumbles into this, my problem was a missing publicPath property on the output object; by setting it to match the publicPath in devServer, style updates now update without refreshing:

    output: {
        path: path.resolve( __dirname, 'app/dist'),
        filename: 'js/main.js',
    +   publicPath: '/app/dist/'
    },
    ...
    devServer: {
        publicPath: '/app/dist/',
        ...
    }
    

    For js changes, I also added if (module && module.hot) module.hot.accept(); to my entry file.