Growing Pains: MaterialUI Dark mode & Gatsby's Advanced Build Features

Growing Pains: MaterialUI Dark mode & Gatsby's Advanced Build Features

I was recommended by one of my mentors,?Alex Li?to complete a FrontEnd Mentor challenge where I gathered data from a restAPI and use it to create dynamic pages displaying that information to demonstrate my frontend proficiency using any frameworks of my choosing (Challenge details). At the time, I felt rather comfortable with React and wanted to add to my skillset by building this project within the Gatsby environment with the MUI component library (Material UI) to leverage and learn rich features from both. MUI for its beautiful UI elements and Gatsby for convenient routing and GraphQL built in to handle data (this was prior to the Next.js 13 update ??). You can check out the deployed site?here?and the repo?here!

MUI has a lot of different customizable components, that come with different props which created a lot of documentation to get familiar with. I found the two biggest hurdles when working with MUI were creating a unified custom theme and handling dark mode.?

To tackle theme uniformity, MUI has a built-in API called createTheme which allows you to customize every last bit of typography, spacing, palettes, breakpoints, zIndexing, and transitions as well as allowing the user to create custom versions of each component directly.?

One issue I found myself stuck on for some time was affecting pseudo-classes and other nested elements within an MUI component, as it required some very specific key values that were not so clearly demonstrated in the documentation. The first step was uncovering which classes and nested elements needed to be adjusted as each MUI component is built of multiple elements and varying classNames applied throughout its different states (hover, focus, etc). After that, it took some deep googling to discover how to nest these classes properly within the createTheme object.

For example, a select <input> in MUI is wrapped in a custom React parent component designated <FormControl>, a styled <InputLabel>, MUI's <Select> and nested within it are your selections as <MenuItem>'s

return (
??<FormControl onKeyUp={handleClearSelect}>
???<InputLabel>Filter By Region</InputLabel>
???<Select
????label="Filter By Region"
????value={regionValue}
????onChange={handleRegionChange}
???>
????<MenuItem value="Africa">Africa</MenuItem>
????<MenuItem value="Americas">America</MenuItem>
????<MenuItem value="Asia">Asia</MenuItem>
????<MenuItem value="Europe">Europe</MenuItem>
????<MenuItem value="Oceania">Oceania</MenuItem>
???</Select>
??</FormControl>
);        
No alt text provided for this image
initially drop down arrow identifier still uses 'light' colour theme in DarkMode

In dark mode, my Select component had an arrow signifying a drop-down menu that initially was still filled with a darker colour creating a poor visual experience for the user.

My first inclination was to check my chrome dev tools to see how MUI builds out the components to gain a deeper grasp of where I could potentially correct this poorly coloured SVG.

No alt text provided for this image
Actual HTML compiled from the MUI/React snippet previously mentioned above

Looking at the elements section of our dev tools, you notice the wrapping div surrounding the entire segment with a class of 'MuiFormControl-root' which is our FormControl MUI component, directly below is a label element, and below that is div to represent our Select component with class 'MuiInputBase-root', nesting another div, an SVG element and a fieldset element (but no actual <select> element ?? all right then MUI, keep your secrets). Focusing on the SVG element code highlighted the arrow in my browser and confirmed it was the element I needed to affect.

To do this let's dive into our createTheme object:


export const darkTheme = createTheme(theme
?palette: {
??...
?},
?components: {
??MuiInputBase: {
???styleOverrides: {
????root: {
?????"& > svg": {
??????color: "rgba(255,255,255,0.54)"
?????}
????}
???}
??},
??...
?}
})?,        

Above you see a snippet from my darkTheme object which is a createTheme function call. I first provide the generic 'theme' object I created prior with base styles for my components but had no palette colours as they would differ between light and dark modes. To affect a component specifically, we nest into the 'components' key, then select our specific component, in this case, the MuiInputBase. We want to target 'styleOverrides' at the 'root' of the element which is specific to MUI's way of doing just that, overriding styles. Finally, we want to change only the SVGs nested within it which thankfully we can do similarly to SASS/SCSS and use a key value with an ampersand and a greater than symbol with whatever element following we want to affect: '& > svg'. After this, it's simply nesting your customized CSS. (Of course, I could have created a CSS file but I felt this would defeat the purpose of using and exploring MUI as a tool. Someone went through the effort of building this amazing library and the exercise for me was to become familiar with it.)

Dark mode was a whole other world of difficulties. Initially, it started off very simple: create a state variable for your dark mode if it’s true or false, select the appropriate theme dependent on that state value and connect the setTheme to a button 'onClick' prop. What I wasn’t prepared for was handling the notorious theme flicker that novice websites can have when the initial theme is unknown and then React rehydrates the page with the JavaScript that determines which theme the user prefers.

To check the user's preference of theme, I thought to utilize two methods: checking local storage in case the user is returning and the user's preferred system theme with a media query if it's their first visit. Regardless, these assessments happen with JavaScript, specifically, in React's useEffect hook that is fired AFTER the initial HTML is sent from the server and React's first render. I needed to know the user's preference as the initial HTML was sent by the server. But when the page is initially built out we don’t know what the user’s preferred mode is, let alone who they are (We really just hope someone visits ??).

To understand how to tackle this, we need to understand how Gatsby builds a production version of your site. Gatsby uses a process called SSG (static site generation) which gathers all the data at build time and creates a separate static HTML page for each of your data points. How can we have a static page with a dynamic theme colour? We need JS to fetch this data as the user first loads the webpage, so the best solution is to embed a script in each HTML static page that will be in the initial packet from the server when the user first visits the site and by placing the script at the top of our HTML, it prevents any further HTML from loading until its completion. Gatsby came prepared, with two helper files (Gatsby-ssr.js and Gatsby-browser.js) which run at build time and inject our custom script.

By doing so, the script will run entirely before any HTML is rendered, therefore determining our user's preferences and adopting the appropriate background colour. Still, since we used MUI, its nature is to predefine every component with initial colours and since we are in the React framework, they will load first as predefined. It's not until the `useEffect` hook is fired will they change to the appropriate colours which happens after the initial render. This led to some funny behaviour. Let’s say the user preferred dark mode, our script would make the background dark-themed, but MUI needs to give initial theme values so then the components would load in light mode (or dark depending on our `createTheme` settings) and briefly after the useEffects fired, the components would finally change to the appropriate dark mode theming.

The solution I found is to transfer the dark mode state into react's useContext hook so that we can wrap the entire Gatsby root element. During build time, gatsby recognizes the existence of our Gatsby-ssr/browser files and then wraps each static page in our script and theme context. Providing our context at the highest level means we could pass our dark mode site-wide and get the setTheme anywhere within our React app.?

I also used a Layout component to wrap each page's return elements so my which looked like this:


index.js


export const index = () => {
?...

?return (
??<Layout>
????...
??</Layout>
?)
}



Layout.js


export const Layout = () => {
?const [darkMode, setDarkMode] = useState(false);
?const [isMounted, setIsMounted] = useState(false);
??

?useEffect(() => {
??const theme = localStorage.getItem('color-mode');
??

??if (theme) {
???const themePreference = localStorage.getItem('color-mode');
??
???if (themePreference === 'dark') {
????setDarkMode(true);

???} else {
????setDarkMode(false);
???}
??} else {
???const prefersMode = useMediaQuery('(prefers-color-scheme: dark)')
????? 'dark'
????: 'light';

???const prefersModeBool = useMediaQuery('(prefers-color-scheme: dark)');
???localStorage.setItem('color-mode', prefersMode);
???setDarkMode(prefersModeBool);
??}

??setHasMounted(true);
?}, []);


?if (!hasMounted) {
??return null;
?}

??
?return (
???...
?)
}        

By running each page through a layout component, I could put a hasMounted state with an initial value of false and an 'if (!hasMounted) return null' expression to prevent any JSX from rendering until the first useEffect call. It would be set to true in the useEffect hook after our theming logic. Therefore, the correct background colour was determined by our script in our Gatsby-ssr.js file and the `if (!hasMounded) return null` prevented any MUI components from loading until the useEffect fired where we determined what our users preferred colour mode. This one problem took two days of research and probably less than an hour of refactoring but felt incredibly triumphant once it was solved!

UPDATE: I completed this project ages ago and it worked fine in my local development environment but when someone DMd me after I initially published this article that it wasn't working on Chrome or Safari (I had tested the site on the Brave Browser) I quickly took it down and proceeded to scratch my head. This happened at the end of November right as I was ramping up into the busiest time of the year so this project fell on the back burner. I felt pretty defeated as I couldn't figure out the minified errors in the production site's dev tools. It took a couple of weeks of 14-hour days working on other projects before I got a chance to sit down and debug again. I went for the tried and true method of console.log'ing my way through the workflow to figure out where in the code the error was occurring. It turned out the custom MUI `useMediaQuery` hook that was nested in my `useEffect` hook was causing an error in my production site. By moving it out of the `useEffect` hook and into the React Component itself, the site began to work as expected.

There was one problem I couldn’t solve and that was figuring out why Gatsby's HeadAPI, which affects the head element of each page, was producing an undefined error. To figure out what was causing the error, I created a blank new project and slowly added component by component and plugin by plugin to determine what was causing the error. I discovered it was the way I was creating GraphQL nodes from the countries' API data. I was using a plugin at the time and thought what if I wrote the code manually, maybe this would correct the undefined error. Through some research, I learned you can create GraphQL nodes with another Gatsby file (Gatsby-node.js) and my trusty friend Axios for the data fetching. Even by doing this, I found I was still running into the error and after what felt like looking everywhere on the internet, I found myself stumped. My next steps were to submit an issue on the GitHub repo and install React-Helmet instead. React-Helmet does the exact same thing as Gatsby's HeadAPI but it still felt unsatisfying that I couldn’t solve why Gatsby’s HeadAPI didn’t work.?

What do you think, should I have kept trying to figure out why the HeadAPI didn’t work or celebrate small victories and build something new?

How do you troubleshoot?

Are there better habits or what would have you done differently?

This project was a reminder that what starts out simple can still have massive time suckers if not planned for accordingly. Whenever I set out to build something, I can only plan for the known and yet it seems there are always new problems that pop up that I had not even known could occur. These exercises help me round out my knowledge and gain a better way of accounting for the time a project can take!

If you are still reading, thanks for coming along on the journey! See you on the next project =)

要查看或添加评论,请登录

社区洞察

其他会员也浏览了