Published 2 aug 2022
In the previous article, we have documented our code (very important practice, I cannot stress enough the importance of this), refactored some more stuff and replaced the 'prop drilling' with a proper React context .
This is the 5th part of an on-going series of my blog, you can check it out here .
In order to start from where we are currently in our project you need to clone this repo and then rollback to this commit hash: ba6ed933c24f717a7d1819b82a3de3e56a0e5838. So the first command you should run is:
git reset --hard ba6ed933c24f717a7d1819b82a3de3e56a0e5838
Set up your project (there is a markdown file in the repo showing you how), add the back-end .envfile and we are ready to go.
We will start again with some UI improvements (I've started looking into properly designing the app with a tool such as Figma but it'll be a while before I actually produce something good with it as I am still just learning the ropes).
However an immediate improvement for our UI would be to add a Theme for our application and also to centralize all the colors and other variables that we used throughout our code in some unique file. Let's start by adding a theme. In the 'src' directory, create a 'theme' one and inside of it create a Theme.js file. Inside of it paste the below:
import { createTheme } from "@mui/material/styles"; import { green, purple, red, deepOrange, grey, indigo, } from "@mui/material/colors"; /* * @getTheme (function) * * @props: { * mode: string, * } * * @returns Theme object */ export const getTheme = ({ mode }) => { return createTheme({ palette: { mode, ...(mode === "light" ? { // palette values for light mode primary: { main: green[800], }, secondary: { main: purple[500], }, error: { main: red[400], }, } : { // palette values for dark mode background: { paper: `${indigo[800]}!important`, }, primary: { main: deepOrange[800], }, secondary: { main: grey[400], }, error: { main: red[700], }, }), }, }); };
This will allow us to also have dark/light mode too.
Next, inside App.js add the below 3 imports:
import React, { useState } from "react"; import { ThemeProvider } from "@mui/material/styles"; import { getTheme } from "./theme/Theme";
Next, define the below piece of state and the theme in the returned function:
const [mode, setMode] = useState("light"); const theme = getTheme({ mode });
Finally, wrap the div returned from the App.js into the ThemeProvider:
<ThemeProvider theme={theme}> //rest of returned code here... </ThemeProvider>
As you may have noticed, the theme implementation in react Material UI is dependant upon the React Context that we've been looking at in the previous post.
Test out the dark/light functionality by changing the string in the useState call.
Let's create one more front-end component. Create a ToggleSwitch.js file inside the components directory and paste the below to it:
import React, { useState } from "react"; import FormControl from "@mui/material/FormControl"; import FormGroup from "@mui/material/FormGroup"; import FormControlLabel from "@mui/material/FormControlLabel"; import Switch from "@mui/material/Switch"; const emojiStyle = { display: "inline-block", position: "absolute", top: 10, left: 40, }; /* * @ToggleSwitch (functional component) * * @props: { * setThemeMode: function, * } */ export function ToggleSwitch({ setThemeMode }) { const [state, setState] = useState({ checked: false, }); const [placeholder, setPlaceholder] = useState("light"); const changePlaceholder = () => { placeholder === "light" ? setPlaceholder("dark") : setPlaceholder("light"); }; const handleChange = (event) => { setState({ ...state, [event.target.name]: event.target.checked, }); changePlaceholder(); setThemeMode(placeholder); }; return ( <FormControl component="fieldset" variant="standard"> <FormGroup> <FormControlLabel control={ <Switch checked={state.checked} onChange={handleChange} name="checked" color="toggleSwitch" /> } /> {placeholder === "dark" && <span style={emojiStyle}>🌚</span>} {placeholder === "light" && <span style={emojiStyle}>🌞</span>} </FormGroup> </FormControl> ); }
Next, add the belo2 property in the Theme.js in the palette object under the error color (don't forget to put it in both places for both light and dark mode):
toggleSwitch: { main: grey[800], },
Next, import the ToggleSwitch in App.js and use it as per below and add the setThemeMode() method before the return statement:
import { ToggleSwitch } from "./components/ToggleSwitch"; ... function App () { ... const setThemeMode = (placeholder) => { if (mode === "light" && placeholder === "light") { setMode("dark"); } else if (mode === "dark" && placeholder === "dark") { setMode("light"); } else { setMode(placeholder); } }; return(... <ToggleSwitch setThemeMode={setThemeMode} /> );
Try changing the theme of your application now. It should work as intented. However, there are 2 bugs in our implementation. First, we get a red outline on the inputs while clicking them in dark mode (the outline gets thinner and becomes green in light mode) and the placeholder disappears. Let's fix them.
First for the placeholder, just add 1 more line in the StyledTextField.js file:
color: #000;
As for the coloured outline of the textfields (once clicked), we need to customize the theme a bit. First add the below prop in the CustomTextField.js component definition (add it around line 30):
color="outlineColor"
Next, in the Theme.js, for both light and dark palettes, right where you added the toggleSwitch option, add one more:
outlineColor: { main: grey[50], },
Now everything looks good. However, I would like a custom background color for both light/dark modes. For that we will need to add 1 more element in the App.js. Start by importing: import { Box } from "@mui/material"; up top.
Next, wrap everything inside the ThemeProvider as per below:
<ThemeProvider theme={theme}> <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, }} > ...rest of code </Box> </ThemeProvider>
Finally, in the Theme.js in the 'light' version palette add:
background: { defaultColor: blue[200], },
* Don't forget to add blue in the colors import.
For the 'dark' mode, you already have the background option (and are overwriting just the paper color), so add under that:
background: { paper: `${indigo[800]}!important`, defaultColor: indigo[200], },
I would also want the sun/moon emoji there to look bigger. So first in the emojiStyles add:
fontSize: "3rem",
Now we need to move around the ToggleSwitch too so add one more style object for it:
//edit the emojiStyle too as per below const emojiStyle = { display: "inline-block", position: "absolute", top: 10, left: 60, fontSize: "4rem", }; const switchStyle = { marginTop: "1.8rem", marginLeft: "1rem", };
Next, attach the switchStyle to the FormGroup component in the ToggleSwitch.
Since we already have 2 style objects in the ToggleSwitch.js, let's move it in it's own directory and put the style objects in their own seperate files. Create a ToggleSwitch folder, inside of it create a ToggleSwitchStyles.js and export the 2 style objects from it.
Next, we need to add a UI context module (to store stuff such as the theme and maybe later on other pieces of info such as whether the user is logged in or not). Also, we will make a little LocalStorage module (we need to persist some data such as the mode for the theme, but we won't bother to use the back-end yet for that). Maybe at a later time we will move this piece of info from LS into the back-end.
Let's start with the LS module (as if we switch to dark mode for instance, and refresh the page, our app will go back to default light mode and we do not want that).
Inside the 'services' directory create a ClientStorage.js file and inside of it paste:
/* * Service for handling ClientStorage for the interface */ export const storeItem = (key, value) => { localStorage.setItem(key, JSON.stringify(value)); }; export const getItem = (key) => { return JSON.parse(localStorage.getItem(key)); };
Now let's use the new service in App.js. Import it and use it as per below:
import { storeItem, getItem } from "./services/ClientStorage";
Next, add a useEffect call:
useEffect(() => { const clientMode = getItem("caloriesMode"); if (clientMode) { setMode(clientMode); } }, [theme]);
Next, replace the setThemeMode() method as per below:
const setThemeMode = (placeholder) => { if (mode === "light" && placeholder === "light") { setMode("dark"); storeItem("caloriesMode", "dark"); } else if (mode === "dark" && placeholder === "dark") { setMode("light"); storeItem("caloriesMode", "light"); } else { setMode(placeholder); storeItem("caloriesMode", placeholder); } };
Now, your dark/light mode will 'stick' beyond a page refresh and we've introduced a bug in our code, namely the fact that when we refresh the page, if we are in dark mode, we still see the sun emoji and the toggle unchanged. This happens because once we refresh the page, the ToggleSwitch is reinitialized, it uses the light config as default, and it's basically 'recreated' in light mode. Let's fix the bug first and then we will further optimize our implementation.
First, our ToggleSwitch.js should 'know' if we are in light or dark mode and it should automatically configure itself to do so. ClientStorage service to the rescue! Let's import it in the toggle component and use it. Import the getItem method from the newly created service, and add the below useEffect() call in the ToggleSwitch:
useEffect(() => { const clientMode = getItem("caloriesMode"); if (clientMode) { setPlaceholder(clientMode); if (clientMode === "dark") { setState({ ...state, checked: true }); } else { setState({ ...state, checked: false }); } } }, []);
Now we have fixed our bug, but we have sort of duplicated some code. We are double checking for the user theme in localStorage in both App.js and ToggleSwitch.js. The solution to that will be, you've guessed it (I hope 😅) context. We need to create a UI context module and centralize everything in it.
First of all, let's move the MealsService.service.js into its own folder (let's call that 'state'). Also, rename the MealsService.service.js into MealItems.module.js (as this file represents a 'state' module in our application for the user meals).
Alongside the MealItems.module.js file, create a UI.module.js. Inside of it paste the below:
import React, { createContext, useEffect, useState } from "react"; import { storeItem, getItem } from "../services/ClientStorage"; export const UIContext = createContext(); export const UIContextProvider = ({ children }) => { const [mode, setMode] = useState("light"); const [state, setState] = useState({ checked: false, }); const [placeholder, setPlaceholder] = useState("light"); const setThemeMode = (placeholder) => { if (mode === "light" && placeholder === "light") { setMode("dark"); storeItem("caloriesMode", "dark"); } else if (mode === "dark" && placeholder === "dark") { setMode("light"); storeItem("caloriesMode", "light"); } else { storeItem("caloriesMode", placeholder); setMode(placeholder); } }; const changePlaceholder = () => { placeholder === "light" ? setPlaceholder("dark") : setPlaceholder("light"); }; const handleChange = (event) => { setState({ ...state, [event.target.name]: event.target.checked, }); changePlaceholder(); setThemeMode(placeholder); }; useEffect(() => { const clientMode = getItem("caloriesMode"); if (clientMode) { setMode(clientMode); setPlaceholder(clientMode); } else { setMode("light"); setPlaceholder("light"); } }, []); return ( <UIContext.Provider value={{ mode, setMode, placeholder, setPlaceholder, setThemeMode, handleChange, state, setState, changePlaceholder, }} > {children} </UIContext.Provider> ); };
We have moved quite some logic into it, now we can just use this context in both the App.js and in ToggleSwitch.js. But before anything, let's add the new UIContextProvider. We'll do this in the index.js which will now look like this:
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { UIContextProvider } from "./state/UI.module"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <UIContextProvider> <App /> </UIContextProvider> </React.StrictMode> );
Next, replace the contents of App.js as per below:
import React, { useContext } from "react"; import { ThemeProvider } from "@mui/material/styles"; import { MealItemsProvider } from "./state/MealItems.module"; import { UIContext } from "./state/UI.module"; import { MainCard } from "./components/MainCard/MainCard"; import { ToggleSwitch } from "./components/ToggleSwitch/ToggleSwitch"; import { getTheme } from "./theme/Theme"; import { Box } from "@mui/material"; function App() { const { mode } = useContext(UIContext); const theme = getTheme({ mode }); return ( <ThemeProvider theme={theme}> <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, }} > <ToggleSwitch /> <div className="App"> <MealItemsProvider> <MainCard /> </MealItemsProvider> </div> </Box> </ThemeProvider> ); } export default App;
Note that we are not passing anymore prop to ToggleSwitch.js. By the way, replace its contents too:
import React, { useContext, useEffect } from "react"; import FormControlLabel from "@mui/material/FormControlLabel"; import Switch from "@mui/material/Switch"; import FormControl from "@mui/material/FormControl"; import FormGroup from "@mui/material/FormGroup"; import { UIContext } from "../../state/UI.module"; import { getItem } from "../../services/ClientStorage"; import { emojiStyle, switchStyle } from "./ToggleSwitchStyles"; /* * @ToggleSwitch (functional component) * */ export function ToggleSwitch() { const { state, setState, placeholder, setPlaceholder, changePlaceholder, setThemeMode, } = useContext(UIContext); const handleChange = (event) => { setState({ ...state, [event.target.name]: event.target.checked, }); changePlaceholder(); setThemeMode(placeholder); }; useEffect(() => { const clientMode = getItem("caloriesMode"); if (clientMode) { setPlaceholder(clientMode); if (clientMode === "dark") { setState({ ...state, checked: true }); } else { setState({ ...state, checked: false }); } } }, []); return ( <FormControl component="fieldset" variant="standard"> <FormGroup style={switchStyle}> <FormControlLabel control={ <Switch checked={state.checked} onChange={handleChange} name="checked" color="toggleSwitch" /> } /> {placeholder === "dark" && <span style={emojiStyle}>🌚</span>} {placeholder === "light" && <span style={emojiStyle}>🌞</span>} </FormGroup> </FormControl> ); }
If you've stuck with me this much, thank you very much! We have improved quite a bit our application. If you want to check your code with my latest commit, you can do so here.