javascript - Inject per-component style tags dynamically with Rollup and scss - Stack Overflow

I am building a React ponent library whose source code takes this general structure:- src- mon.scss (co

I am building a React ponent library whose source code takes this general structure:

- src
  - mon.scss (contains things like re-usable css variables)
  - ponents
    - button
      - index.js
      - button.scss
    - dialog
      - index.js
      - dialog.scss

My ponents are responsible for importing their own per-ponent styles (using scss), so for example, button/index.js has this line:

import "./button.scss";

So far, in my application I have been consuming my library directly from source like this:

// app.js
import "mylib/src/mon.scss" // load global styles
import Button from 'mylib/src/ponents/button/index.js'
import Dialog from 'mylib/src/ponents/dialog/index.js'

// ...application code...

When my application uses webpack, along with style-loader, the per-ponent css is appended as style tags in head dynamically when the ponent is first used. This is a nice performance win since the per-ponent styling doesn't need to be parsed by the browser until it's actually needed.

Now though, I want to distribute my library using Rollup, so application consumers would do something like this:

import { Button, Dialog } from 'mylib'
import "mylib/mon.css" // load global styles

// ...application code...

When I use rollup-plugin-scss it just bundles the per-ponent styles all together, not dynamically adding them as before.

Is there a technique I can incorporate into my Rollup build so that my per-ponent styles are dynamically added as style tags in the head tag as they are used?

I am building a React ponent library whose source code takes this general structure:

- src
  - mon.scss (contains things like re-usable css variables)
  - ponents
    - button
      - index.js
      - button.scss
    - dialog
      - index.js
      - dialog.scss

My ponents are responsible for importing their own per-ponent styles (using scss), so for example, button/index.js has this line:

import "./button.scss";

So far, in my application I have been consuming my library directly from source like this:

// app.js
import "mylib/src/mon.scss" // load global styles
import Button from 'mylib/src/ponents/button/index.js'
import Dialog from 'mylib/src/ponents/dialog/index.js'

// ...application code...

When my application uses webpack, along with style-loader, the per-ponent css is appended as style tags in head dynamically when the ponent is first used. This is a nice performance win since the per-ponent styling doesn't need to be parsed by the browser until it's actually needed.

Now though, I want to distribute my library using Rollup, so application consumers would do something like this:

import { Button, Dialog } from 'mylib'
import "mylib/mon.css" // load global styles

// ...application code...

When I use rollup-plugin-scss it just bundles the per-ponent styles all together, not dynamically adding them as before.

Is there a technique I can incorporate into my Rollup build so that my per-ponent styles are dynamically added as style tags in the head tag as they are used?

Share Improve this question edited Jul 3, 2019 at 20:09 Jonathan.Brink asked Jul 3, 2019 at 17:05 Jonathan.BrinkJonathan.Brink 25.5k20 gold badges83 silver badges125 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 4 +150

One approach would be to load your SCSS as a CSS stylesheet string the output:false option in the plugin (see the Options section of the docs), then in your ponent use react-helmet to inject the stylesheet at runtime:

import ponentCss from './myComponent.scss'; // plain CSS from rollup plugin
import Helmet from 'react-helmet';

function MyComponent(props) {
    return (
        <>
             <ActualComponentStuff {...props} />
             <Helmet>
                 <style>{ ponentCss }</style>
             </Helmet>
        </>
    );
}

This basic idea should work, but I wouldn't use this implementation for 2 reasons:

  1. Rendering two instances of MyComponent will cause the stylesheet to be injected twice, causing lots of unnecessary DOM injection
  2. It's a lot of boilerplate to wrap around every ponent (even if we factor out our Helmet instance into a nice wrapper)

Therefore you're better off using a custom hook, and passing in a uniqueId that allows your hook to de-duplicate stylesheets. Something like this:

// -------------- myComponent.js -------------------
import ponentCss from "./myComponent.scss"; // plain CSS from rollup plugin
import useCss from "./useCss";

function MyComponent(props) {
    useCss(ponentCss, "my-ponent");
    return (
        <ActualComponentStuff {...props} />
    );
}

// ------------------ useCss.js ------------------
import { useEffect } from "react";

const cssInstances = {};

function addCssToDocument(css) {
    const cssElement = document.createElement("style");
    cssElement.setAttribute("type", "text/css");

    //normally this would be dangerous, but it's OK for
    // a style element because there's no execution!
    cssElement.innerHTML = css;
    document.head.appendChild(cssElement);
    return cssElement;
}

function registerInstance(uniqueId, instanceSymbol, css) {
    if (cssInstances[uniqueId]) {
        cssInstances[uniqueId].symbols.push(instanceSymbol);
    } else {
        const cssElement = addCssToDocument(css);
        cssInstances[uniqueId] = {
            symbols: [instanceSymbol],
            cssElement
        };
    }
}

function deregisterInstance(uniqueId, instanceSymbol) {
    const instances = cssInstances[uniqueId];
    if (instances) {
        //removes this instance by symbol
        instances.symbols = instances.symbols.filter(symbol => symbol !== instanceSymbol);

        if (instances.symbols.length === 0) {
            document.head.removeChild(instances.cssElement);
            instances.cssElement = undefined;
        }
    } else {
        console.error(`useCss() failure - tried to deregister and instance of ${uniqueId} but none existed!`);
    }
}

export default function useCss(css, uniqueId) {
    return useEffect(() => {
        // each instance of our ponent gets a unique symbol
        // to track its creation and removal
        const instanceSymbol = Symbol();

        registerInstance(uniqueId, instanceSymbol, css);

        return () => deregisterInstance(uniqueId, instanceSymbol);
    }, [css, uniqueId]);
}

This should work much better - the hook will use effectively a app-wide global to track instances of your ponent, add the CSS dynamically when it gets first rendered, and remove it when the last ponent dies. All you need to do is add that single hook as an extra line in each of your ponents (assuming you're using only function React ponents - if you're using classes you'll need to wrap them, maybe using a HOC or similar).

It should work fine, but it also has some drawbacks:

  1. We're effectively using global state (cssInstances, which is kind of unavoidable if we're trying to prevent clashes from different parts of the React tree. I was hoping there would be a way to do this by storing state in the DOM itself (this makes sense given that our de-duplication stage is the DOM), but I couldn't find one. Another way would be to use the React Context API instead of a module-level global. This would work fine too and be easier to test; shouldn't be hard to rewrite the hook with useContext() if that's what you want, but then the integrating app would need to set up a Context provider at the root level and that creates more work for integrators, more documentation, etc.

    1. The entire approach of dynamically adding/removing style tags means that stylesheet order is not only non-deterministic (which it already is when doing style loading with bundlers like Rollup), but also can change during runtime, so if you have stylesheets that conflict, the behaviour might change during runtime. Your stylesheets should ideally be too tightly scoped to conflict anyway, but I have seen this go wrong with a Material UI app where multiple instances of MUI were loaded - it's real hard to debug!

The dominant approach at the moment seems to be JSS - using something like nano-renderer to turn JS objects into CSS and then injecting them. There doesn't seem to be anything I can find that does this for textual CSS.

Hope this is a useful answer. I've tested the hook itself and it works fine, but I'm not totally confident with Rollup so I'm relying on the plugin documentation here. Either way, good luck with the project!

Use rollup-plugin-styles instead. It will inject styles to head tag on a per ponent basis if you use mode: inject which is the default. See doc

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744802348a4594569.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信