Styles' cache for Webpack with Style Loader

Facing style cleanup issues when integrating with 3rd-party apps? Learn how to customize Webpack's style-loader to preserve your styles on page reload.

Styles' cache for Webpack with Style Loader

This is not your everyday scenario. Not even your general scenario of web app development and Webpack usage.

But when you face it, I want this article to help you.

Scenario

When your app works on top of 3ʳᵈ party apps, e.g., customization tools, editors, etc, your app will attach styles on top. In the best case scenario, your app runs fine the 3ʳᵈ party app is not gonna clean up the things on page.

But if it does, you would need a way to keep the cache of the styles you have added to the page. So that you can detect what has been removed & re-add to the page.

When we see it technically, we use Webpack with style–loader to load & attach the css files in the style tag in the page. Normally you don't need to do anything. But in some cases, when the 3ʳᵈ party app does the cleanup on the page, styles will be lost. JS will be fine as it is loaded by JS and added to the execution context internally.

To counter this cleanup of styles from the page, we would need to customize the style loader.

Solution

Here is how we will adjust the style insertion to DOM with style-loader

// ...
{
  test: /\.css$/,
  exclude: /\.excluded\.css$/,
  use: [
    {
      loader: 'style-loader',
      options: {
        attributes: { class: 'my-styles' },
        // Insert at top to let our own custom CSS to override Element React CSS
        insert: require.resolve('./config/styleLoaderInsert.js'),
      },
    },
    {
      loader: 'css-loader',
      options: { importLoaders: 1 },
    },
    'postcss-loader',
  ],
},
// ...

And with above code, here is the styleLoaderInsert.js relative to Webpack config containing above code:

// ./config/styleLoaderInsert.js
window.top.StylesCache = window.top.StylesCache || [];

module.exports = (element) => {
  const insertBefore = (element) => {
    const parent = window.top.document.head;

    if (parent.querySelector(`#${element.id}`)) return;

    const lastInsertedElement = window.top._lastElementInsertedByStyleLoader;

    if (!lastInsertedElement) {
      parent.insertBefore(element, parent.firstChild);
    } else if (lastInsertedElement.nextSibling) {
      parent.insertBefore(element, lastInsertedElement.nextSibling);
    } else {
      parent.appendChild(element);
    }

    window.top._lastElementInsertedByStyleLoader = element;
  };

  // very basic randomizer, you can bring in your own, more robust, function
  const getRandomStyleId = () =>
    [
      new Date().getTime(),
      Math.round(Math.random() * 1000000),
      Math.round(Math.random() * 2000000),
      Math.round(Math.random() * 3000000),
    ].join('_');

  element.id = `style-${getRandomStyleId()}`;

  window.top.StylesCache.push(element);

  for (const StyleCache of window.top.StylesCache) { 
    insertBefore(StyleCache);
  }
};

Few tings to take note of in above function:

  • We are using window.top instead of window; You can stick to window if you don't have multi-frame setup for your application
  • Randomizer function is very rudimentary; please bring in your own random ID generator for style tags if you notice collisions in IDs. You can use uuid libs too

Conclusion

Webpack is configs take time to be fine tuned and performing as per expectations; above modification to style-loader is one such part of work we had to go through at ABTasty.

If you have such stories, I would love to hear them out. Please let me know through comments? or on Twitter at @heypankaj_ and/or @time2hack

If you find this article helpful, please share it with others.

And if you enjoyed this post, consider subscribing to receive new posts right to your inbox.