neezer
neezer

Reputation: 20560

Dynamically load a stylesheet with React

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?

Upvotes: 41

Views: 97010

Answers (8)

user21411520
user21411520

Reputation: 21

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>
     ...
</>

Upvotes: 2

FatihAziz
FatihAziz

Reputation: 453

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")
)

Upvotes: 0

Eric
Eric

Reputation: 575

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

{inject ? 
    <Helmet>
        <link rel="stylesheet" href="css/style.css" />
    </Helmet> : null}

Upvotes: 4

Foram Shah
Foram Shah

Reputation: 115

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) => {});
}

Upvotes: 0

Mohamed Magdy
Mohamed Magdy

Reputation: 605

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>
  );
};

Upvotes: 21

Caio Mar
Caio Mar

Reputation: 2624

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;

Upvotes: 1

burak
burak

Reputation: 4064

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

Upvotes: 43

Brigand
Brigand

Reputation: 86220

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.

Upvotes: 8

Related Questions