1

How CSS files will load dynamically using react-loadable library on client side?

I have included react-loadable library on both server and client side rendering, from server-side rendering everything works fine but client side, how CSS will load dynamically?

webpack.config.prod.js : Client/Server -

'use strict';

const autoprefixer = require('autoprefixer');
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractCssChunks = require('extract-css-chunks-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const eslintFormatter = require('react-dev-utils/eslintFormatter');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const paths = require('./paths');
const getClientEnvironment = require('./env');
const { ReactLoadablePlugin } = require('react-loadable/webpack');

const publicPath = paths.servedPath;

const shouldUseRelativeAssetPaths = publicPath === './';

const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const publicUrl = publicPath.slice(0, -1);
const env = getClientEnvironment(publicUrl);
if (env.stringified['process.env'].NODE_ENV !== '"production"') {
  throw new Error('Production builds must have NODE_ENV=production.');
}
const cssFilename = 'static/css/[name].[contenthash:8].css';

const client = {
  bail: true,
  devtool: shouldUseSourceMap ? 'source-map' : false,
  entry: [require.resolve('./polyfills'), paths.appIndexJs],
  output: {
    // The build folder.
    path: paths.appBuild,
    filename: 'static/js/[name].[chunkhash:8].js',
    chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
    publicPath,
    devtoolModuleFilenameTemplate: info =>
      path
        .relative(paths.appSrc, info.absoluteResourcePath)
        .replace(/\\/g, '/'),
  },
  resolve: {
    modules: ['node_modules', paths.appNodeModules].concat(
  process.env.NODE_PATH.split(path.delimiter).filter(Boolean)),
    extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
    alias: {
      'react-native': 'react-native-web',
    },
    plugins: [
      new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),

    ],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.(js|jsx|mjs)$/,
        enforce: 'pre',
        use: [
          {
            options: {
              formatter: eslintFormatter,
              eslintPath: require.resolve('eslint'),

            },
            loader: require.resolve('eslint-loader'),
          },
        ],
        include: paths.appSrc,
      },
      {
        oneOf: [
   
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          // Process JS with Babel.
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {
              compact: true,
              plugins: ['react-loadable/babel'],
            },
          },
          {
            test: /\.(?:css|less)$/,
            use: ExtractCssChunks.extract({
              use: [
                {
                  loader: 'css-loader?modules',
                  options: {
                    minimize: true,
                    sourceMap: shouldUseSourceMap,
                    importLoaders: true,
                    localIdentName: '[name]__[local]__[hash:base64:7]',
                  },
                },
                {
                  loader: 'less-loader',
                  options: {
                    minimize: true,
                    sourceMap: shouldUseSourceMap,
                    importLoaders: true,
                  },
                },
                {
                  loader: require.resolve('postcss-loader'),
                  options: {
                  ident: 'postcss',
                    plugins: () => [
                      require('postcss-flexbugs-fixes'),
                      autoprefixer({
                        browsers: [
                          '>1%',
                          'last 4 versions',
                          'Firefox ESR',
                          'not ie < 9',
                        ],
                        flexbox: 'no-2009',
                      }),
                    ],
                  },
                },
              ],
              fallback: 'style-loader',
            }),
            exclude: /\.(eot|woff|woff2|ttf|otf|svg)(\?[\s\S]+)?$/,
          },
          {
            loader: require.resolve('file-loader'),
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new InterpolateHtmlPlugin(env.raw),
    new HtmlWebpackPlugin({
      inject: true,
      template: paths.appHtml,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: false,
        minifyCSS: true,
        minifyURLs: true,
      },
    }),
    new webpack.DefinePlugin(env.stringified),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
        comparisons: false,
      },
      mangle: {
        safari10: true,
      },
      output: {
        comments: false,
        ascii_only: true,
      },
      sourceMap: shouldUseSourceMap,
    }),
    new ExtractCssChunks({
      filename: cssFilename,
    }),
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest.js',
      minChunks: Infinity,
    }),
    new ManifestPlugin({
      fileName: 'asset-manifest.json',
    }),
    new ReactLoadablePlugin({
      filename: './build/react-loadable.json',
    }),
    new SWPrecacheWebpackPlugin({
      dontCacheBustUrlsMatching: /\.\w{8}\./,
      filename: 'service-worker.js',
      logger(message) {
        if (message.indexOf('Total precache size is') === 0) {
          return;
        }
        if (message.indexOf('Skipping static resource') === 0) {
          return;
        }
        console.log(message);
      },
      minify: true,
      navigateFallback: `${publicUrl}/index.html`,
      navigateFallbackWhitelist: [/^(?!\/__).*/],
      staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
    }),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],

  node: {
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty',
  },
};

// Server render
const nodeExternals = require('webpack-node-externals');
const server = Object.assign({}, client);
server.target = 'node';
server.node = {
  __filename: true,
  __dirname: true,
};
server.externals = [nodeExternals()];
server.entry = [
  './server/middleware/renderer.js',
];

delete server.devtool;
delete server.node;
server.module = {};
server.plugins = [
  new webpack.HashedModuleIdsPlugin(),
  new webpack.optimize.LimitChunkCountPlugin({
    maxChunks: 1,
  }),
];
server.output = {
  path: paths.appBuild,
  filename: 'handleRender.js',
  publicPath,
  libraryTarget: 'commonjs2',
};
server.module.rules = [{
  test: /\.(?:js|jsx)$/,
  exclude: /node_modules/,
  loader: require.resolve('babel-loader'),
  options: {
    compact: true,
    plugins: ['react-loadable/babel'],
  },
},
{
  test: /\.(?:css|less)$/,
  loader: 'css-loader/locals?modules&localIdentName=[name]__[local]__[hash:base64:7]!less-loader',
  exclude: /\.(eot|woff|woff2|ttf|otf|svg)(\?[\s\S]+)?$/,
}];

module.exports = [server, client];

Server index.js:

...
import Loadable from 'react-loadable';
import serverRenderer from '../build/handleRender.js';
...
router.use('*', serverRenderer);
...
app.use(router);
// Pre-load all compoenents
Loadable.preloadAll().then(() => {
  app.listen(PORT, (error) => {
    if (error) {
      return console.log('something bad happened', error);
    }
    console.log(`listening on ${PORT}...`);
  });
}).catch((e) => {
  console.log('Loadable Error : ', e);
});

renderer.js:

import { renderToStringWithData } from 'react-apollo';
import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
...

const mainApp = renderToStringWithData(<Loadable.Capture
      report={moduleName => modules.push(moduleName)}
    >
      <App req={req} context={context} client={client} />
    </Loadable.Capture>);
...
  const bundles = getBundles(JSON.parse(stats), modules);
  const styles = bundles.filter(bundle =>                   bundle.file.endsWith('.css'));
  const scripts = bundles.filter(bundle => bundle.file.endsWith('.js'));    

...
//mainApp=>html
   const replacedStyle = html.replace(
            '<link id="codeSplittingStyle">',
            styles.map(bundle => `<link
                rel="stylesheet"
                href="/${bundle.file}"/>`).join('\n'),
          );

          const replacedScript = replacedStyle.replace(
            '<script id="codeSplittingScript"></script>',
            scripts.map(bundle => `<script
                type="text/javascript"
                src="/${bundle.file}"></script>`).join('\n'),
          );
...
   return res.send(replacedScript);    

Browser.js:

import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';

import Browser from './layout/browser';
import registerServiceWorker from './registerServiceWorker';

Loadable.preloadReady().then(() => {
  ReactDOM.hydrate(<Browser />, document.getElementById('root'));
});
registerServiceWorker();
ashbuilds
  • 1,401
  • 16
  • 33
Nithish
  • 369
  • 1
  • 8
  • 20
  • Basically, CSS loading will be taken care by the webpack loaders. Look into your webpack config file. – Akhil P Apr 23 '18 at 10:41
  • which loader is used for dynamically loading css ? – Nithish Apr 23 '18 at 10:44
  • Share your webpack config. Normally css-loader will be used for that. – Akhil P Apr 23 '18 at 10:47
  • webpack.config.client.js => module.rules:[{ test: /\.(css)?$/, use: ExtractCssChunks.extract({ use: { loader: 'css-loader', options: { modules: true, localIdentName: '[local]',//[name]__[local]--[hash:base64:5] } } }] ,plugins: [ new ExtractCssChunks({ filename: '[name].css', }), ] – Nithish Apr 23 '18 at 10:49
  • not the file name. File content. One can do nothing with the filename – Akhil P Apr 23 '18 at 10:50
  • module.rules:[{ test: /\.(css)?$/, use: ExtractCssChunks.extract({ use: { loader: 'css-loader', options: { modules: true, localIdentName: '[local]',//[name]__[local]--[hash:base64:5] } } }] ,plugins: [ new ExtractCssChunks({ filename: '[name].css', }), ] – Nithish Apr 23 '18 at 10:52
  • Now you have the answer with you. css-loader is actually extracting the css imports content from your application and attaching it. read css-loader documentation for more details. – Akhil P Apr 23 '18 at 10:53
  • I have checked css-loader documentation but didn't find anything, if you have any solution then share it. – Nithish Apr 23 '18 at 10:59

1 Answers1

1

Please have a look at "Desired output" section in extract-css-chunks-webpack-plugin repo (https://github.com/faceyspacey/extract-css-chunks-webpack-plugin#desired-output). It states that:

webpack-flush-chunks will scoop up the exact stylesheets to embed in your response. It essentially automates producing the above.

So you need to use webpack-flush-chunks in order to generate cssHash which is essential part of dynamic css loading as it determines when css chunks should be loaded.

  • do you need to be using react-universal-component to do this? or will flush chunks work with react loadable? – Thomas Sep 15 '18 at 05:07