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 yourgatsby-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;
};
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>
);
};
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);
};
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
};
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)
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-bonesstyle
attributes, everything looks fine 🤔I might create a repo case for @garronej (thanks for tss-react ❤️) soon, but for now I'm happy enough 🙈
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:
and then checking on localhost to see if it works?
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.Thanks for the nice feedback, feel free to reach out. :)
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,
Thanks! :) Keep up the great work!
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..
How do you make it all work together today?
This should now be updated :) See the repo for changes, but I'm leaving the article as is for historical reasons.