Best way to do database-driven styles in react - javascript

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.

Related

Vue.js/NuxtJS - how to create components with a default design customizable by a JSON configuration file

I'm developing a NuxtJS website and the pages/components can have either a generic design by default, either one that is customizable by client, and will be specified in the url.
Something alone the lines of:
http://localhost:3000/registration - For a generic page
http://localhost:3000/client-name/registration - Client specific page
To achieve that goal, I have a JSON configuration file per client (say client-name.json) that has this structure.
{
"some_configuration_property": {},
"another_configuration_property": {},
"design": {
"logoUrl": "/assets/client-name/logo.png",
"backgroundColor": "#000000",
"primaryColor": "#ffffff",
"secondaryColor": "#ffff00"
},
}
To start things, I implemented the routing system and I can successfully read each client's configuration based on the current route (inside the <script> tag of the Vue file of that route), inside the setup method (I use #nuxt/composition-api).
The problem that I'm facing now is to figure out how to pass these "design variables" into the <style> tag of my Vue file, which uses SCSS. The behaviour that I wanted to implement was to have a default design for a specific component/page, but that could be overridden by these "design variables" specific to each client.
The first thing that came to my mind was to use CSS variables, that
would allow me to create variables with a default values but that I
would be able to override inside the styles. I created a sample component for test and it worked with CSS properties and the v-deep pseudo element. However, this means that I would have to create a class for each client in the customized component and that's what I'd like to avoid. I first thought to this approach because it would give me a lot of flexibility about how I choose to use these design colors inside the styles.
Example:
// From the customizable component
.my-button {
color: (--button-color, teal);
}
// Styling from a parent component/view
// Had to create a selector with a style like <div> for superior specificity though, not so clean
v::deep {
div {
&.my-button {
--button-color: purple;
}
}
}
I've seen the /deep/ selector or ::v-deep pseudo selector but I don't think it's a very clean solution since it would be used a lot in the codebase. Styling component from parents would make the code hardly maintainable.
Another approach could be to pass a variable, classArray for instance, inside the setup method to dynamically bind CSS classes on the DOM elements. Although, it would be way too cumbersome to create a CSS class per client with the associated styles.
Like this:
<template>
<my-button :class="classArray"></my-button>
</template>
<script lang="ts">
import { defineComponent } from '#nuxtjs/composition-api'
export default defineComponent({
name: 'MyPage',
setup() {
const clientName = 'someClientName';
const classArray = [clientName]
return { classArray };
},
})
</script>
<style lang="scss" scoped>
.someClientName {
// some custom styles
}
</style>
What would be your approach in this situation?
Thanks for help!
If custom theme configuration needs to be loaded at runtime, this requires to use CSS variables (properties). They can be wrapped in SCSS functions and have default theme fallbacks:
// theme.scss
$primaryColor: #abc;
// $buttonColor: $primaryColor
#function primaryColor() {
#return #{var(--primary-color, $primaryColor)}
}
#function buttonColor() {
#return #{var(--button-color, primaryColor())}
}
Then primaryColor(), etc are used instead of direct use of $primaryColor, etc like it's done in regular SCSS theme:
// From the customizable component
.my-button {
color: buttonColor();
}
And custom theme can be applied on load to the entire document or a part of it (a hierarchy of components) that should be affected by custom theme:
const config = await loadClientConfig();
document.documentElement.style.setProperty(--primary-color, config.design.primaryColor)

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

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.

How to test conditional styling made with Material UI makeStyles()?

I've been working on a React app for a few weeks and I use Material UI components for most of by base.
I have this component that has its style change depending on the values of its props. To do this, I did something like:
const useStyles = makeStyles({
component: {
...
backgroundColor: getComponentBackgroundColor(),
...
}
});
with getComponentBackgroundColor() being defined as
const getComponentBackgroundColor = () => {
if (props.someProp) {
return "green";
}
return "red";
};
and then by setting the component's className.
My problem is that I want to test this component to ensure that the styling is correctly applied (some getStyle() methods are more complex than juste looking if a prop exists or not).
I'm using the react-testing-library and my first instinct was to check if the rendered component had the right className, but upon further inspection, I realized that makeStyle() assigns some random className like makeStyles-component-12 to each component. I also noticed that components with the same styling had different classNames. So that was a no-go.
Is there an easy way to test conditional styling when using Material UI's makeStyles() ?
Thanks a bunch.
Although it's not recommended to couple your tests with specific class or css style, you could use jest-dom's .toHaveStyle matcher to test if the right style is applied when passing the corresponding props.

How to type styles in JSS with TypeScript?

In a React project I'm using JSS for styling components. This works really nice, but now I'm trying to introduce TypeScript into the project. Most of the things work fine, but I am now stuck at typing the styles object.
Supposed I define styles like this:
const styles = (theme: XXX): YYY => ({
Foo: {
// ...
}
});
export default styles;
This is then imported into the actual component and applied using this line:
export default withStyles(styles)(MyComponent);
Now I wonder about the types for XXX and YYY. So far I'm using any here, but of course this does not provide any meaningful type information. Of course I could create an interface which describes everything in detail, but this quickly becomes cumbersome and annoying and a PITA to maintain.
How do you solve this in a reasonable way?
PS: Oh, and it's not an option to not provide YYY as return value, since our ESLint settings enforce that you have to specify a return value, so we can not rely on type inference here, or something like that. Besides, there must be a type…

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!

Categories

Resources