Search code examples
webpackserver-sidereactjsisomorphic-style-loader

How to load styles onto React server-side using Webpack and go back to style-loader client-side?


My goal is to render CSS into a style tag server-side similar or exactly how the style-loader plugin would do it client-side. I know this is impossible because style-loader writes directly to the DOM and the DOM doesn't exist in Node.js.

Currently I'm using the ExtractTextPlugin, but if I don't have it compile all CSS into one big file, it's gonna miss some styles when the page loads unless I'm running Webpack on the server and not compiling it outright.

I've got this code rendering the page:

server.jsx

const renderedContent = renderToString(
    <Provider store={store} history={history}>
        <RoutingContext {...renderProps} />
    </Provider>
)

const finalState = store.getState()
const renderedPage = renderFullPage(renderedContent, finalState)

render-full-page.jsx

module.exports = function renderFullPage(renderedContent = undefined, state = {}) {
    return '<!DOCTYPE html>' + renderToStaticMarkup(
        <html lang="en">
        <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            {typeof __production !== 'undefined' && __production === true && <link id="css" rel="stylesheet" href="/main.css" />}
        </head>
        <body>
            <div id="root"><div dangerouslySetInnerHTML={{__html: renderedContent}}></div></div>
            <script dangerouslySetInnerHTML={{__html: 'window.__INITIAL_STATE__ =' + JSON.stringify(state)}} />
            <script src="/bundle.js"></script>
        </body>
        </html>
    )
}

I am importing styles globally when I need them in my React modules like so:

import './../../assets/styl/flex-video'

And I'd like to change how I'm loading CSS to put all CSS into a var I can loop through and output <style> tags the same way style-loader does it like so:

{typeof __production !== 'undefined' && __production === true && cssFiles.map((styles) => {
    <style>{styles}</style>
})}

Is this possible in Webpack? Is there a plugin like isomorphic-style-loader that's capable of doing this? If so, how would I modify my code?


Solution

  • Here's an example of what you want using isomorphic-style-loader.

    First you create a Style Helper to automate either pushing styles to the DOM or loading them into a var that will later be rendered on the server.

    This is an example of a Style Helper I created modified to accept an array of style files (CSS, Sass, LESS, Stylus):

    utilities/style-helper.jsx

    import React, { Component } from 'react'
    import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'
    
    const css = []
    
    const collectOrRender = function(styles) {
        let renderedCollection = []
    
        for (let i = 0, l = styles.length; i < l; i++) {
            const stylesFile = styles[i]
    
            if (canUseDOM) {
                renderedCollection[stylesFile._insertCss()]
            }
    
            css.push(stylesFile._getCss())
        }
    
        return renderedCollection
    }
    
    export function styleHelper(ComposedComponent, styles) {
        return class Styles extends Component {
            componentWillMount() {
                this.styleRemovers = collectOrRender(styles)
            }
    
            componentWillUnmount() {
                setTimeout(() => {
                    for (let i = 0, l = this.styleRemovers.length; i < l; i++) {
                        let styleRemover = this.styleRemovers[i]
                        typeof styleRemover === 'function' && styleRemover()
                    }
                }, 0)
            }
    
            render() {
                return <ComposedComponent {...this.props} />
            }
        }
    }
    
    export function renderStyles() {
        return css.join('')
    }
    

    Then you have to grab your Style Helper and have it wrap your module on load.

    This example shows how you'd do this whilst using connect() from react-redux.

    my-module.jsx

    // Utilities
    import { styleHelper } from 'utilities/style-helper'
    
    const styles = [
        require('normalize.css'),
        require('styl/global'),
        require('styl/site'),
        require('styl/bubble')
    ]
    
    class myModule extends Component {
        render() { return (
            <div />
        )}
    }
    
    export default connect(
        state => ({ menuIsOpen: state.collapsibleMenu.menuIsOpen })
    )(styleHelper(myModule, styles))
    

    That handles loading on the client. Now on the server, you're storing all these styles to an array. You'll wanna pull that array into your HTML page and place it between some <style> tags.

    Here's an example of how I did it:

    render-full-page.jsx

    import React from 'react'
    import { renderToStaticMarkup } from 'react-dom/server'
    
    // Utilities
    import { renderStyles } from './style-helper'
    
    module.exports = function renderFullPage(renderedContent = undefined, state = {}) {
        return '<!DOCTYPE html>' + renderToStaticMarkup(
            <html lang="en">
            <head>
    
                {/* Critical Styles */}
                <style dangerouslySetInnerHTML={{__html: renderStyles()}} />
            </head>
            <body>
                <div id="root" dangerouslySetInnerHTML={{__html: renderedContent}}></div>
                <script dangerouslySetInnerHTML={{__html: 'window.__INITIAL_STATE__ =' + JSON.stringify(state)}} />
                <script src="/bundle.js"></script>
            </body>
            </html>
        )
    }
    

    Notice how the style tag gets the styles from Style Helper and puts them inside the <style> tag. When react loads in, it'll figure out what to do, but now the server will have only CSS necessary to actually display the page properly.