DEV Community

Cover image for Gatsby JS — How to solve FOUC when using tss-react and Material UI v5
Deckstar
Deckstar

Posted on • Updated on

Gatsby JS — How to solve FOUC when using tss-react and Material UI v5

Material UI v5 brought some amazing updates, but switching from JSS to Emotion had an arguably nasty side-effect: it was no longer as straightforward to group your component styles in classes. Fortunately, a fantastic library emerged that allowed developers to not only reduce the extreme pain from migrating all their classes from v4's makeStyles to emotion, but to also to continue to writing classes in practically the same syntax, with wonderful TS type-safety. This library was tss-react, and it was one of my favorite open source discoveries of 2021.

Unfortunately, however, tss-react and material-ui don't exactly work "out of the box" when it comes to Gatsby JS. Gatsby is a fantastic framework for generating static websites with React code. Static websites are great because they load fast, can be largely cached in a visitor's browser and have great Search Engine Optimization (SEO).

The trouble I was having was that my production website was displaying what is known as a "flash of unseen content", or FOUC. Material UI users have known about this problem for a long time — in fact, there even exists a popular plugin for solving this exact issue.

But because tss-react and material-ui do not share the same emotion cache, then simply adding gatsby-plugin-material-ui doesn't work. A more custom solution is needed.

This post is about how I solved the FOUC-problem in my Gatsby JS website after migrating to Material UI v5 with tss-react.


TLDR:

  • copy paste the two files below
  • make sure your <CacheProvider /> component imports your emotion cache from the new place
  • remove gatsby-plugin-material-ui from your gatsby-node.js file if you had it

Showcase:

Here's a sample implementation. Feel free to copy from it! 😉

This little example project should be helpful if you get stuck 🙂


Explained

Step 1: make an emotion cache that can be used both on the client and the server

This is the cache that will be used by emotion to make your Material UI styles.

Unfortunately, you probably want this file to be a vanilla JS file, without TypeScript. This is because the gatsby-srr.js file that we will have to create later must be a vanilla JS file, and cannot parse uncompiled TypeScript. But don't worry — you can still get some TS typing intellisense just by adding a few JSDoc comments! ;)

// ./src/theme/cache.js

import createCache from '@emotion/cache';

/** @type {import('@emotion/cache').Options} */
export const cacheProps = {
    key: 'mui',
    prepend: true,
};

/** @type {import("@emotion/cache").EmotionCache | undefined} */
export let muiCache;

export const makeMuiCache = () => {
    if (!muiCache) {
    muiCache = createCache(cacheProps);
    }

    return muiCache;
};
Enter fullscreen mode Exit fullscreen mode

Don't forget to use this cache for your <CacheProvider /> that wraps your Material UI <ThemeProvider />:

// example

import { makeMuiCache } from './cache';

const muiCache = makeMuiCache();

const ThemeWrapper = (props: { children: ReactNode }) => {
  const { children } = props;

  return (
    <CacheProvider
        value={muiCache} // <-- use the new cache here
    >
      <ThemeProvider theme={yourTheme}>
        {children}
      </ThemeProvider>
    </CacheProvider>
  );
};

Enter fullscreen mode Exit fullscreen mode

Step 2: create a gatsby-srr.js file, and add the generated styles to the server-side <head>-components

You'll probably want it to look something like this:

// ./gatsby-srr.js

import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getTssDefaultEmotionCache } from 'tss-react';

import { makeMuiCache } from './src/theme/cache';

/** @param {import('gatsby').ReplaceRendererArgs} args */
export const replaceRenderer = (args) => {
  const { bodyComponent, replaceBodyHTMLString, setHeadComponents } = args;

  const muiCache = makeMuiCache();
  const { extractCriticalToChunks } = createEmotionServer(muiCache);

  const emotionStyles = extractCriticalToChunks(
    renderToString(
      <CacheProvider value={muiCache}>{bodyComponent}</CacheProvider>
    )
  );

  const muiStyleTags = emotionStyles.styles.map((style) => {
    const { css, key, ids } = style || {};
    return (
      <style
        key={key}
        data-emotion={`${key} ${ids.join(` `)}`}
        dangerouslySetInnerHTML={{ __html: css }}
      />
    );
  });

  const tssCache = getTssDefaultEmotionCache();
  const { extractCritical } = createEmotionServer(tssCache);
  const { css, ids } = extractCritical(renderToString(bodyComponent));

  const tssStyleTag = (
    <style
      key="tss-styles"
      data-emotion={`css ${ids.join(' ')}`}
      dangerouslySetInnerHTML={{ __html: css }}
    />
  );

  const combinedStyleTags = [...muiStyleTags, tssStyleTag];
  setHeadComponents(combinedStyleTags);

  // render the result from `extractCritical`
  replaceBodyHTMLString(emotionStyles.html);
};
Enter fullscreen mode Exit fullscreen mode

The idea here is that we are exporting a replaceRenderer function that Gatsby will use internally to render your page when generating it (see Gatsby the docs here).

The first part of this function is copy-pasted straight out of gatsby-plugin-material-ui . However, where the plugin has to use clever file-system tricks to get access to your emotion cache, we can just import our own cache directly! With our cache available from our makeMuiCache function, we can easily generate our muiStyleTags using emotions server-side helpers.

tss-react is also very helpful — you can easily access its emotion cache using the getTssDefaultEmotionCache helper. As Nicholas Tsim documents in his blog post, it is then very easy to create the style tags for any emotion cache — much like how we did earlier for Material UI, we create another emotion server, extract the critical CSS and map it convert it into a JSX style tag.

We then combine all our style tags into one array, and use the Gatsby setHeadComponents helper so that all of them would be included on our page. And finally, we call replacebodyhtml with the generated HTML-string.

Step 3: Remove gatsby-plugin-material-ui from gatsby-config.js to avoid conflicts.

Self-explanatory. If you were using the plugin before, then open your Gatsby config and delete it:

// ./gatsby-config.js

module.exports = {
 /* Your site config here */
 plugins: [
   `gatsby-plugin-material-ui`, // <-- delete me!
    // ...rest plugins
 ],
 // ...rest config
};

Enter fullscreen mode Exit fullscreen mode

Don't forget to run yarn remove gatsby-plugin-material-ui (or the same command with whatever package manager you're using), since the package is no longer needed.


That's it! Your Gatsby site should now load without any flash of unseen content, and you can continue to use the spectacular Material UI library in all its v5 glory, together with tss-react, of course ;)

Good luck!

Top comments (9)

Collapse
 
stefanpl profile image
stefanpl

Thanks so much for publishing this! Saved me a lot of trouble finding this out myself 🙏

The site I'm building looks 10x better now on initial load, but some of the styles are still not there on the SSR build. You wouldn't have any idea what could be the cause for this, would you?
It's fully static styles, created with makeStyles. If I instead apply them as bare-bones style attributes, everything looks fine 🤔

I might create a repo case for @garronej (thanks for tss-react ❤️) soon, but for now I'm happy enough 🙈

Collapse
 
deckstar profile image
Deckstar

Hey @stefanpl , glad I could help! :)

Really basic advice here but... have you tried clearing your Gatsby cache? I had an issue with that once 😄 Try running:

gatsby clean && gatsby build && gatsby serve
Enter fullscreen mode Exit fullscreen mode

and then checking on localhost to see if it works?

Collapse
 
stefanpl profile image
stefanpl

Thanks for answering, but I'm afraid that didn't help ☺️

Like I said, the current state is basically fine, not sure I'll actually invest more time investigating. Adding a few styles as styles attributes is okay as a workaround for now.

Collapse
 
garronej profile image
Garrone Joseph

Thanks for the nice feedback, feel free to reach out. :)

Collapse
 
garronej profile image
Garrone Joseph

Thank you very much for putting this piece together.

I have linked this article in the documentation.

In a week or so I will attempt to make a TSS plugin for Gatsby based on your work.

Best regards,

Collapse
 
deckstar profile image
Deckstar

Thanks! :) Keep up the great work!

Collapse
 
antholife profile image
Anthony Chahat • Edited

Hello,

Unfortunately your solution for Gatsby is not functional under TssReact 4.4.4, indeed I use Gatsby for static pages,
This breaks: import { getTssDefaultEmotionCache } from 'tss-react';

It's not available in the latest version.. so I'm stuck with MUI, Emotion, TSS, Gatsby.. Im lost

Currently I am getting this error..

 `See our docs page for more info on this error: https://gatsby.dev/debug-html
  ownerState.variant === 'contained' && ownerState.color !== 'inherit' && {
   >     backgroundColor: (theme.vars || theme).palette[ownerState.color].dark,     
  '@media (hover: none)': {
   backgroundColor: (theme.vars || theme).palette[ownerState.color].main
  WebpackError: TypeError: Cannot read properties of undefined (reading 'dark')
  - Button.js:100 
    [HostingV3]/[@mui]/material/esm/Button/Button.js
  - createStyled.js:180 
    [HostingV3]/[material]/[@mui]/system/esm/createStyled.js
  - emotion-serialize.esm.js:139 
    [HostingV3]/[@emotion]/serialize/dist/emotion-serialize.esm.js
  - emotion-serialize.esm.js:253 
    [HostingV3]/[@emotion]/serialize/dist/emotion-serialize.esm.js
  - emotion-styled-base.esm.js:140 
    [HostingV3]/[@emotion]/styled/base/dist/emotion-styled-base.esm.js
  - emotion-element-3838ba9e.esm.js:59 
    [HostingV3]/[@emotion]/react/dist/emotion-element-3838ba9e.esm.js
  `
Enter fullscreen mode Exit fullscreen mode

How do you make it all work together today?

Collapse
 
deckstar profile image
Deckstar • Edited

This should now be updated :) See the repo for changes, but I'm leaving the article as is for historical reasons.