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 component use react-helmet
to inject the stylesheet at runtime:
import componentCss from './myComponent.scss'; // plain CSS from rollup plugin
import Helmet from 'react-helmet';
function MyComponent(props) {
return (
<>
<ActualComponentStuff {...props} />
<Helmet>
<style>{ componentCss }</style>
</Helmet>
</>
);
}
This basic idea should work, but I wouldn't use this implementation for 2 reasons:
- Rendering two instances of
MyComponent
will cause the stylesheet to be injected twice, causing lots of unnecessary DOM injection
- It's a lot of boilerplate to wrap around every component (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 componentCss from "./myComponent.scss"; // plain CSS from rollup plugin
import useCss from "./useCss";
function MyComponent(props) {
useCss(componentCss, "my-component");
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 component 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 component, add the CSS dynamically when it gets first rendered, and remove it when the last component dies. All you need to do is add that single hook as an extra line in each of your components (assuming you're using only function React components - 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:
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.
- 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!