I'm hosting legacy React components in an Angular app using a method similar to this one. There are lots of React components to bring in. They all import their own CSS with JS module imports:
import "./Admin.css";
This means every component is responsible for importing its own stylesheet, which is great. If I could just get the compiler to be happy with this like it was in the React app, that would be the best possible outcome.
The problem is this doesn't work when the React component is hosted in Angular. Importing a CSS file this way breaks compilation:
X [ERROR] Could not resolve "./Admin.css"
src/app/components/react/Admin/Admin.tsx:3:7:
3 │ import "./Admin.css";
╵ ~~~~~~~~~~~~~
Instead, I have to use CSS imports to include the React component's stylesheet in the .scss file of the Angular host:
// Inside AdminHost.component.scss
@import '../../react/Admin/Admin';
This comes with a much bigger problem: Now that each React component isn't responsible for its own styles, I have to dig through all of Admin.tsx's child components and manually import all their stylesheets as above. Then their children, and so on. Even if a component is the child of several parents, I have to repeat this process every time unless I happen to remember all the stylesheets it needs. There are a lot of components in the project, so it's a ton of tree-walking.
I tried programmatically importing the CSS files with a helper function:
// sanitizer is injected by the Angular host
export function importCss(cssUrl: string, sanitizer: DomSanitizer) {
const head = document.getElementsByTagName('head')[0];
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = sanitizer.sanitize(SecurityContext.URL, cssUrl);
head.appendChild(style);
}
// inside Admin.tsx:
importCss("path/to/Admin.css");
You may have already spotted the problem: cssUrl
is a URL, not a relative file path. This means I can no longer import "./Admin.css"
. I have to use a URL, and that means Admin.css has to live in a static assets folder. I explored this a little before deciding it wouldn't save me any work.
My current approach is to use a convention. Whenever I bring over a React component, I use the following rules:
The advantage here is it saves a lot of repeated effort. The problem is it doesn't save enough. There's still a lot of pain and manual tree-walking. I'm hoping there's a better way.
import "./Admin.css"
line and play nicely with it. All my attempts to do this have failed. (I should say I'm just assuming this is from Angular's compiler, since it worked in the React app.)link
tag. Then Attempt 1 would work. I don't know how to do that, though.Unfortunately, all solutions involving Webpack failed for me, despite Webpack's own documentation recommending them. This is probably due to the uniqueness of my situation: The components aren't exactly running in an Angular environment, and they aren't exactly running in React either. No matter what I tried, I couldn't construct an import
statement that would give me the raw contents of a .css file.
What finally did work is require
:
const css = require("./my-css.css");
This required one change to angular.json, only supported since Angular 17:
"architect": {
"build": {
"options": {
"loader": {
".css": "text"
},
As soon as I could get the raw content, the hard part was over. This left me free to pursue Idea 3, which is now working great:
const registry = new Set();
export function registerCss(...fileContents: string[]): void {
fileContents.forEach(cssCode => {
const cssBase64 = btoa(cssCode);
if (!registry.has(cssBase64)) {
registry.add(cssBase64);
const styleElem = document.createElement('link');
styleElem.setAttribute('rel', 'stylesheet');
styleElem.setAttribute('href', `data:text/css;base64,${cssBase64}`);
document.head.appendChild(styleElem);
}
});
}
I even made a version to support .scss files:
export function registerScss(...fileContents: string[]): void {
const cssContents = fileContents.map(fileContent => compileString(fileContent).css);
registerCss(...cssContents);
}
Usage in hosted React components is simple:
useEffect(() => {
registerCss(
require("./Comments.css"),
require("./AddCommentModal.css"),
);
}, []);
I'm currently deciding whether it's better to reuse the raw base64 CSS code as the registry key or use a simple hashing function like iMurmurhash. It would have to be as fast and resource-cheap as possible. Any advice on that is welcome.
Hope this helps someone!