FED Talk! Episode 3: Setting Up Routes in React

In today's episode we will step through how to implement your applications routing using React Router, configuring everything from:
  • Defining Routes,
  • Linking between content,
  • Setting up parameters,
  • Utilizing Route hooks
  • Let's get started!
    Table of Contents:
    🤔 What's a Route?
    From the get-go React Apps are configured as a Single Page Application (SPA).
    This means when you build your App everything is shelled into your projects root index.html file made available in the public folder. If you create anchor tag links expecting your users to be navigated to another landing URL, it simply will not work as the only .html page exported from the build at this stage is the root file.
    This is where the recommended library React Router comes into play.
    A route is where we bind the URL to our React App and as developers, we can configure them in a meaningful way.
    For example we can configure:
  • our home page: /,
  • nested child pages: /product-category/products,
  • contextual information: /product-category/products/ABC -> /product-category/products/:productId -> console.log(productId) // "ABC",
  • redirects,
  • fallbacks to things like a "Page not found" page.
  • Setting Up Routing in React
    Before we start implementing we should spend some time upfront to design what our Routes will look like.
    The following questions help me during this phase:
    Will your App be publicly available and are you expecting Google (or any other engine) to index your pages?
    The following topics are worth a read:
    Will users copy/paste URLs to deep link into your content?
    Will users bookmark URLs for future use?
    For the rest of our journey we will build out our App answering the last two questions.
    Let's check the current state of our App to see how we can design our Information Architecture.
    There are 3 areas which can be broken down into smaller digestible bits of content: Typographies, Colour Palette, Buttons. Off the bat we can declare 3 routes:
  • /typographies
  • /colour-palette
  • /buttons
  • Take some time to imagine how your App will evolve. I foresee it containing a mixture of information:
  • Getting Started (Home page): /
  • UI: /ui/*
  • Components: /components/*
  • Feedback: /feedback
  • Page Not found
  • So, because of this we should change our routes to be:
  • /ui/typographies
  • /ui/colour-palette
  • /ui/buttons
  • Now that we have a clear idea on how our routes can be implemented, lets install the react-router-dom library to get started:
    npm install react-router-dom
    npm install --save-dev @types/react-router-dom
    Configuring Routes
    It's best to setup Routes at the highest logical level in your App so all the Router contextual information can propagate down to your components.
    Following on from the previous episode, we can update our App code with the following:
    // src/App.tsx
    
    import { BrowserRouter } from "react-router-dom";
    
    import { CssBaseline, ThemeProvider } from "@material-ui/core";
    
    import AppBar from "./components/AppBar";
    import BodyContent from "./components/BodyContent";
    import Routes from "./Routes";
    import Theme from "./theme";
    
    export default function App() {
      return (
        <ThemeProvider theme={Theme}>
          <CssBaseline />
          <BrowserRouter>
            <AppBar />
            <BodyContent>
              <Routes />
            </BodyContent>
          </BrowserRouter>
        </ThemeProvider>
      );
    }
    Note how the BrowserRouter component wraps your content.
    Update the BodyContent code with the following:
    // src/components/BodyContent/index.tsx
    
    import React from "react";
    
    import { makeStyles } from "@material-ui/core";
    
    const useStyles = makeStyles(() => ({
      root: {
        margin: '0 auto',
        maxWidth: '57rem',
        padding: '2rem 0'
      }
    }))
    
    export default function BodyContent({ children }: { children: React.ReactNode }) {
      const classes = useStyles();
    
      return (
        <main className={classes.root}>
          {children}
        </main>
      )
    }
    Note how we've replaced the manually imported UI components with React's Children prop; this is where our new Router will pass in the Component per the browser's URL.
    Lastly we've got to create our Routes file:
    // src/Routes.tsx
    
    import React from "react";
    import { Route, Switch } from "react-router-dom";
    
    import Buttons from "./ui/Buttons";
    import ColourPalette from "./ui/ColourPalette";
    import Typographies from "./ui/Typographies";
    
    export default function Routes() {
      return (
        <Switch>
          <Route path="/ui/buttons" component={Buttons} />
          <Route path="/ui/colour-palette" component={ColourPalette} />
          <Route path="/ui/typographies" component={Typographies} />
        </Switch>
      );
    }
    Note the use of Route and Switch.
    React Router: Route

    The Route component is perhaps the most important component in React Router to understand and learn to use well. Its most basic responsibility is to render some UI when its path matches the current URL.

    Learn more

    React Router: Switch

    Renders the first child <Route> or <Redirect> that matches the location.
    How is this different than just using a bunch of <Route>s?
    <Switch> is unique in that it renders a route exclusively. In contrast, every <Route> that matches the location renders inclusively.

    Learn more

    Let's take a look at what our Buttons page looks like, by typing in the URL: "http://localhost:3000/ui/buttons"
    ❤️
    That's pretty cool, we've now just split out the content for our App!
    Linking between pages
    Now that our base routes have been setup, let's configure the Links in our left menu to allow users to navigate between the content.
    // src/components/MainMenu/index.tsx
    
    import React from "react";
    import { useHistory } from "react-router";
    
    import { Drawer, List, ListItem, ListItemText } from "@material-ui/core";
    
    const menuItems = [
      { label: 'Buttons', url: '/ui/buttons' },
      { label: 'Colour Palette', url: '/ui/colour-palette' },
      { label: 'Typogaphies', url: '/ui/typographies' },
    ]
    
    function MenuItems({ setOpenMenu }: { setOpenMenu: React.Dispatch<React.SetStateAction<boolean>> }) {
      const { push } = useHistory();
    
      const onLinkNavigation = (url: string) => {
        push(url);
        setOpenMenu(false);
      }
    
      return (
        <List>
          {menuItems.map(({ label, url }) => (
            <ListItem button key={label} onClick={() => onLinkNavigation(url)}>
              <ListItemText primary={label} />
            </ListItem>
          ))}
        </List>
      )
    }
    
    /* ...Rest of code */
    Notes:
  • We moved the menuItems outside the component, this is simply to initialize the menuItems once and refer to it there after.
  • We declare the use of the History hook and explicitly require its push function for future use.
  • We then created a function onLinkNavigation to manage the users click event. Upon clicking we instruct the App to push the new navigation URL into the browsers History queue; then we hide the menu.
  • Here's what this new change looks like:
    ⚠️
    Hang on, this implementation has flaws!
    Even though this functionally works, it's unfortunately not accessible!
    MUI Have realized this is a problem and have provided a way for us to integrate 3rd party components such as react-router-dom Link component; which would ultimately render our ListItem component as an anchor tag, with a href value.
    Let's make the changes:
    // src/components/MainMenu/index.tsx
    
    import React from "react";
    import { Link } from "react-router-dom";
    
    import { Drawer, List, ListItem, ListItemText } from "@material-ui/core";
    
    const menuItems = [
      { label: 'Buttons', url: '/ui/buttons' },
      { label: 'Colour Palette', url: '/ui/colour-palette' },
      { label: 'Typogaphies', url: '/ui/typographies' },
    ]
    
    function MenuItems({ setOpenMenu }: { setOpenMenu: React.Dispatch<React.SetStateAction<boolean>> }) {
      return (
        <List>
          {menuItems.map(({ label, url }) => (
            <ListItem
              button
              component={Link}
              key={label}
              onClick={() => setOpenMenu(false)}
              to={url}
            >
              <ListItemText primary={label} />
            </ListItem>
          ))}
        </List>
      )
    }
    
    /* ...Rest of code */
    Notes:
  • We've imported the Link component from react-router-dom and then passed it through to the ListItem "component" property. This then extends the TypeScript definition of ListItem with the types of Link, making the "to" property available.
  • We then removed the need to include the History hooks as we've passed the menuItem's url value into the "to" property.
  • We update the "onClick" property to collapse the main menu there after.
  • 🍾
    Those links are now accessible!
    Parameterized Routes
    Depending on your App's architecture and the data in which it needs to process, there will be a time where you need to configure parameters.
    There are two type of parameters:
    Path Parameters:
    /productCategory/:category/product/:productId
    const { match: { params }} = useParams();
    console.log(params);
    // { category: string?, productId: string? }
    
    const { search } = useLocation();
    console.log(search);
    // ""
    Search Parameters:
    /products-page?category=CATEGORY_ID&productId=PRODUCT_ID
    const { search } = useLocation();
    console.log(search);
    // "?category=CATEGORY_ID&productId=PRODUCT_ID"
    
    const { match: { params }} = useParams();
    console.log(params);
    // {}
    You can also combine the two:
    /productCategory/:category/product/:productId?tab=general
    const { match: { params }} = useParams();
    console.log(params);
    // { category: string?, productId: string? }
    
    const { search } = useLocation();
    console.log(search);
    // "?tab=general"
    It can be hard to differentiate when to use either solution but I draw the line applying the following principles:
  • Use Path params if it follows on the Information Architecture, maintaining its hierarchy.
  • Fallback to Search params if it breaks the above or the Search param is used to alter a smaller section of your App.
  • For pure example, we can implement Parameterized Routes in our UI Library (this is just for demonstration purposes).
    import React from "react";
    import { Route, RouteComponentProps, Switch } from "react-router-dom";
    
    export default function Routes() {
      return (
        <Switch>
          <Route path="/ui/:name" component={UIPage} />
        </Switch>
      );
    }
    
    function UIPage({ match: { params: { name } } }: RouteComponentProps<{ name?: string }>) {
      return (
        <>
          name: {name}
        </>
      )
    }
    Notes:
  • We've replaced all of the explicit routes with a single pattern match Route. The convention is to add your arbitrarily defined parameter name after the parent route. ie. /ui/ = parent route. :name = parameter name.
  • We've then created a UIPage component so you can see how the parent Route component propagates data down.
  • We've defined the parameter Type inside the RouteComponentProps definition so our codebase has reference to it.
  • Here's a screenshot illustrating how the URL affects the View and what props get passed down through the Route HoC.
    Route Hooks
    There will be times you'll need access to the URL parameter when you are many levels deep in the component tree.
    This is where Route Hooks come into play, the hook exposes the current state of your BrowserRouter.
    Here's an example demonstrating the above need:
    import React from "react";
    import { Route, RouteComponentProps, Switch, useRouteMatch } from "react-router-dom";
    
    export default function Routes() {
      return (
        <Switch>
          <Route path="/ui/:name" component={UIPage} />
        </Switch>
      );
    }
    
    function UIPage({ match: { params: { name } } }: RouteComponentProps<{ name?: string }>) {
      return (
        <>
          name: {name}
          <Child1 />
        </>
      )
    }
    
    function Child1() {
      return <Child2 />
    }
    
    function Child2() {
      return <Child3 />
    }
    
    function Child3() {
      const { params } = useRouteMatch();
      return (
        <>
          <br />
          URL parameter: {JSON.stringify(params)}
        </>
      )
    }
    Notes:
  • The parent page renders Child1 -> renders Child2 -> renders Child3
  • Child3 uses the the useRouteMatch hook which exposes the route's current Match properties. The component now has access to the URL parameter to do as it wishes.
  • Notice how clean this implementation is, there are no prop drilling annoyances.
    Let's now use this hook to show which of the Left Menu items are activate.
    // src/components/MainMenu/index.tsx
    
    import React from "react";
    import { Link, useLocation } from "react-router-dom";
    
    import { Drawer, List, ListItem, ListItemText } from "@material-ui/core";
    
    const menuItems = [
      { label: 'Buttons', url: '/ui/buttons' },
      { label: 'Colour Palette', url: '/ui/colour-palette' },
      { label: 'Typogaphies', url: '/ui/typographies' },
    ]
    
    function MenuItems({ setOpenMenu }: { setOpenMenu: React.Dispatch<React.SetStateAction<boolean>> }) {
      const { pathname } = useLocation();
    
      return (
        <List>
          {menuItems.map(({ label, url }) => (
            <ListItem
              button
              component={Link}
              key={label}
              onClick={() => setOpenMenu(false)}
              style={pathname === url ? { backgroundColor: '#40bfb4' } : undefined}
              to={url}
            >
              <ListItemText primary={label} />
            </ListItem>
          ))}
        </List>
      )
    }
    
    /* ...Rest of code */
    Notes:
  • We've introduced the useLocation hook so we can use the pathname to validate if one of our links are active
  • We've added a style prop to the ListItem component so we can visually change the background colour if it is active.
  • useHistory vs useLocation
    Sometimes you need access to the current pathname, derived from the Location object. It can be easy to confuse where to retrieve the current pathname from as both useHistory and useLocation expose it. But the truth of the matter is useLocation is the one to use in this case as it exposes the current state values.
    Redirect
    There might be times where your App's Information Architecture changes and you need to redirect users from 1 area to another. This is where Redirect comes in handy, you simply find the Route you want to target and define the Redirect component.
    import React from "react";
    import { Redirect, Route, RouteComponentProps, Switch, useRouteMatch } from "react-router-dom";
    
    export default function Routes() {
      return (
        <Switch>
          <Redirect from="/ui/:name" to="/uiNew/:name" />
          <Route path="/uiNew/:name" component={UIPage} />
        </Switch>
      );
    }
    
    /* ...Rest of code */
    Notes:
  • We've inserted the Redirect component before the Route Component
  • We've defined the from prop with the old URL we want to redirect from. Likewise we're defined the to prop to instruct where to redirect to.
  • We've updated the Route to contain the new pathname and the rest is business as usual.
  • 🙏 Closing
    At this stage your application should be wrapped with a Router Component.
    You should have enough knowledge on how to setup your Applications routes, link between pages and use Router hooks to access parameterized data.
    You are now ready to move onto the next episode where I’ll walk you through how to implement React Components, covering the following topics:
  • Component Fundamentals
  • Component Composition
  • Performance Considerations
  • Don't be shy, get in touch with us!

    32

    This website collects cookies to deliver better user experience

    FED Talk! Episode 3: Setting Up Routes in React