Published on 25th August 2022
In the last post, we have implemented dark/light mode for our application and improved a bit more the interface and the user experience. Now it's time we added Routing .
If you've reached to this article, I suppose you did read the previous ones. The application repo is here . If you just want to follow along, you can clone the repo above and then run:
git reset --hard bfd2b09fffec0a0375655d457b0b605b77a04826
You also need to set up your mongoDB cloud instance. You can do that by following This article
Start your app, and let's dig in. We will start by adding some routes in our application. These are URLs mapped to certain screens. We need to do this because we will definitely want dedicated screens for stuff such as: log in, meals list (we kind of have that base screen), maybe some meals history or any other functionality we might want to add later on.
We will start with basic routing . We want to show specific 'screens' when hitting a certain URL in the browser. Note that for now we are only hitting the http://localhost:3000 route.
Run the following command in the front directory terminal:
yarn add react-router-dom
Next, add the below import in the App.js:
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
We will implement our routing in the App.js. Next, replace its contents as per below:
import React, { useContext } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; 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"; const NotFound = () => { return "page not found"; }; const TestRoute = () => { return "Test Route Here.."; }; const AppContainer = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, }} > <ToggleSwitch /> <div className="App"> <MealItemsProvider> <MainCard /> </MealItemsProvider> </div> </Box> ); }; function App() { const { mode } = useContext(UIContext); const theme = getTheme({ mode }); return ( <ThemeProvider theme={theme}> <BrowserRouter> <Routes> <Route path="*" element={<NotFound />} /> <Route path="/" element={<AppContainer />} /> <Route path="test" element={<TestRoute />} /> </Routes> </BrowserRouter> </ThemeProvider> ); } export default App;
Now, you have implemented 3 routes. If you go to http://localhost:3000/randomRoute, if you go to http://localhost:3000/test you get the test route and if you go to home (http://localhost:3000) you can see our current screen.
This is how routing basically works but we definitely need to clean up our code a bit and our logic. The way I like to set things up, is creating 2 folders (1 will be called 'pages' and the other one 'routes'). This will allow me to better segment my logic (one page represents 1 route but it might output multiple components so apart from the route, I need to encapsulate the 'screen' logic in 1 single element and that will be the 'page').
Create a 'routes' directory in the 'src' one. Inside of it create a NotFound.js. Inside of it paste:
import { NotFoundPage } from "../pages/NotFound.page"; export const NotFound = () => { return <NotFoundPage />; };
Next, create a 'pages' directory (inside 'src'), and inside of it create a NotFound.page.js file. Inside of it paste:
import React from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import { ToggleSwitch } from "../components/ToggleSwitch/ToggleSwitch"; export const NotFoundPage = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "100vh", }} > <div className="App"> <ToggleSwitch /> <Typography variant="h3" gutterBottom align="center" color="white"> Page Not Found 😵 </Typography> </div> </Box> ); };
Finally, in App.js replace the NotFound definition with the below import:
import { NotFoundPage as NotFound } from "./pages/NotFound.page";
And voila! Your not found route works and has the background set as it should.
Let's do some cleanup now. We'll start by moving the AppContainer() function in its own separate file. Move it in its dedicated file in the components directory.
Next, let's create a TestRoute.js file inside the routes:
import { TestPage } from "../pages/Test.page"; /* * @Default route * */ export const Test = () => { return <TestPage /> };
Next, create the Test.page.js inside pages:
import React from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import { ToggleSwitch } from "../components/ToggleSwitch/ToggleSwitch"; /* * @TestPage (contents of the page) * */ export const Test = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "100vh", }} > <div className="App"> <ToggleSwitch /> <Typography variant="h3" gutterBottom align="center" color="white"> Test Route Here </Typography> </div> </Box> ); };
Finally, replace the const TestRoute ... in App.js with:
import { Test as TestPage } from "./pages/Test.page";
Don't forget to replace TestRoute with it.
Our TestRoute works good (we have the page and all) but we need a layout that is a bit more complex. Let's add a Navigation bar for starters. Create a Navigation directory inside the components one, and inside of it create a Navigation.js. Paste the below inside:
import React, { useState } from "react"; import AppBar from "@mui/material/AppBar"; import Box from "@mui/material/Box"; import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; import Menu from "@mui/material/Menu"; import MenuIcon from "@mui/icons-material/Menu"; import Container from "@mui/material/Container"; import Avatar from "@mui/material/Avatar"; import Button from "@mui/material/Button"; import Tooltip from "@mui/material/Tooltip"; import MenuItem from "@mui/material/MenuItem"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import { ToggleSwitch } from "../ToggleSwitch/ToggleSwitch"; const pages = ["Home", "Sign Up", "Calories"]; const settings = ["Profile", "Logout"]; /* * @Navigation Component (functional component) * */ export const Navigation = () => { const [anchorElNav, setAnchorElNav] = useState(null); const [anchorElUser, setAnchorElUser] = useState(null); const appTitle = "Meals"; const handleOpenNavMenu = (event) => { setAnchorElNav(event.currentTarget); }; const handleOpenUserMenu = (event) => { setAnchorElUser(event.currentTarget); }; const handleCloseNavMenu = () => { setAnchorElNav(null); }; const handleCloseUserMenu = () => { setAnchorElUser(null); }; return ( <AppBar position="static" sx={{ bgcolor: "appBarColor.main" }}> <Container maxWidth="xl"> <Toolbar disableGutters> <FastfoodIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} /> <Typography variant="h6" noWrap component="a" href="/" sx={{ mr: 2, display: { xs: "none", md: "flex" }, fontFamily: "monospace", fontWeight: 700, letterSpacing: ".3rem", color: "inherit", textDecoration: "none", }} > {appTitle} </Typography> <Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}> <IconButton size="large" aria-label="account of current user" aria-controls="menu-appbar" aria-haspopup="true" onClick={handleOpenNavMenu} color="inherit" > <MenuIcon /> </IconButton> <Menu id="menu-appbar" anchorEl={anchorElNav} anchorOrigin={{ vertical: "bottom", horizontal: "left", }} keepMounted transformOrigin={{ vertical: "top", horizontal: "left", }} open={Boolean(anchorElNav)} onClose={handleCloseNavMenu} sx={{ display: { xs: "block", md: "none" }, }} > {pages.map((page) => ( <MenuItem key={page} onClick={handleCloseNavMenu}> <Typography textAlign="center">{page}</Typography> </MenuItem> ))} </Menu> </Box> <FastfoodIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} /> <Typography variant="h5" noWrap component="a" href="" sx={{ mr: 2, display: { xs: "flex", md: "none" }, flexGrow: 1, fontFamily: "monospace", fontWeight: 700, letterSpacing: ".3rem", color: "inherit", textDecoration: "none", }} > {appTitle} </Typography> <Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}> {pages.map((page) => ( <Button key={page} onClick={handleCloseNavMenu} sx={{ my: 2, color: "white", display: "block" }} > {page} </Button> ))} </Box> <Box sx={{ marginRight: "5rem", marginTop: "-1rem" }}> <ToggleSwitch /> </Box> <Box sx={{ flexGrow: 0 }}> <Tooltip title="Open settings"> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <Avatar alt="Remy Sharp" src="/static/images/avatar/2.jpg" /> </IconButton> </Tooltip> <Menu sx={{ mt: "45px" }} id="menu-appbar" anchorEl={anchorElUser} anchorOrigin={{ vertical: "top", horizontal: "right", }} keepMounted transformOrigin={{ vertical: "top", horizontal: "right", }} open={Boolean(anchorElUser)} onClose={handleCloseUserMenu} > {settings.map((setting) => ( <MenuItem key={setting} onClick={handleCloseUserMenu}> <Typography textAlign="center">{setting}</Typography> </MenuItem> ))} </Menu> </Box> </Toolbar> </Container> </AppBar> ); };
Finally remove the ToggleSwitch from the Test.page.js and adjust its styling (in the ToggleSwitchStyles.js, in the emojiStyle object, make the font size 3 rem instead of 4).
Our navigation looks good so far but we want each of the links to point to a dedicated page so we can move on with our implementation.
First, in the Navigation.js, replace the pages array with:
const pages = [ { title: "Home", path: "/" }, { title: "Sign Up", path: "/signup" }, { title: "Calories", path: "/calories" }, ];
Next, in the 2 map() calls, pass the index as key and display the page.title property.
Do the same for the settings array. If you get lost in changes, just look at the commit changes at the end of this post.
Next, import the below inside the Navigation:
import { Link } from "react-router-dom";
Finally, replace the contents of Navigation.js as per below:
import React, { useState, useContext } from "react"; import AppBar from "@mui/material/AppBar"; import Box from "@mui/material/Box"; import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; import Menu from "@mui/material/Menu"; import MenuIcon from "@mui/icons-material/Menu"; import Container from "@mui/material/Container"; import Avatar from "@mui/material/Avatar"; import Button from "@mui/material/Button"; import Tooltip from "@mui/material/Tooltip"; import MenuItem from "@mui/material/MenuItem"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import { Link } from "react-router-dom"; import { ToggleSwitch } from "../ToggleSwitch/ToggleSwitch"; import { UIContext } from "../../state/UI.module"; const pages = [ { title: "Home", path: "/" }, { title: "Sign Up", path: "/signup" }, { title: "Calories", path: "/calories" }, ]; const settings = [ { title: "Profile", path: "/profile" }, { title: "Logout", path: "/logout" }, ]; /* * @Navigation Component (functional component) * */ export const Navigation = () => { const { mode } = useContext(UIContext); const linkStyles = { textDecoration: "none", color: "#fff", }; const [anchorElNav, setAnchorElNav] = useState(null); const [anchorElUser, setAnchorElUser] = useState(null); const appTitle = "Meals"; const handleOpenNavMenu = (event) => { setAnchorElNav(event.currentTarget); }; const handleOpenUserMenu = (event) => { setAnchorElUser(event.currentTarget); }; const handleCloseNavMenu = () => { setAnchorElNav(null); }; const handleCloseUserMenu = () => { setAnchorElUser(null); }; return ( <AppBar position="static" sx={{ bgcolor: "appBarColor.main" }}> <Container maxWidth="xl"> <Toolbar disableGutters> <FastfoodIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} /> <Typography variant="h6" noWrap component="a" href="/" sx={{ mr: 2, display: { xs: "none", md: "flex" }, fontFamily: "monospace", fontWeight: 700, letterSpacing: ".3rem", color: "inherit", textDecoration: "none", }} > {appTitle} </Typography> <Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}> <IconButton size="large" aria-label="account of current user" aria-controls="menu-appbar" aria-haspopup="true" onClick={handleOpenNavMenu} color="inherit" > <MenuIcon /> </IconButton> <Menu id="menu-appbar" anchorEl={anchorElNav} anchorOrigin={{ vertical: "bottom", horizontal: "left", }} keepMounted transformOrigin={{ vertical: "top", horizontal: "left", }} open={Boolean(anchorElNav)} onClose={handleCloseNavMenu} sx={{ display: { xs: "block", md: "none" }, }} > {pages.map((page, index) => ( <MenuItem key={index} onClick={handleCloseNavMenu}> <Link to={page.path} style={{ ...linkStyles, color: mode === "dark" ? "#fff" : "#000", }} > <Typography textAlign="center">{page.title}</Typography> </Link> </MenuItem> ))} </Menu> </Box> <FastfoodIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} /> <Typography variant="h5" noWrap component="a" href="" sx={{ mr: 2, display: { xs: "flex", md: "none" }, flexGrow: 1, fontFamily: "monospace", fontWeight: 700, letterSpacing: ".3rem", color: "inherit", textDecoration: "none", }} > {appTitle} </Typography> <Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}> {pages.map((page, index) => ( <Button key={index} onClick={handleCloseNavMenu} sx={{ my: 2, color: "white", display: "block" }} > <Link to={page.path} style={{ ...linkStyles }}> {page.title} </Link> </Button> ))} </Box> <Box sx={{ marginRight: "5rem", marginTop: "-1rem" }}> <ToggleSwitch /> </Box> <Box sx={{ flexGrow: 0 }}> <Tooltip title="Open settings"> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <Avatar alt="Remy Sharp" src="/static/images/avatar/2.jpg" /> </IconButton> </Tooltip> <Menu sx={{ mt: "45px" }} id="menu-appbar" anchorEl={anchorElUser} anchorOrigin={{ vertical: "top", horizontal: "right", }} keepMounted transformOrigin={{ vertical: "top", horizontal: "right", }} open={Boolean(anchorElUser)} onClose={handleCloseUserMenu} > {settings.map((setting, index) => ( <MenuItem key={index} onClick={handleCloseUserMenu}> <Link to={setting.path} style={{ ...linkStyles, color: mode === "dark" ? "#fff" : "#000", }} > <Typography textAlign="center">{setting.title}</Typography>{" "} </Link> </MenuItem> ))} </Menu> </Box> </Toolbar> </Container> </AppBar> ); };
Now our navigation works but it points to the not found page. Let's start by creating a
import { AppContainer } from "../components/AppContainer"; /* * @HomePage (contents of the page) * */ export const HomePage = () => { return <AppContainer /> };
Next, replace the contents of AppContainer.js:
import { Box } from "@mui/material"; import { MealItemsProvider } from "../state/MealItems.module"; import { MainCard } from "../components/MainCard/MainCard"; import { Navigation } from "./Navigation/Navigation"; /* * @AppContainer (functional component) * */ export const AppContainer = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, }} > <Navigation /> <div className="App"> <MealItemsProvider> <MainCard /> </MealItemsProvider> </div> </Box> ); };
Now we have the home route properly set. Next Create a SignUp.page.js file inside pages:
import React from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import { Navigation } from "../components/Navigation/Navigation"; /* * @SignUp Page (contents of the page) * */ export const SignUpPage = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "100vh", }} > <div className="App"> <Navigation /> <Typography variant="h3" gutterBottom align="center" color="white"> Sign Up Page </Typography> </div> </Box> ); };
Next import it in App.js and add its route:
<Route path="/signup" element={<SignUpPage />} />
Let's do the Calories.page.js next:
import React from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import { Navigation } from "../components/Navigation/Navigation"; import { MainCard } from "../components/MainCard/MainCard"; import { MealItemsProvider } from "../state/MealItems.module"; /* * @Calories Page (contents of the page) * */ export const CaloriesPage = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "auto", }} > <div className="App"> <Navigation /> <MealItemsProvider> <MainCard /> </MealItemsProvider> </div> </Box> ); };
Add its route in the App.js
<Route path="/calories" element={<CaloriesPage />} />
Next create the Profile.page.js:
import React from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import { Navigation } from "../components/Navigation/Navigation"; /* * @Profile Page (contents of the page) * */ export const ProfilePage = () => { return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "100vh", }} > <div className="App"> <Navigation /> <Typography variant="h3" gutterBottom align="center" color="white"> Profile Page </Typography> </div> </Box> ); };
Add its route too in App.js:
<Route path="/profile" element={<ProfilePage />} />
For the logout route, let's leave the default 404 for now. We will implement the functionality when we finish the signup and login ones.
Let's work a bit on the homepage of our app. We already have a dedicated screen for the calories, so we can remove it from there. Furthermore we want a sign in form on the homepage.
First, let's change some stuff in the Theme.js. First, replace the 'primary' value of the light palette with:
primary: { main: blue[500], },
Next, add the below values in the light and dark palette:
//light version submitBtnColor: { main: blue[300], }, //dark version submitBtnColor: { main: `${indigo[300]}!important`, },
Next, replace the contents of AppContainer.js as per below:
import React, { useState } from "react"; import { Box } from "@mui/material"; import FormGroup from "@mui/material/FormGroup"; import Typography from "@mui/material/Typography"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import Button from "@mui/material/Button"; import { Link } from "react-router-dom"; import { CustomTextField } from "./CustomTextField/CustomTextField"; import { Navigation } from "./Navigation/Navigation"; import { Item } from "./MainCard/Item"; /* * @AppContainer (functional component) * */ export const AppContainer = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( <Box component="form" sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, "& .MuiTextField-root": { m: 1, width: "25ch" }, height: "100vh", textAlign: "center", }} noValidate autoComplete="off" > <Navigation /> <Typography variant="h3" component="div" color="#fff" sx={{ margin: "1rem" }} > Welcome to <br></br> <FastfoodIcon sx={{ marginLeft: "2rem" }} /> Meals! </Typography> <Item sx={{ maxWidth: "25rem", margin: "auto", padding: "3rem 1rem" }}> <FormGroup sx={{ textAlign: "center" }}> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Email </Typography> <CustomTextField placeholder="email" type="email" changeHandler={setEmail} value={email} /> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Password </Typography> <CustomTextField placeholder="password" type="password" changeHandler={setPassword} value={password} /> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > Sign In </Button> </div> </FormGroup> </Item> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > No account yet? </Typography> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > <Link style={{ textDecoration: "none", color: "#fff" }} to="/signup"> Sign Up </Link> </Button> </div> </Box> ); };
We have a login form ready and we can add a signup form too. Replace the contents of SignupPage.js as per below:
import React, { useState } from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import FormGroup from "@mui/material/FormGroup"; import Button from "@mui/material/Button"; import { Navigation } from "../components/Navigation/Navigation"; import { CustomTextField } from "../components/CustomTextField/CustomTextField"; import { Item } from "../components/MainCard/Item"; /* * @SignUp Page (contents of the page) * */ export const SignUpPage = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "100vh", }} > <div className="App"> <Navigation /> <Typography variant="h3" gutterBottom align="center" color="white"> Sign Up Page </Typography> <Item sx={{ maxWidth: "25rem", margin: "auto", padding: "3rem 1rem" }}> <FormGroup sx={{ textAlign: "center" }}> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Email </Typography> <CustomTextField placeholder="email" type="email" changeHandler={setEmail} value={email} /> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Password </Typography> <CustomTextField placeholder="password" type="password" changeHandler={setPassword} value={password} /> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Confirm Password </Typography> <CustomTextField placeholder="confirm password" type="password" changeHandler={setPassword} value={password} /> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > Sign Up </Button> </div> </FormGroup> </Item> </div> </Box> ); };
If you want to check your code against mine, you can do so here
In the next article we will start working on the user authentication as this post is already too long. Thanks for sticking around!