Dynamically load and unload content of css file in javascript/react - javascript

I've been banging my head against this for a few days now. I read all the similar questions, but they don't quite match what I need.
I find it hard to believe no one else has tried, or documented, this before, and I'd love some thoughts on how to approach this.
Requirement
I am using react bootstrap for my app and I want to give users the choice between a set of bootstrap themes. To keep it simple I currently only have a light and a dark one. These styles ship as scss from https://bootswatch.com/ and so I'd rather not try the usual CSS in JS theme approaches.
What I'm trying to do
Ideally, what I want is the ability to conditionally render/import the css files. I've tried a few things most of them don't work and one works sort of, but is really hacky.
What I'd like is a way to construct a DOM node with a style node from the css file and not render it. Then I can put all these style nodes into the DOM and selectively add/remove them.
Ideally, I don't need to manually alter the DOM at all. I haven't been able to find a way to do it. I'd have hoped conditional rendering of my style component would do the trick, but this doesn't work as the style is never loaded (beyond first time) or unloaded (ever).
What I tried so far
Use lazy loading (works sort of)
I put my css in a lightweight component such as
import React from 'react';
import './_light.scss';
const Theme = React.forwardRef(( props, ref) => (<div ref={ref}></div>));
export default Theme;```
Then I lazy load either or in my App like so
const LightTheme = React.lazy(() => import('../themes/lightTheme'));
const DarkTheme = React.lazy(() => import('../themes/darkTheme'));
....
const element = (chosenTheme === PreferredTheme.LIGHT) ?
<LightTheme /> : <DarkTheme />
...
return (
<ThemeContext.Provider value={initialValue}>
<React.Suspense fallback="{<></>}">
{element}
</React.Suspense>
{children}
</ThemeContext.Provider>
)
The problem is that I give the user the choice to toggle themes and what seems to happen is two problems 1) that once both have loaded (once) they never load again 2) ordering of style nodes matters and so whichever was loaded last will always stay active.
I was able to work around this by doing some hacky <head> node manipulation in my useEffect to delete (no longer used style)/add (to be applied) style nodes to the DOM, but it's hacky at best.
useRef
to get a reference to each style node, save it to a dict and then delete it. This breaks in an interesting way where the reference works the first time the thing renders but on subsequent renders the reference becomes undefined
Creating a stylesheet on the fly
The solution here looks promising How to load/unload css dynamically. Trouble is I don't know how to get to the inner text of my stylesheet in this scenario. As I said it's shipped as scss so it gets preprocessed and I don't know how I could even get the text.
Any thought or explanations much appreciated.

Right, so I found a quite neat solution now that at least has straightforward logic. I had to learn a bit more about Webpack and how its loaders work. Specifically how to use CRA and still be able to modify it's behaviour. Long story short is that CRA webpack chains the style-loader at the end of scss files and therefore tries to put the css into the DOM as a node (https://webpack.js.org/loaders/style-loader/)
Since I don't want to do that (yet) I need to disable the style-loader that is taken care of by this abomination of an import statement (https://webpack.js.org/concepts/loaders/#inline)
// #ts-ignore
import { default as DarkThemeC } from '!css-loader!sass-loader!../themes/_dark.scss'; // eslint-disable-line import/no-webpack-loader-syntax
// #ts-ignore
import { default as LightThemeC } from '!css-loader!sass-loader!../themes/_light.scss'; // eslint-disable-line import/no-webpack-loader-syntax
Now I have the css as an array at my disposal without rendering it so I put it into a Map for future use.
const availableThemes = new Map<PreferredTheme, string>();
availableThemes.set(PreferredTheme.LIGHT, LightThemeC.toString());
availableThemes.set(PreferredTheme.DARK, DarkThemeC.toString());
This means I can do this in my useEffect callback
useEffect( () => {
// Add new sheet
const newTheme = availableThemes.get(chosenTheme);
if(newTheme) {
// injectCss borrowed from https://stackoverflow.com/questions/60593614/how-to-load-unload-css-dynamically
injectCss(newTheme, chosenTheme);
}
// Remove all others
availableThemes.forEach( (style, theme) => {
if(theme !== chosenTheme) {
var stylesheet = document.getElementById(`dynamicstylesheet${theme}`);
if (stylesheet) {
head.removeChild(stylesheet);
}
}
});
}, [chosenTheme])
Works like a charm, but the only thing I observed is that it's a bit slow on switch (fraction of a second)

Here is my solution (looks like #sev solution)
import { useEffect } from 'react';
export const useDynamicStyleSheet = (styleSheet: string): void => {
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = styleSheet;
document.head.append(styleElement);
return () => styleElement.remove();
}, [styleSheet]);
};
import { FC } from 'react';
import { useDynamicStyleSheet } from 'app/hooks/useDynamicStyleSheet';
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved
import lightTheme from '!css-loader!antd/dist/antd.css';
const LightTheme: FC = () => {
useDynamicStyleSheet(lightTheme.toString());
return null;
};

Why not specify the theme as a class to the top-level element of your application, and build your CSS classes under that using CSS variables?
#app.dark {
--app-bg: black;
--app-fg: white;
}
#app.light {
--app-bg: white
--app-fg: black;
}
#app {
background-color: var(--app-bg);
color: var(--app-fg);
}
You could indeed use a higher-order component to wrap your UI, and change that based the selected theme, and use Webpack for the lazy loading, if it is worth it. FWIW, my experience is that dark/light themes are easiest and most clearly handled as above.

Related

Appending JSX in React Native

I'm searching for a way to add a JSX element programmatically in React Native. Not conditionally.
It needs to be independent function, so far I couldn't find anything about this. Let me give you code example;
const appendJSX = () => {
const jsx = <Text> Hello! </Text>
append(jsx) // <---- can we do something like this?
}
let's say I call this function with useEffect and it should add whatever jsx I have inside the function. Up until this point I always see things like pushing inside of an array or something like that.
UPDATE
Equivalent behaviour that works on web;
useEffect(() => {
const div = document.createElement("div");
div.innerText = "appended div"
document.body.append(div)
}, [])
As you can see we don't have to touch any JSX in application. Reaching document.body and appending whatever we want is possible in React Web. But how can we achieve this in React Native?
Not quite sure what you want to do, but as for to add a JSX manually. Here's the answer.
JSX is already part of the language in most of the cases.
const a = <Text />
export default a
Will translates into:
const a = createElement(Text, null, null)
export default a
Therefore in most of common cases, if you continue using React somewhere else, then the variable a holds a React element without any compilation error.
You might wonder what a actually really holds, it's an object:
const a = {
$$typeof: Symbol(ReactElement),
props: null,
type: Text
}
So you can see the only dependencies in above piece is Text and the ReactElement Symbol. As long as you can resolve them, you are good to export this to anywhere. The latter is normally taken care by Babel.
NOTE:
There's a difference between Text and <Text />. If you just want to export a Text which is a function component, there'll tutorial online, also you can dig into any third party library, because essentially that's what they do, export Text so other people can use it.

Inject per-component style tags dynamically with Rollup and scss

I am building a React component library whose source code takes this general structure:
- src
- common.scss (contains things like re-usable css variables)
- components
- button
- index.js
- button.scss
- dialog
- index.js
- dialog.scss
My components are responsible for importing their own per-component 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/common.scss" // load global styles
import Button from 'mylib/src/components/button/index.js'
import Dialog from 'mylib/src/components/dialog/index.js'
// ...application code...
When my application uses webpack, along with style-loader, the per-component css is appended as style tags in head dynamically when the component is first used. This is a nice performance win since the per-component 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/common.css" // load global styles
// ...application code...
When I use rollup-plugin-scss it just bundles the per-component styles all together, not dynamically adding them as before.
Is there a technique I can incorporate into my Rollup build so that my per-component styles are dynamically added as style tags in the head tag as they are used?
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!

Isomorphic hyperHTML components without passing in wires

I have the following two components:
// component.js
// imports ...
function ListItem(item) {
const html = wire(item)
function render() {
return html`<li>${item.foo}</li>`
}
return render()
}
function List(items) {
const html = wire(items)
function render() {
return html`<ul>${items.map(ListItem)}</ul>`
}
return render()
}
I want to put them in a module which is shared between the client and the server. However, as far as I can tell, although the API pretty much identical, on the server I have to import the functions from the viperHTML module, on the client I have to use the hyperHTML module. Therefore I can not just import the functions at the top of my shared module, but have to pass to my components at the call site.
Doing so my isomorphic component would look like this:
// component.js
function ListItem(html, item) {
//const html = wire(item) // <- NOTE
function render() {
return html`<li>${item.foo}</li>`
}
return render()
}
function List(html, itemHtmls /* :( tried to be consistent */, items) {
//const html = wire(items) // <- NOTE
function render() {
return html`<ul>${items.map(function(item, idx) {
return ListItem(itemHtmls[idx], item)
})}</ul>`
}
return render()
}
Calling the components from the server:
// server.js
const {hyper, wire, bind, Component} = require('viperhtml')
const items = [{foo: 'bar'}, {foo: 'baz'}, {foo: 'xyz'}]
// wire for the list
const listWire = wire(items)
// wires for the children
const listItemWires = items.map(wire)
const renderedMarkup = List(listWire, listItemWires, items)
Calling from the browser would be the exact same, expect the way hyperhtml is imported:
// client.js
import {hyper, wire, bind, Component} from 'hyperhtml/esm'
However it feels unpleasant to write code like this, because I have a feeling that the result of the wire() calls should live inside the component instances. Is there a better way to write isomorphic hyperHTML/viperHTML components?
update there is now a workaround provided by the hypermorphic module.
The ideal case scenario is that you have as dependency only viperhtml, which in turns brings in hyperhtml automatically, as you can see by the index.js file.
At that point, the client bundler should, if capable, tree shake unused code for you but you have a very good point that's not immediately clear.
I am also not fully sure if bundlers can be that smart, assuming that a check like typeof document === "object" would always be true and target browsers only.
One way to try that, is to
import {hyper, wire, bind, Component} from 'viperhtml'
on the client side too, hoping it won't bring in viperHTML dependencies once bundled 'cause there's a lot you'd never need on the browser.
I have a feeling that the result of the wire() calls should live
inside the component instances.
You could simplify your components using viper.Component so that you'll have render() { return this.html... } and you forget about passing the wire around but I agree with you there's room for improvements.
At that point you only have to resolve which Component to import in one place and define portable components that work on b both client and server.
This is basically the reason light Component exists in the first place, it give you the freedom to focus on the component without thinking about what to wire, how and/or where (if client/server).
~~I was going to show you an example but the fact you relate content to the item (rightly) made me think current Component could also be improved so I've created a ticket as follow up for your case and I hope I'll have better examples (for components) sooner than later.~~
edit
I have updated the library to let you create components able to use/receive data/items as they're created, with a code pen example.
class ListItem extends Component {
constructor(item) {
super().item = item;
}
render() {
return this.html`<li>${this.item.foo}</li>`;
}
}
class List extends Component {
constructor(items) {
super().items = items;
}
render() {
return this.html`
<ul>${this.items.map(item => ListItem.for(item))}</ul>`;
}
}
When you use components you are ensuring yourself these are portable across client/server.
The only issue at this point would be to find out which is the best way to retrieve that Component class.
One possible solution is to centralize in a single entry point the export of such class.
However, the elephant in the room is that NodeJS is not compatible yet with ESM modules and browsers are not compatible with CommonJS so I don't have the best answer because I don't know if/how you are bundling your code.
Ideally, you would use CommonJS which works out of the box in NodeJS and is compatible with every browser bundler, and yet you need to differentiate, per build, the file that would export that Component or any other hyper/viperHTML related utilities.
I hope I've gave you enough hints to eventually work around current limitations.
Apologies if for now I don't have a better answer. The way I've done it previously used external renders but it's quite possibly not the most convenient way to go with more complex structures / components.
P.S. you could write those functions just like this
function ListItem(item) {
return wire(item)`<li>${item.foo}</li>`;
}
function List(items) {
return wire(items)`<ul>${items.map(ListItem)}</ul>`;
}

Best way to do database-driven styles in react

This is a little bit of a subjective question but I hope SO can help.
I'm making a ReactJS app and I need to customize certain parts of the style (the color scheme mostly) based on a user's settings in a database. The color palette is spread out over a lot of the style sheet so I'd like to avoid setting all those colors by hand in the render() method.
One option is to server-side render the full css file on every request. Would something like SASS be able to do this in real time? I know most preprocessors are designed for compile-time use.
Another option would be to embed the styling in a JavaScript module with something like react-style. Each components "stylesheet" could actually become a function that takes some preferences and returns styles. This strategy has some downsides (as described here) plus the extra inconvenience of having to pas a "styleSettings" prop down to EVERY component so it could know how to request its styles.
Alternatively I could just have a single global or page-wide style sheet so I have to deal with fewer props, but I like the modularity of each component getting its own style.
Are there any other options I'm missing here? Is there a best practice for doing this type of thing?
I'm not sure if this is the best way to do this, but I accomplished something similar using a theme-provider HoC.
class ThemeProvider extends React.Component {
componentDidMount() {
// fetch required data if you don't already have it
// create a <style> node
this.styleTag = document.createElement('style')
this.styleTag.type = 'text/css'
document.head.appendChild(this.styleTag)
}
/* update the styles if the data source has changed */
componentDidUpdate ({ styles: prevStyles }, prevState) {
const { styles: currentStyles } = this.props
if (!isEqual(currentStyles, prevStyles)) {
// generate the css ex
const css = `
.some_selector {
color: ${currentStyles.color}
}
`
// update the <style> node
this.styleTag.textContent = css
}
}
render() {
const { children } = this.props
return children
}
}
you would use it like
<ThemeProvider>
<RootComponent/>
</ThemeProvider>
pros
globally apply CSS from one location
fairly cheap in terms of reconciliation
cons
a little bit of "magic" involved (i.e. where are these styles coming from?)
component is mounted before styles are applied - i.e. after render... meaning the first load might have styles "flash" in
slightly destructive - .some_selector { color: ... } would override hover, etc effects... nothing you can't easily fix, though.
It turns out styled-components does most of what I wanted to do pretty easily. As a bonus, it has built in support for global themes, so I don't have to think about theme setting in my component props.
It sounds like you either intend to or already have made use of inline styles within your React components. I myself use inline styles from time to time even if this is frowned upon by the vox populi of the React community. Sometimes inline styles are actually the most elegant solution to a React problem, however, in this situation I propose that you stick with using class names so you can leave the application of color to your stylesheet. You would still want to use a CSS preprocessor like LESS or SASS to apply the different color themes based on the user's settings, which is what I am currently doing on my project. So you would apply classes such as: color-primary, color-secondary, color-accent to your DOM content and those classes would have their colors set via the user data.

How to combine emojione with markdown in React?

Right now I am trying to parse some text with both react-emojify and react-markdown. I would like to combine somehow the functionality of both utilities.
Problem is (if I understand correctly) that both convert string into React DOM. When I run emojify on content the result cannot be passed into <ReactMarkdown source={result} /> and vice versa.
I was thinking about doing sth like serializing React DOM into HTML and allowing some tags in the other parser, but both have rather limited options when it comes to making them compatible (e.g. emojify spits emoticons as spans which cannot be allowed in ReactMarkdown).
Have anyone else tried that? Is there some way (even by changing libraries) that could help me achieve this?
I managed to make things work by replacing react-emojify with emojione:
import emojione from 'emojione';
import React from 'react';
import ReactMarkdown from 'react-markdown';
class ExampleComponent extends React.Component {
render() {
const content = this.props.content;
const emojified = emojione.shortnameToImage(content);
return (
<ReactMarkdown source={emojified} />
);
}
}
Later on I only had to tweak how emojis are shown by changing .emojione class properties in CSS (as opposed to passing option object into react-emojify function).

Categories

Resources