35

I'm building a CMS system for managing marketing landing pages. On the "Edit Landing Page" view, I want to be able to load the associated stylesheet for whichever landing page the user is editing. How could I do something like this with React?

My app is fully React, isomorphic, running on Koa. My basic component heirarchy for the page in question looks something like this:

App.jsx (has `<head>` tag)
└── Layout.jsx (dictates page structure, sidebars, etc.)
    └── EditLandingPage.jsx (shows the landing page in edit mode)

Data for the landing page (including the path of the stylesheet to load) is fetched asynchronously in EditLandingPage in ComponentDidMount.

Let me know if you need any additional info. Would love to get this figured out!

Bonus: I'd also like to unload the stylesheet when navigating away from the page, which I assume I can do the reverse of whatever answer comes my way in ComponentWillUnmount, right?

Brigand
  • 84,529
  • 20
  • 165
  • 173
neezer
  • 19,720
  • 33
  • 121
  • 220

8 Answers8

38

Just update stylesheet's path that you want to be dynamically loaded by using react's state.

import * as React from 'react';

export default class MainPage extends React.Component{
    constructor(props){
        super(props);
        this.state = {stylePath: 'style1.css'};
    }

    handleButtonClick(){
        this.setState({stylePath: 'style2.css'});
    }

    render(){
        return (
            <div>
                <link rel="stylesheet" type="text/css" href={this.state.stylePath} />
                <button type="button" onClick={this.handleButtonClick.bind(this)}>Click to update stylesheet</button>
            </div>
        )
    }
};

Also, I have implemented it as react component. You can install via npm install react-dynamic-style-loader.
Check my github repository to examine:
https://github.com/burakhanalkan/react-dynamic-style-loader

burak
  • 3,839
  • 1
  • 15
  • 20
  • I didn't want to load a whole stylesheet especially since I'm using Rails, so I did this bit of hackery based on your answer but adding a style tag conditionally https://gist.github.com/siakaramalegos/eafd1b114ddcbe8fac923edbc9f8a553 – Sia Oct 06 '16 at 20:50
  • 3
    this worked nicely for what i need. Im using react-create-app so i had to move the css to the public folder. – cabaji99 May 23 '17 at 16:32
  • @cabaji99 What does your href look like for your link element? Did you do `"%PUBLIC_URL%/stylesheet_name.css"` or what? – Tur1ng Aug 01 '17 at 16:18
  • 1
    This worked nicely for me too, remember that if you want to dynamically access stylesheets that are situated in your `public` folder from within your Javascript code you should use the `PUBLIC_URL` environment variable as follows: `` – Andy Oct 03 '17 at 14:39
  • What if you want to put it in the `` tag? – Marco Prins Jul 12 '18 at 09:12
  • 2
    @MarcoPrins You can try react-helmet to render into the `` element – senornestor Jan 22 '19 at 22:32
  • seems to work on load, but not when state changes later. i.e. reloading the style – Sonic Soul Feb 21 '19 at 14:00
  • @ShahriyarImanov it's valid. As long as the link tag has 'body ok' type, it can be used in the body. Please check usage notes in here: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link – burak Nov 25 '19 at 15:17
  • @burakhanalkan : which is quite discouraged practice. – Gelmir Nov 27 '19 at 00:18
  • @ShahriyarImanov that's true. Although it's not the best practice, it's still valid markup. – burak Nov 27 '19 at 08:56
  • how can I load css to style tag from package, bootstrap for example? before I used `import 'bootstrap/dist/css/bootstrap.min.css';` – Ivan Bryzzhin Apr 04 '20 at 10:32
18

I think that Burakhan answer is correct but it is weird to load <Link href = "" /> inside the body tag. That's why I think it should be modified to the following [ I use React hooks]:

import * as React from 'react';
export default MainPage = (props) => {
  const [ stylePath, setStylePath ] = useState("style1.css");
    
  const handleButtonClick = () => {
    setStylePath({stylePath: 'style2.css'});
  }

  useEffect(() => {
    var head = document.head;
    var link = document.createElement("link");

    link.type = "text/css";
    link.rel = "stylesheet";
    link.href = stylePath;

    head.appendChild(link);

    return () => { head.removeChild(link); }

  }, [stylePath]);

  return (
    <div>
      <button type="button" onClick={handleButtonClick}>
        Click to update stylesheet
      </button>
    </div>
  );
};
Mohamed Magdy
  • 535
  • 5
  • 9
  • 9
    I think you'd need a `return () => { head.removeChild(link); }` just under the `head.appendChild` node to clean up, else you'll just keep adding nodes when the stylePath will change. – Emmanuel Touzery Apr 20 '20 at 07:52
8

This is prime mixin teritority. First we'll define a helper to manage style sheets.

We need a function that loads a style sheet, and returns a promise for its success. Style sheets are actually pretty insane to detect load on...

function loadStyleSheet(url){
  var sheet = document.createElement('link');
  sheet.rel = 'stylesheet';
  sheet.href = url;
  sheet.type = 'text/css';
  document.head.appendChild(sheet);
  var _timer;

  // TODO: handle failure
  return new Promise(function(resolve){
    sheet.onload = resolve;
    sheet.addEventListener('load', resolve);
    sheet.onreadystatechange = function(){
      if (sheet.readyState === 'loaded' || sheet.readyState === 'complete') {
        resolve();
      }
    };

    _timer = setInterval(function(){
      try {
        for (var i=0; i<document.styleSheets.length; i++) {
          if (document.styleSheets[i].href === sheet.href) resolve();
        } catch(e) { /* the stylesheet wasn't loaded */ }
      }
    }, 250);
  })
  .then(function(){ clearInterval(_timer); return link; });
}

Well $#!@... I was expecting to just stick an onload on it, but nope. This is untested, so please update it if there are any bugs – it's compiled from several blog articles.

The rest is fairly straight forward:

  • allow loading a stylesheet
  • update state when it's available (to prevent FOUC)
  • unload any loaded stylesheets when the component unmounts
  • handle all the async goodness
var mixin = {
  componentWillMount: function(){
    this._stylesheetPromises = [];
  },
  loadStyleSheet: function(name, url){
    this._stylesheetPromises.push(loadStyleSheet(url))
    .then(function(link){
      var update = {};
      update[name] = true;
      this.setState(update);
    }.bind(this));
  },
  componentWillUnmount: function(){
    this._stylesheetPromises.forEach(function(p){
      // we use the promises because unmount before the download finishes is possible
      p.then(function(link){
        // guard against it being otherwise removed
        if (link.parentNode) link.parentNode.removeChild(link);
      });
    });
  }
};

Again, untested, please update this if there are any issues.

Now we have the component.

React.createClass({
  getInitialState: function(){
    return {foo: false};
  },
  componentDidMount: function(){
    this.loadStyleSheet('foo', '/css/views/foo.css');
  },
  render: function(){
    if (!this.state.foo) {
      return <div />
    }

    // return conent that depends on styles
  }
});

The only remaining todo is checking if the style sheet already exists before trying to load it. Hopefully this at least gets you on the right path.

Brigand
  • 84,529
  • 20
  • 165
  • 173
3

I use react-helmet, in render function....

{inject ? 
    <Helmet>
        <link rel="stylesheet" href="css/style.css" />
    </Helmet> : null}
Eric
  • 365
  • 3
  • 6
1

https://www.npmjs.com/package/react-helmet

Install react-helmet and use it for dynamic css for separate components. Example is For 1st component, using style1.css

<>
     <Helmet>
         <link rel="stylesheet" href="/css/style1.css" />
     </Helmet>
     ...
</>
For 2nd component, using style2.css
<>
     <Helmet>
         <link rel="stylesheet" href="/css/style1.css" />
     </Helmet>
     ...
</>
0

This is how I add style dynamically:

import React, { Component } from "react";

class MyComponent extends Component {
    componentDidMount() {
        const cssUrl = "/public/assets/css/style.css";
        this.addStyle(cssUrl);
    }

    addStyle = url => {
        const style = document.createElement("link");
        style.href = url;
        style.rel = "stylesheet";
        style.async = true;

        document.head.appendChild(style);
    };

    render() {
        return <div> textInComponent </div>;
    }
}

export default MyComponent;
Caio Mar
  • 2,344
  • 5
  • 31
  • 37
0

Instead of creating elements for stylesheet, you can also try importing your css based on some condition. ECMAScript provides a proposal that enables dynamic module imports, that works as follows:

if (condition) {
  import('your css path here').then((condition) => {});
}
Foram Shah
  • 107
  • 1
  • 4
0

On my approach i use this:

const TenantSelector = ({ children }) => {
  // imagine its value from a json config
  const config = {
      custom_style: 'css/tenant.css' 
  }
  require(`./assets/${config.custom_style}`)
  return (
    <>
      <React.Suspense fallback={<></>}>
      </React.Suspense>
      {children}
    </>
  )
}

ReactDOM.render(
  <TenantSelector>
   <YourApp>
  </TenantSelector>,
  document.getElementById("root")
)
FatihAziz
  • 438
  • 4
  • 11