DEV Community

Cover image for Exploring the Browser Environment through Routing
Ash
Ash

Posted on • Updated on

Exploring the Browser Environment through Routing

For better or worse, the npm library has changed the day-to-day life of devs the world over. While NPM can be host to poorly maintained, un-documented, and even malicious packages - there are just as many helpful, well-intentioned libraries and frameworks designed to get the job done better and faster.

Some libraries are so well supported, and so good at doing their job, they become a standard. React Router, with over 4 million weekly downloads at the time of this writing, is a great example of just such a library.

Unfortunately, even well-supported libraries experience deprecations and breaking changes. The only difference is that when a library like React Router breaks, many pages across the world-wide-web break with it.

By peering under the hood to see how React Router works, we can mend future breaks faster and gain a deeper understanding of how it interacts with the browser environment - a rich resource available to us at all times.


Prerequisites: The Browser Environment, the DOM, and the BOM

React is written in JavaScript, a language initially constructed to exist inside the browser environment, and in many ways the browser was built around JS, as well.

Though terms like environment can sometimes sound elusive, we only need to understand that an environment is nothing more than a collection of bindings and their values. These values are often objects or functions that store information and help us do a job. Their bindings are their names - the ways in which we must call them so that we can use the value they contain.

To get a better understanding of the structure of the browser environment, let's take a very broad look from the top level.

screenshot

The top-level, or root, object of the browser environment is known as the window object. This massive object houses all the properties and methods of the browser environment. With the data, logic, and behavior that lives here, JS is able to execute on the screen. Beyond that, the JS can borrow from this object to accomplish tasks.

This is exemplified by the first child seen in the diagram above, called the document object or DOM. The DOM provides us with a virtual, tree-like representation of the elements on the page, along with ways to isolate and manipulate them. You might be familiar with some of these functions, like document.body.getElementByID() or document.body.getElementByClassName().

The DOM is a subject covered early, and often so exhaustively when first learning to work with JS that it is often taken for granted. The document property of the window object did not always exist as it does now, in a standardized form. In the early days of the Web, every browser followed a different set of ideas and implementations, and cross-browser support simply wasn't a thing.

In those days the DOM was more similar to our next subject - the browser object model, or BOM. To make things more confusing, the BOM is also commonly known as Web APIs, or the hybridized BOM APIs.

Whatever you call it, the BOM serves as the core of JS in the browser. It's host to a collection of objects and interfaces that are always there, waiting to be used. We could get information about the user's display with screen, their browser OS with navigator, and, what we'll be most concerned with today, their destination with the location object.

As evidenced by its many names, the BOM is not well defined or standardized - meaning every browser provides a different set of BOM objects. Which explains why we won't find this functionality on a handy parent object, like document, but rather stuck directly onto the root window object.

Thankfully, though while not officially standardized, most browsers today are more alike than different. We work around differences in varying BOM implementations with supplemental pieces of code, like WebKit, language wrappers like jQuery, and utilizing cross-browser supports built into each.


Project Introduction

The example repo for this post can be found here

This project explores routing by keeping things simple. There are 4 main, pre-built components - App, About, Home, and Contact. App will serve as the parent component, initially rendering all 3 children simultaneously.

project hierarchy

We're going to handle building our own routing from the ground up, discussing Reactive patterns and exploring the browser environment along the way. We'll build 2 more components, Link and Route, to do that. We'll also construct a Nav component and briefly discuss navigation.


Brute Force Routing

If you're following along on the repo, the starter code for this section can be found on branch section-one/setup. The final code can be found on branchsection-two/brute-force-routing.

The basic premise of routing is simple: When the user is at a certain location, show a certain component or page. To do that we need to know the user's url at any given moment. Lucky for us, that information is always available to us in the browser environment on the window.location object.

To take a look at the location object in the App component, we'll log it at the top of the App component:

src/App.js

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

function App() {
  // ⭐
  console.log(window.location);

  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
      <Home />
      <About />
      <Contact />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The console should return an object with a lot of properties and a few methods.

console screenshot

This is the entire location object. Most of the properties found here hold information directly concerned with the current url, a standardized string that specifies a virtual resource's location and content. The location object includes a separate key-value pair for each of its constituent parts.

anatomy of a url graphic

To accomplish routing for pages within the same app we're only really concerned with the pathname property. By watching the pathname we can decide when we should or should not render a component.

We'll start by removing the component instances from the return, and defining a function named showHome at the top of the App component:

src/App.js

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

function App() {
    // ⭐
  const showHome = () => {
    if (window.location.pathname === '/') {
      return <Home />;
    }
  };

  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
            {showHome()}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The showHome function's job is simple: If the window.location.pathname contains the the string "/" (that is, when it is empty) return the Home component.

To use the function, we've simply called it in the JSX. Opening the project you should see the Home component rendering. If we update the url to something like http://localhost:3000/about, the Home component will be removed.

We'll create and call 2 more functions, showAbout and showContact, for the remaining components:

src/App.js

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

function App() {
  // routing functions
  const showAbout = () => {
    if (window.location.pathname === '/about') {
      return <About />;
    }
  };

  const showContact = () => {
    if (window.location.pathname === '/contact') {
      return <Contact />;
    }
  };

  const showHome = () => {
    if (window.location.pathname === '/') {
      return <Home />;
    }
  };

  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
      {showAbout()}
      {showContact()}
      {showHome()}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

A quick save and a visit to the browser, and we can see that when the url is updated, and we spell everything correctly, the About and Contact components render at their corresponding pathname.

Just like that - we've created brute force routing, and all we had to do was look to the surrounding environment and read the information found there. However, there are some holes in this approach.

For starters, the url has to be manually updated when the user wants to see a different component. Furthermore, each of the show functions is very similar. We could easily refactor, combining all of the show functions into one function capable of receiving multiple arguments... but this entire approach is decidedly un-Reactive.

Instead, we'll build a reusable component with the logic to decide when it should and shouldn't show its children. We'll call it the same thing it's known as in React Router: The Route component.


The Route Component

If you're following along on the repo, the code for this section can be found on branch section-three/Route-component.

To begin building the Route component, create another directory inside the src folder named routing. Inside of routing create a file named Route.js with the following code:

src/routing/Route.js

const Route = ({ children, path }) => {
  return window.location.pathname === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

The Route component is a simple function component. So simple that because it's not returning any JSX, there's no need to explicitly have React in scope. Meaning we don't need to import React from 'react'.

The component requires 2 props - children and path. The path is checked against the window.location.pathname value, while children represents a React-specific compositional principle.

When creating components we intend to use as "generic boxes", like the Route component, they likely will not know their children ahead of time. Will it be a component? A dataset? A link? An image?

Passing the children prop and consuming it in the return allows us to work around that unknown. Using it ensures the component can effortlessly consume any child we provide - be that another component, a string, or any other piece of data. To see how the children prop is used, import and use the Route component in App.js.

After importing, remove the show functions and their instances from the JSX, and add an opening and closing <Route> tag in their place. The Route component requires a path attribute and value, which we'll set to "/". Finally, provide a basic string between tags:

src/App.js

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

import Route from './routing/Route';

function App() {
  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
         <Route path="/">
         "I'm being passed on the `children` prop!"
         </Route>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Back in Route.js add a log statement at the top of the component, save, and run the project.

src/routing/Route.js

const Route = ({ children, path }) => {
  console.log(children);
  return window.location.pathname === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

In the browser we should see the passed string both on the webpage, and in the console.

This shows us that using the children prop is pretty simple and straightforward: The content passed between opening and closing JSX tags, like <Route> and </Route>, will be available to the parent component on the special children prop.

The children prop can be used to pass one, or more, components, strings, elements, and more. Which we can see by adding all 3 components between <Route> tags:

src/App.js 

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

import Route from './routing/Route';

function App() {
  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
      <Route path="/">
        <Home />
        <About />
        <Contact />
      </Route>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Save and refresh the project, and the console should return an array of 3 objects, each with the type of react.element.

console screenshot

Because we want each Route to conditionally render one child at a time, we'll finish by giving each component its own path and <Route>:

src/App.js 

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';

import Route from './routing/Route';

function App() {
  return (
    <div className="App">
      <h1 className="headline">Routing in React</h1>
      <Route path="/">
        <Home />
      </Route>
      <Route path="/about">
        <About />
      </Route>
      <Route path="/contact">
        <Contact />
      </Route>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

On the webpage we'll have essentially the same functionality we had in the brute force routing approach, but we're no longer repeating ourselves. Additionally, this new approach takes better advantage of key principles and patterns in React.

By updating the url we can see each of the components at their respective path, but we're still missing an important and expected feature - navigation.


The Nav Component

If you're following along on the repo, the code for this section can be found on branch section-four/navigation.

When users arrive at a webpage, especially one with subsequent inner pages and resources, they expect the app to have some type of navigation in place. That could be a dropdown menu, a navigation bar, or any one of the many patterns for organizing a collection of links. At the end of the day, navigation, be it good or bad, can be the "make it or break it" feature of an app.

Because our app is basic, we'll use the nav bar approach to implement and discuss navigation. Inside the components directory make a new file named Nav.js. Nav will return some basic JSX:

src/components/Nav.js

import React from 'react';

const Nav = () => {
  return (
    <div className="nav">
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
    </div>
  );
};

export default Nav;
Enter fullscreen mode Exit fullscreen mode

This component resembles any basic, run-of-the-mill nav bar you might come across in the wild. It works just like one too - meaning the anchor element, <a>, has pre-defined, default behavior coming from the browser environment.

To explore why this presents problems in React, we'll import and use the Nav component in App.js:

src/App.js 

import About from './components/About';
import Contact from './components/Contact';
import Home from './components/Home';
import Nav from './components/Nav';

import Route from './routing/Route';

function App() {
  return (
    <div className="App">
      <Nav />
      <h1 className="headline">Routing in React</h1>
      <Route path="/">
        <Home />
      </Route>
      <Route path="/about">
        <About />
      </Route>
      <Route path="/contact">
        <Contact />
      </Route>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Though the problems surrounding a full-page reload are less obvious in small projects like ours, in more robust programs this behavior can cause a cascade of costly network requests, unexpected state changes, and other less-than-desirable consequences.

Furthermore, when the user first arrives at the page their machine and browser have already taken care of loading the CSS, the JS, the html and more. There's simply no reason to do this over and over again as they navigate through the app.

To solve for this, we'll need to stop the anchor's default behavior from ever being triggered and provide custom logic in its place. In React we can do that by wrapping the anchor element in a component, where we will have full control. We'll call this component the Link component.


The Link Component

The code for this section can be found on branch section-four/navigation.

In the routing directory, make a new a file named Link.js. To start, declare a function component that returns a pair of <a> tags, with a href value of "/", and text "Link" between:

/src/routing/Link.js

import React from 'react';

const Link = () => {
    return <a href="/">Link</a>;
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

👆 Because the component only returns a single element we can omit the smooth parentheses with a single line return!

In Nav.js import the Link component and provide it in place of the anchor tags:

src/components/Nav.js

import React from 'react';

import Link from '../routing/Link';

const Nav = () => {
  return (
    <div className="nav">
      <Link>Home</Link>
      <Link>About</Link>
      <Link>Contact</Link>
    </div>
  );
};

export default Nav;
Enter fullscreen mode Exit fullscreen mode

After saving, the project should reveal a set of three identical Link elements on the page.

Link screenshot

All the links navigate to the same location, that is http://localhost:3000/. Therefore they will all lead to the Home component view.

To make the Link component reusable we can pass props as placeholders for values that can be changed with each instantiation. The Link component requires a few props: a href attribute, the children, and a className.

Back in Link.js we'll start by passing href:

src/routing/Link.js

import React from 'react';

const Link = ({ href }) => {
    return <a href={href}>Link</a>;
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

The href address is no longer hardcoded, so the Link must explicitly receive a passed href value at the time of instantiation. Saving this update will break the app, but we can quickly fix this by providing the href prop to each Link back in Nav.js:

src/components/Nav.js

import React from 'react';

import Link from '../routing/Link';

const Nav = () => {
  return (
    <div className="nav">
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/contact">Contact</Link>
    </div>
  );
};

export default Nav;
Enter fullscreen mode Exit fullscreen mode

In the browser we'll see that the app is back up, and the Links are working again - mostly. They all still say "Link", rather than "Home", "About", or "Contact". This doesn't help the user much. They can probably already tell that the Link is, in fact, a link simply because it's a blue, underlined piece of text.

To pass a destination description, we'll use the React children prop as we did in the Route component:

src/routing/Link.js

import React from 'react';

const Link = ({ href, children }) => {
  return <a href={href}>{children}</a>;
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

Using the children prop, any text we pass between Link tags will be passed and consumed by the Link component. No updates are required in Nav.js, saving now will show all of the Links restored with no question as to where they lead.

Link update screenshot

Finally, we'll pass an optional className prop. This step isn't strictly necessary, but it's a good idea to "future-proof" code with this kind of foresight.

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {
  return <a href={href} className={className}>{children}</a>;
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

We used ES6 destructuring assignment syntax to provide a default value. That way, if a Link is used without a given className nothing will break. By default it will receive the class name "link", or it can be provided by us in the instance that we need a special link in the future.

Now, you may be thinking "Great, we're back to where we started." To some degree you're correct, but we've installed ourselves in a much greater seat of power. On the component-level we can now begin wrapping the Link in custom logic and behavior.


Preventing Default Behavior

The code for this section can be found on branch section-four/navigation.

We discussed why the browser's default behavior for anchor elements presents a problem in React. By wrapping the anchor element in a React component we started providing it dynamic data. Now we can begin re-wiring its behavior, cherry-picking functionality from the browser environment that fits our needs.

To handle user interactions anchor elements wait for an event (e.g. clicking) to occur. Once this happens, an event object containing all sorts of information about what the event was, where it occurred, and what's to happen next is emitted. In React we have access to all of this within the anchor's onClick property, where we can attach a handler function.

At the top of Link.js, we'll declare our own click handler, called onClick to keep things simple, and connect it in the JSX:

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {
    // ⭐
    const onClick = (e) => {
        // ...
    };

  return (
        <a 
            onClick={onClick} 
            href={href} 
            className={className}
        >
                {children}
        </a>;
    );
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

The onClick function takes in the event object, e. To stop default behavior the first thing we'll do is use the browser-provided preventDefault() method on the event:

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {

    const onClick = (e) => {
        // ⭐
        e.preventDefault();
    };

  return (
        <a 
            onClick={onClick} 
            href={href} 
            className={className}
        >
                {children}
        </a>;
    );
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

In the browser if we click a Link we'll see there is no full-page reload, the url is not updated, and well... nothing happens. One line of code has effectively "shut off" the anchor element's behavior, and presented the opportunity to step in a redefine it ourselves.


Implementing Custom Behavior

If you're following along on the repo, the code for this and the next section can be found on branch section-five/custom-PopStateEvent.

We know that our Route component relies on the url pathname property. So our first concern is finding a way to update this piece of information. Luckily, we don't have to look too far or hard to find it.

Nestled not-so-deeply in the BOM, alongside the location object, is the history object. As its name suggests, the history object contains information regarding session history. This includes an array of visited urls, known as the history stack, a forward(), back(), and go() function, and several other methods, including pushState().

The pushState() method can be used to add to the history stack, but not trigger a full-page reload. The method comes with a couple of caveats - first, pushState() can only be used to add a new url to the history stack if that url shares the same origin as the current url.

The pages we are routing between belong to the same document, and originate from the same host, or origin. They're simply small pieces of a larger whole, making pushState a perfect option for our use case.

In Link.js, we'll update the onClick handler right below the call to preventDefault():

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {

    const onClick = (e) => {
        e.preventDefault();
            // ⭐
        window.history.pushState({}, '', href);
    };

  return (
        <a 
            onClick={onClick} 
            href={href} 
            className={className}
        >
                {children}
        </a>;
    );
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

There are three values passed to the pushState() method - a state object, a title, and the url destination, which we have access to through the href prop.

pushState graphic

In the project you'll now notice that while clicking a Link does not update the render, it does update the url, and that means we're halfway there.

Using the environment, we found a way to update the url, but avoid costly reloads and network requests - but we've run up against the other caveat of using pushState(). This one surrounds a special type of event that occurs in the browser known as a popstate event.


Creating a Custom PopStateEvent

By definition a popstate event is a special type of event dispatched on the window whenever the active history entry changes, but only when that change is a consequence of a browser action. Browser actions include clicking the back and forward buttons, and changing the url manually. Our call to pushState() is not a browser action, so a popstate event is not emitted.

Because the popstate event, which inherits from the parent Event object, is built perfectly for our needs we still want to use it, and we can.

Below the call to pushState(), we can explicitly create our own popstate event using the new keyword and the PopStateEvent constructor:

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {

    const onClick = (e) => {
        e.preventDefault();
        window.history.pushState({}, '', href);
        // ⭐
        const navEvent = new PopStateEvent('popstate');
    };

  return (
        <a 
            onClick={onClick} 
            href={href} 
            className={className}
        >
                {children}
        </a>;
    );
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

We've constructed a synthetic event, named navEvent with a type of popstate. Once created, the navEvent must be manually "turned on", or emitted. This is done using the browser-provided dispatchEvent() method:

src/routing/Link.js

import React from 'react';

const Link = ({ href, children, className = 'link' }) => {

    const onClick = (e) => {
        e.preventDefault();
        window.history.pushState({}, '', href);
        const navEvent = new PopStateEvent('popstate');
        // ⭐
        window.dispatchEvent(navEvent);
    };

  return (
        <a 
            onClick={onClick} 
            href={href} 
            className={className}
        >
                {children}
        </a>;
    );
};

export default Link;
Enter fullscreen mode Exit fullscreen mode

Unfortunately, in the browser we won't find our app magically working. The links still update the url, but fail to change the render. Because we dispatched the navEvent to the window object, all of its children should be aware of it. So what gives?

In this instance components are a lot like us as humans. Simply being aware of an occurrence doesn't guarantee that we know what to do about it, or in response to it. We could freeze and do nothing, act uncharacteristically, or need to be told how to respond.

So, while the Link component successfully sends of an event object when clicked, the problem lies with the Route component. While aware of the event, it needs help to understand what to do next.


Updating the Route Component

In the first iteration of the Nav component all of the links were working - that is to say clicking one brought us to a new page. But this was because the anchor elements triggered a full-page reload, re-initializing everything in the app all over again at the correct destination. We've disabled this costly process, but lost some expected functionality along the way. To reinstate some of that behavior we created our own navigation event by borrowing the popstate event type from the browser environment.

The last step requires clueing in the Route component, coaching it to handle the popstate event using an event listener. The browser provides 2 self-explanatory methods you're probably familiar with, addEventListener() and removeEventListener(), to accomplish this.

Performing tasks with event listeners in the confines of React is typically a good indication that we'll require the useEffect hook. This special hook gives us the power to reach into the lifecycle of a component - opening windows wherein we can turn listeners on and off.

To start, import the useEffect hook directly from React. At the top of the component set up the useEffect function with an empty callback and dependency array:

src/routing/Route.js

import { useEffect } from 'react';

const Route = ({ children, path }) => {
    // ⭐
    useEffect(() => {},[]);

  return window.location.pathname === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

React hooks come with their own pre-defined behavior and purpose, and the choice to employ useEffect here is no accident. The code placed in the hook's first argument, the callback function, will run after first render - the perfect moment in time to turn on the event listener.

Inside the callback function we'll declare an arrow function, called onPathChange(), which will serve as the event handler. Then we'll bind an event listener to the window object:

src/routing/Route.js

import { useEffect } from 'react';

const Route = ({ children, path }) => {
    // ⭐
    useEffect(() => {
        // 1️⃣ handler function
        const onPathChange = () => {
            console.log("URL path changed!");
        };
        // 2️⃣ event listener
        window.addEventListener('popstate', onPathChange);
    },[]);

  return window.location.pathname === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

The onPathChange function is passed as the second argument to addEventListener(), the first argument is a string indicating the event type.

Back in the browser, with the console open, clicking a link should return 3 log statements of "URL path changed!" We get 3 statements because there are 3 instances of the Route component currently rendering and now listening for a popstate event.

project screenshot

Now that Route has started listening for a popstate event, it's aware when the url pathname changes, but it is not privy to what the pathname change to. To isolate this information we'll use another React hook, called useState.

The useState hook returns a slice of state, which we'll call currentPath, and its setter function, named setCurrentPath by convention. Finally useState requires one argument, the initial state value - which we'll set to the browser-maintained window.location.pathname property. To finish, we'll consume currentPath in the return:

src/routing/Route.js

import { useEffect, useState } from 'react';

const Route = ({ children, path }) => {
    // ⭐
    const [currentPath, setCurrentPath] = useState(window.location.pathname);

    useEffect(() => {
        const onPathChange = () => {
            console.log("URL path changed!");
        };
        window.addEventListener('popstate', onPathChange);
    },[]);
  // ⭐
  return currentPath === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

While window.location.pathname is kept constantly up-to-date by the browser, it's important to note that our currentPath piece of state is a separate variable. Upon render it is given the value of whatever window.location.pathname is at that moment, but to change the currentPath any time after render we must use its setter function, setCurrentPath. Only then can the Route component know when and what the url pathname has changed to.

To update currentPath in response to a popstate event, call the setCurrentPath function in the event handler - setting it to now updated window.location.pathname:

src/routing/Route.js

import { useEffect, useState } from 'react';

const Route = ({ children, path }) => {

    const [currentPath, setCurrentPath] = useState(window.location.pathname);

    useEffect(() => {
        const onPathChange = () => {
      // ⭐
            setCurrentPath(window.location.pathname);
        };
        window.addEventListener('popstate', onPathChange);
    },[]);

  return currentPath === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

In short, the popstate event lets the Route component know that a change has been made to window.location.pathname, its event listener then goes about retrieving those changes. After that, it can figure out which of its children, i.e. Home, About, or Contact, should be rendered.

In the browser the links should be working again. The Route and Link components are working together with the browser environment to track a user's location in the app and update as needed. Crucially, they do all of this without triggering a full-page reload... But we're not quite done yet.

In the case that any of the Route or Link components is removed from render at any time in the future, we must turn the event listener off. Failing to do so will result in a breaking error.

Thankfully the useEffect hook has a special feature known as a cleanup function. The cleanup function can be optionally returned from the hook, and will execute before the component is removed from the render.

To use this feature in Route.js, we'll update the useEffect hook found there to return an anonymous function:

src/routing/Route.js

import { useEffect, useState } from 'react';

const Route = ({ children, path }) => {

    const [currentPath, setCurrentPath] = useState(window.location.pathname);

    useEffect(() => {
        const onPathChange = () => {
            setCurrentPath(window.location.pathname);
        };
        window.addEventListener('popstate', onPathChange);
      // ⭐
      // 🧼 anonymous cleanup function
        return () => {
            window.removeEventListener('popstate', onPathChange);
        };
    },[]);

  return currentPath === path ? children : null;
};

export default Route;
Enter fullscreen mode Exit fullscreen mode

The cleanup function turns off the listener with the removeEventListener() method, completing the circle. Basic routing, from beginning to end, is now in place.


Though it seems nebulous, the browser environment wraps everything we do in React, Vue, Angular, and JS at large. In fact, beneath the interfaces of many npm libaries, we'll find the foundations for the framework often belong to the browser, proving that understanding it is essential to truly understanding and working with JavaScript.

I hope this post has helped dispel some of the magic around not just React Router, but the browser environment, and the workings of npm libraries at large.

Resources:

🦄 As always - thank you for reading! 🕶

Top comments (0)