The Pitfall of Material UI Theme with Gatsby
The Order of Nesting Matters
2020-02-27 • 9 min read
TLDR: you should move your theme provider into its own plugin. Jump to the resolution.
This blog is built with Gatsby, as I have explained in my earlier blog post. To handle the styling, I chose Material-UI (MUI). I chose it since I like the it offers to manage style changes within JavaScript. In its implementation, it uses JSS to support the feature. It ensures that the theme of the website is more unified by using a theme object, which is a React context that runs through the entire React virtual DOM tree to allow all components to access the theme and create styling that is coherent with the overall design.
<ThemeProvider theme={myCustomTheme}>
{/* the rest of the entire website... */}
</ThemeProvider>
Another perk with Material-UI is that although the CSS are now being controlled in JavaScript, which is suppose to make style rendering on the server side difficult, the library supports SSR (server side rendering) of styles straight out of the box. It ideally should work really well with Gatsby. To enable SSR of MUI styling, we add the helper plugin
// gatsby-config.js
module.exports = {
plugins: [`gatsby-plugin-material-ui`],
};
When it comes to dealing with components that is shared
across all pages, we will ideally want to adhere to the
DRY principle
by not having to wrap all pages that we code with the
ThemeProvider
component.
Gatsby offers a nice way for us to factor out the
common components from each pages through the
gatsby-browser.js
and gatsby-ssr.js
file.
We can implement the wrapRootComponent
function
as such
// gatsby-browser.js / gatsby-ssr.js
export const wrapRootElement = ({ element }) =>
<ThemeProvider theme={myCustomTheme}>
{element}
</ThemeProvider>;
As explained in the docs, “this is useful to set up any Provider components that will wrap your application.” This is exactly what we want. Now, we can sit tight and enjoy the easy MUI SSR benefit. Right?
Problem with SSR-ing of MUI Theme
Not so fast, in early iteration of this website,
we find the following bug in the production build
(generated from the command gatsby build
).
The following gif shows how the old version of
my blog renders under a 6x CPU slowdown:
The issue here is the flash of font. In CSS, we call this flash of unstyled content (FOUC). Multiple factors can lead to FOUC. Common causes are loading of external CSS stylesheets and external fonts. In my website, I use external fonts from Google Fonts. However, I only slowed down my CPU in the test and did not toggle with the network throttle setting at all. There might be something else that is causing the issue.
The font of the titles are controlled by the MUI theme. The next idea that I had is to check whether my CSS in Material UI is being properly SSR-ed. To do so, we prevent JavaScript from running. The reason to disable JS is that if style is properly SSR-ed, we should still see the font being loaded properly after some delay. However, if style is not SSR-ed, we will not even see the correct font at all. Since Javascript cannot be executed, the font style cannot be mounted into CSS in run time.
To disable JavaScript, you can
open Chrome DevTools, and hit Cmd+Shift+P (Ctrl+Shift+P on Windows). This will open a command dialog and search for the option Disable JavaScript.
After disabling JavaScript, we re-render the page. This is the result of rendering without JavaScript.
We see that the font is not correctly being loaded. The navbar is using the default system font. By inspecting the CSS properties on the navbar, we see that Material-UI font styling is not SSR-ed.
What is going on here? Is the MUI plugin broken such
that it is not rendering the styling? When MUI renders
the styles, it injects the CSS into a style
tag in the
head
tag, with the ID jss-server-side
.
However, this style
tag will be unmounted as soon
as the JavaScript runs since we want the JavaScript
styling to determine the actual styling. It is
there to prevent the FOUC problem between the webpage
loads and when JavaScript runs.
After checking, the jss-server-side
style is there.
Therefore, MUI should be able render the CSS properly.
Note that this behavior can only be found in production build, in which a static website is generated. This behavior cannot be replicated in development mode. If we disable JavaScript in development, we cannot access our site due to the fact that our site is hosted in a webpack dev server which requires JavaScript to run and access the site.
How the Official MUI Gatsby Example Handles Theme
I decided to check out the official MUI Gatsby example
to see how they handled global theme styling.
We can find the example here.
Interestingly, we see that they have written a custom
plugin (gatsby-plugin-top-layout
) to support theme injection.
Inside the plugin, we find a implementation of wrapRootElement
:
import React from 'react';
import TopLayout from './TopLayout';
export const wrapRootElement = ({ element }) => {
return <TopLayout>{element}</TopLayout>;
};
In side TopLayout.js
, we see that it wraps the children
within a ThemeProvider
.
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{props.children}
</ThemeProvider>
This simply means that we are moving the theme wrapper
from our root gatsby-browser.js
into a plugin.
When we try building the page (remember to build the
page not in development mode), the styling works
fine with the theme styling correctly injected.
However, why does this matter?
To understand the reason, we take a look at the order
of plugins specified in the gatsby-config.js
file for
the example repo.
module.exports = {
plugins: [
'gatsby-plugin-top-layout',
'gatsby-plugin-material-ui',
'gatsby-plugin-react-helmet',
],
siteMetadata: {
title: 'My page',
},
};
Interestingly, we see that the plugin that wraps
our page in a theme provider (gatsby-plugin-top-layout
)
goes before the plugin that actually does the SSR
of style (gatsby-plugin-material-ui
).
What happens if you switch the order of plugin and
have gatsby-plugin-top-layout
goes after?
The exact steps taken to reproduce the buggy behavior:
- Clone the Gatsby MUI example (don’t forget
npm install
)- Switch the order of
top-layout
andmaterial-ui
plugingatsby build
gatsby serve
and navigate to the page with JavaScript disabled.
Ha! The ordering of plugins does matter here.
The order of plugin specified within the gatsby-config
file is the order in which Gatsby executes the
plugin during the build stage.
Therefore, the order of how the components are
wrapped are also determined by this ordering.
Let’s assume that we have the following ordering of plugins.
module.exports = {
plugins: [
'gatsby-plugin-1',
'gatsby-plugin-2',
'gatsby-plugin-3'
]
};
Let us also assume that all of them implements the
wrapRootElement
API and wraps some components
around the element. The order of how the components
are wrapped is like the following:
<RootWrapper>
<Plugin3Wrapper>
<Plugin2Wrapper>
<Plugin1Wrapper>
{children}
</Plugin1Wrapper>
</Plugin2Wrapper>
</Plugin3Wrapper>
</RootWrapper>
Note that the
RootWrapper
is the wrapper component specified on the top levelgatsby-browser.js
.
If the the material UI plugin comes first, we will have the following structure.
<ThemeProvider>
<MuiSsrPlugin>
{children}
</MuiSsrPlugin>
</ThemeProvider>
The MuiSsrPlugin
(hypothetical name) is the component
that takes the descendant nodes to render their style
and inject them into the html build. However, since
the ThemeProvider
is not a child of the MuiSsrPlugin
,
whatever styling that it contains will not be rendered
on the static build.
In our case, since the root wrapper is the last thing that
wraps the entire React DOM tree, the MuiSsrPlugin
can
never render its style correctly!
Conclusion: Finally Mastering the Jutsu
Me:
Bug:Meme reference: https://www.youtube.com/watch?v=dNQs_Bef_V8
To conclude, the main reasons that SSR does not work in the website are:
- The
MuiSsrPlugin
only SSR styles of its descendant nodes - The
wrapRootElement
in the top levelgatsby-browser
is always executed last.
Again, to re-iterate, the most important take away here
is that the order of plugin specified in gatsby-config
is the order in which they execute. The solution is that
we extract our theme into a custom plugin. To see
how I implemented that, you can check out this commit:
226fff20.
At this point, you might ask “Why does this matter? Can’t we just live with that flash of styling? We still have to bare with the flash of font styling since we ultimately have to wait for the download of font assets.”
Yes, it does not matter that much. But I simply enjoy figuring things out and fixing this tiny but hard problem is very much beneficial to my understanding of Gatsby as a whole. There are also practical benefits. With fonts, browser can cache them easily and the flash of font is only a problem for first-time visitors. When visitors come back for a second time, browser does not need to wait for font downloading and the flash will not be there. However, with JavaScript styling, browser cannot cache the result and therefore requires re-rendering every time the website is loaded. On low end device with slow CPU, this can be a problem since initial rendering is already slow with the need to run the React code, and building the React DOM tree again.
To learn more about React rendering and front end optimization in general, read another blog post that I wrote for JavaScript Chat with ACM Hack: Optimizing Frontend and React Apps.