Alex Verzea
Alex Verzea

Reputation: 441

Webpack config for SSR SCSS

I have a React-TypeScript SSR app where I used SCSS files for my styling. I need to write a rule in Webpack to load the SCSS and I haven't been able to do it.

I found various solutions online, all of which are extremely complex and use things like mini-css-extract-plugin. I couldn't get any of them to work.

I currently have two webpack config files, one for the client (web) and one for the server (node), both of which load the SCSS as such:

{
    test: /\.scss$/,
    use: ["css-loader", "sass-loader"]
}

I also encountered another issue in that I can't use style-loader as it throws an error about the window object. Does anyone have a working example (simple preferably) of loading SCSS in Webpack?

Upvotes: 5

Views: 4654

Answers (3)

hbrannan
hbrannan

Reputation: 161

Webpack Community now details an approach for SSR sass compiled via webpack in mini-css-extract-plugin/#recommend

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== "production";
module.exports = {
    module: {
        rules: [
          {
            test: /\.(sa|sc|c)ss$/,
            use: [
              devMode ? "style-loader" : MiniCssExtractPlugin.loader,
              "css-loader",
              "postcss-loader",
              "sass-loader",
            ],
          },
        ],
     },
     plugins: [].concat(devMode ? [] : [new MiniCssExtractPlugin()]),
    };

Note that style-loader should not be used in an SSR app or in a webpack production build because injects CSS into the DOM. MiniCSSExtractPlugin is recommended for SSR production builds and should not be used with style-loader (window will not be defined in webpack's node-based prod build).

Upvotes: 0

H Ketabi
H Ketabi

Reputation: 3144

A boilerplate for server-side rendering using react, webpack, Sass (for both css-modules and pure sass)

webpack.config.js

const path = require('path');
const isDevelopment = true;

module.exports = [
  {
    name: 'client',
    target: 'web',
    entry: './client.jsx',
    output: {
      path: path.join(__dirname, 'static'),
      filename: 'client.js',
      publicPath: '/static/',
    },
    resolve: {
      extensions: ['.js', '.jsx']
    },
    devtool: 'source-map',
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /(node_modules\/)/,
          use: [
            {
              loader: 'babel-loader',
            }
          ]
        },
        {
          test: /\.scss$/,
          use: [
            {
              loader: 'style-loader',
            },
            {
              loader: "css-loader",
              options: {
                modules: {
                  localIdentName: "[name]__[local]___[hash:base64:5]",
                },
                sourceMap: isDevelopment,
              }
            },
            {
              loader: 'sass-loader'
            }
          ]
        }
      ],
    },
  },
  {
    name: 'server',
    target: 'node',
    entry: './server.jsx',
    output: {
      path: path.join(__dirname, 'static'),
      filename: 'server.js',
      libraryTarget: 'commonjs2',
      publicPath: '/static/',
    },
    devtool: 'source-map',
    resolve: {
      extensions: ['.js', '.jsx']
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /(node_modules\/)/,
          use: [
            {
              loader: 'babel-loader',
            }
          ]
        },
        {
          test: /\.scss$/,
          use: [
            {
              loader: 'isomorphic-style-loader',
            },
            {
              loader: "css-loader",
              options: {
                modules: {
                  localIdentName: "[name]__[local]___[hash:base64:5]",
                },
                sourceMap: isDevelopment,
              }
            },
            {
              loader: 'sass-loader'
            }
          ]
        }
      ],
    },
  }
];

dev dependencies:

npm i -D @babel/cli @babel/preset-es2015  @babel/core @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-react babel-core babel-loader babel-plugin-lodash babel-plugin-react-transform babel-preset-env babel-preset-es2015 babel-preset-react babel-preset-stage-0 css-loader express isomorphic-style-loader node-sass sass-loader style-loader webpack webpack-dev-middleware webpack-hot-middleware webpack-hot-server-middleware

and dependencies:

npm i react react-dom react-helmet react-router-dom

sever.jsx:

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import {Helmet} from "react-helmet";
import Template from './template';
import App from './App';

export default function serverRenderer({ clientStats, serverStats }) {
    return (req, res, next) => {
        const context = {};
        const markup = ReactDOMServer.renderToString(
            <StaticRouter location={ req.url } context={ context }>
                <App />
            </StaticRouter>
        );
        const helmet = Helmet.renderStatic();

        res.status(200).send(Template({
            markup: markup,
            helmet: helmet,
        }));
    };
}

App.jsx:

import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import Menu from './Menu'
import Helmet from "react-helmet";
import homepageStyles from './homepage.scss';

class Homepage extends Component {
    render() {
        return (
            <div className={ homepageStyles.component }>
                <Helmet
                    title="Welcome to our Homepage"
                />
                <Menu />
                <h1>Homepage</h1>
            </div>
        );
    }
}


class About extends Component {
    render() {
        return (
            <div>
                <h1>About</h1>
            </div>
        );
    }
}

class Contact extends Component {
    render() {
        return (
            <div>
                <h1>Contact</h1>
            </div>
        );
    }
}


export default class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
                <Helmet
                    htmlAttributes={{lang: "en", amp: undefined}} // amp takes no value
                    titleTemplate="%s | React App"
                    titleAttributes={{itemprop: "name", lang: "en"}}
                    meta={[
                        {name: "description", content: "Server side rendering example"},
                        {name: "viewport", content: "width=device-width, initial-scale=1"},
                    ]}
                />
                <Switch>
                    <Route exact path='/' component={ Homepage } />
                    <Route path="/about" component={ About } />
                    <Route path="/contact" component={ Contact } />
                </Switch>
            </div>
        );
    }
}

template.jsx

export default ({ markup, helmet }) => {
    return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
    ${helmet.title.toString()}
    ${helmet.meta.toString()}
    ${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
    <div id="root">${markup}</div>
    <div>Heeeeeeeeeeeeeeeeeeeelmet</div>
    <script src="/static/client.js" async></script>
</body>
</html>`;
};

menu.jsx:

import { Link } from 'react-router-dom';
import React, { Component } from 'react';
import './menu.scss';

class Menu extends Component {
    render() {
        return (
            <div>
                <ul>
                    <li>
                        <Link to={'/'}>Homepage</Link>
                    </li>
                    <li>
                        <Link to={'/about'}>About</Link>
                    </li>
                    <li>
                        <Link to={'/contact'}>Contact</Link>
                    </li>
                </ul>
            </div>
        );
    }
}

export default Menu;

.babelrc:

{
 "presets": [
   "@babel/react",
   "@babel/preset-env"
 ],
 "plugins": [
   "@babel/proposal-class-properties"
 ]
}

homepage.sccs

.component {
    color: blue;
}

menu.scss:


li {
  background-color: yellow;
}

I used this article:

https://blog.digitalkwarts.com/server-side-rendering-with-reactjs-react-router-v4-react-helmet-and-css-modules/

Upvotes: 0

Hamit YILDIRIM
Hamit YILDIRIM

Reputation: 4549

You are on right track with 2 web config file you can use

https://gist.github.com/mburakerman/629783c16acf5e5f03de60528d3139af

But don't set any other config file like babel.rc .yaml etc or other definition in project.json

try this

        test: /\.scss$/,
        use: [
            MiniCssExtractPlugin.loader,
            'css-loader',
            'postcss-loader',
            'sass-loader'
        ]

//..

plugins: [
    new MiniCssExtractPlugin({
        filename: 'assets/css/bundle-[contenthash].css',
        chunkFilename: 'assets/css/bundle-[contenthash].css'
    })
],

Look full example https://github.com/dewelloper/pzone/blob/master/webpack.config.store.js

Upvotes: 1

Related Questions