React: Navigation Without React-Router
Intro
The React-Router library is by far the most popular way to navigate around a React application. However there are times when you may not want to use this library. For example if you don't want to have to update your application when the React-Router library makes a breaking change, which is about once a year.
For our sample we are going to take our Widgets application that we have been working on and create a manual navigation between our different widgets.
window.location
One of the things that is fundamental to understand is the window.location read only property of the window interface.
In short, in a browser, each tab represents a Window, and each window contains a DOM document (a web page).
There is a global variable exposed to JavaScript code called window
, which has various properties, one of which is window.location.
and then within that property we can see another very important property called pathname
Showing Content Based On Pathname
Now that we know we can check the property window.location.pathname
we can use that knowledge to hide or show content based on the value of this property. For example let us re-work our App.js file to try this out.
We create a new function that returns our Accordion component if our location condition is met, and then call that function in the return section of the app.
import React, { useState } from "react"
import Accordion from "./components/Accordion"
const showAccordion = () => {
if (window.location.pathname === "/") {
return <Accordion />
}
}
export default () => {
return <div className="ui container">{showAccordion()}</div>
}
And as expected we can only see our dropdown on the homepage now.
We can easily follow this pattern to build a navigation for the rest of the widgets.
import React from "react"
import ColorSelect from "./components/ColorSelect"
import Translate from "./components/Translate"
import Accordion from "./components/Accordion"
import Search from "./components/Search"
const showAccordion = () => {
if (window.location.pathname === "/") {
return <Accordion />
}
}
const showColorSelect = () => {
if (window.location.pathname === "/color-select") {
return <ColorSelect />
}
}
const showTranslate = () => {
if (window.location.pathname === "/translate") {
return <Translate />
}
}
const showSearch = () => {
if (window.location.pathname === "/search") {
return <Search />
}
}
export default () => {
return (
<div className="ui container">
{showAccordion()}
{showColorSelect()}
{showTranslate()}
{showSearch()}
</div>
)
}
And this all works as expected. If you go to /search you see the search widget, etc etc.
There are some downsides to this however. For example, this code is not DRY. We have lots of repetitive checks going on here.
Building a Re-Usable Route Component
We could do something like this.
const showComponent = (route, component) => {
return window.location.pathname === route ? component : null
}
But in React the preferred way to do things is with components. So let us build a component for navigation. First we make a new component called Route
.
const Route = ({ path, children }) => {
return window.location.pathname === path ? children : null
}
export default Route
Note that instead of passing in the component as a prop directly we passed in children
which will pass in any components that are children of an instance of the Route component. In this way we can show multiple different components on each route. Also note that we did not need to import React into this component because it does not contain any JSX. Exports/Imports (AKA modules) are actually a function of Node, as we learned when we were working with Express.
Then we would re-write our App to be the following.
import React from "react"
import Route from "./components/Route"
import ColorSelect from "./components/ColorSelect"
import Translate from "./components/Translate"
import Accordion from "./components/Accordion"
import Search from "./components/Search"
export default () => {
return (
<div className="ui container">
<Route path="/">
<Accordion />
</Route>
<Route path="/color-select">
<ColorSelect />
</Route>
<Route path="/translate">
<Translate />
</Route>
<Route path="/search">
<Search />
</Route>
<Route path="/all">
<Accordion />
<ColorSelect />
<Translate />
<Search />
</Route>
</div>
)
}
And we can see that our /all route component displays all of the children
Implementing a Header For Navigation
This is simple enough. We just create a new component with some links and some Semantic UI styles.
import React from "react";
const Header = () => {
return (
<div className="ui secondary pointing menu">
<a href="/" className="item">
Accordion
</a>
<a href="/color-select" className="item">
Color Select
</a>
<a href="/translate" className="item">
Translate
</a>
<a href="/search" className="item">
Wiki Search
</a>
<a href="/all" className="item">
All Widgets
</a>
</div>
);
};
export default Header;
and then import that component into our application.
import React from "react";
import Header from "./components/Header";
import Route from "./components/Route";
import ColorSelect from "./components/ColorSelect";
import Translate from "./components/Translate";
import Accordion from "./components/Accordion";
import Search from "./components/Search";
export default () => {
return (
<div className="ui container">
<Header /> // highlight-line
<Route path="/">
<Accordion />
</Route>
<Route path="/color-select">
<ColorSelect />
</Route>
<Route path="/translate">
<Translate />
</Route>
<Route path="/search">
<Search />
</Route>
<Route path="/all">
<Accordion />
<ColorSelect />
<Translate />
<Search />
</Route>
</div>
);
};
This is great but currently the browser is completely reloading the whole page every time we click on one of these nav items. That means we are reloading all our JavaScript and CSS every time we click on a nav link. Let's improve this.
Preventing Full Page Reload
Creating A Link Component
To start let us create a new component called Link that will allow us to add event handlers to links. As props for this new component we will feed it everything that a normal link requires, including CSS classes, a destination href, and we will use the children prop to feed it the link text.
import React from "react";
const Link = ({ className, href, children }) => {
return (
<a className={className} href={href}>
{children}
</a>
);
};
export default Link;
We then replace all our links in our Header with our shiny new Links component.
import React from "react";
import Link from "./Link";
const Header = () => {
return (
<div className="ui secondary pointing menu">
<Link href="/" className="item">
Accordion
</Link>
<Link href="/color-select" className="item">
Color Select
</Link>
<Link href="/translate" className="item">
Translate
</Link>
<Link href="/search" className="item">
Wiki Search
</Link>
<Link href="/all" className="item">
All Widgets
</Link>
</div>
);
};
export default Header;
and we can see that all the information we needed gets passed in as props, just like if these were plain HTML links.
Prevent Page Reload
Now we want to go about adding our new functionality, which is to prevent the page reload. To start we will add a click event handler to our Link components.
import React from "react";
const Link = ({ className, href, children }) => {
// prevent full page reload
const onClick = (event) => {
event.preventDefault();
};
return (
<a className={className} href={href} onClick={onClick}> // highlight-line
{children}
</a>
);
};
export default Link;
Using the preventDefault()
method we have successfully stopped the page from reloading, as well as doing anything else. Now we need change the visibility of the URL and the content without actually reloading the page.
Change The URL
So what is the point of even changing the URL if we can just change the content dynamically without reloading the page? Simply because users expect to be able to bookmark certain portions of your site so that they can return to them. It is not strictly necessary for functionality, but is absolutely best practice.
There is a method built into the browser to manually update the URL. window.history.pushState()
MDN: history.pushState
Knowing that we can add that to the click listener on our Link component
import React from "react";
const Link = ({ className, href, children }) => {
// prevent full page reload
const onClick = (event) => {
event.preventDefault();
window.history.pushState({}, "", href) // highlight-line
};
return (
<a className={className} href={href} onClick={onClick}>
{children}
</a>
);
};
export default Link;
and note that we passed in the href
prop as the url to update to.
Communicate URL change to Route
We are successfully updating the URL when we click the Link components now. And now we need to alert our routes that the URL has changed, so that they can update the content in the window. We can do this with another native window method called window.dispatchEvent()
and a React method called PopStateEvent()
import React from "react";
const Link = ({ className, href, children }) => {
const onClick = (event) => {
// prevent full page reload
event.preventDefault();
// update url
window.history.pushState({}, "", href);
// communicate to Routes that URL has changed
const navEvent = new PopStateEvent('popstate');
window.dispatchEvent(navEvent);
};
return (
<a className={className} href={href} onClick={onClick}>
{children}
</a>
);
};
export default Link;
which then gets received in our Route component like so.
import { useEffect } from 'react'; // highlight-line
const Route = ({ path, children }) => {
useEffect(() => {
// define callback as separate function so it can be removed later with cleanup function
const onLocationChange = () => {
console.log('Location Change');
}
window.addEventListener('popstate', onLocationChange);
// clean up event listener
return () => {
window.removeEventListener('popstate', onLocationChange)
};
}, [])
return window.location.pathname === path
? children
: null;
}
export default Route;
What we did there was create a custom event, like "click", and we called it popstate
, and told that event to get triggered every time the URL changes. Then we added an event listener to our Route component, and said that every time the event popstate
occurs we fire the function onLocationChange()
, which currently just creates a console log. Then we added a cleaner function to clean up the event listener.
If any part of this is confusing you will want to reference the post where we learned about the useEffect
hook here Ncoughlin: React Hooks useEffect and then we learned about the CleanUp Function here Ncoughlin: React Throttling API Requests #cleanup-function
And this is currently working, every time we click on a link we are getting FIVE console logs (one for each route).
Get Route Component to Re-render Itself
Now that the Route is able to detect when the URL has changed, we are going to introduce a piece of state that is going to cause the Route to re-render itself. Remember that a component always re-renders when a piece of it's state changes, so that is what we are going to do, introduce a state and update it every time the URL changes.
import { useEffect, useState } from 'react';
const Route = ({ path, children }) => {
// state to track URL and force component to re-render on change
const [currentPath, setCurrentPath] = useState(window.location.pathname); // highlight-line
useEffect(() => {
// define callback as separate function so it can be removed later with cleanup function
const onLocationChange = () => {
// update path state to current window URL
setCurrentPath(window.location.pathname); // highlight-line
}
// listen for popstate event
window.addEventListener('popstate', onLocationChange);
// clean up event listener
return () => {
window.removeEventListener('popstate', onLocationChange)
};
}, [])
return currentPath === path // highlight-line
? children
: null;
}
export default Route;
In the third highlight we changed the boolean to compare the state currentPath
instead of the current URL window.location.pathname
, this is just for clarity as the purpose of the state to re-render the component has already been achieved, the component would still work with the window pathname as we have set currentPath
to be equal to it anyways.
Open Link In New Tab With Command Click
Some users expect that you will open a link in a new tab if you hold the command key, so we are going to implement that functionality.
import React from "react";
const Link = ({ className, href, children }) => {
const onClick = (event) => {
// if ctrl or meta key are held on click, allow default behavior of opening link in new tab
if (event.metaKey || event.ctrlKey) {
return;
}
// prevent full page reload
event.preventDefault();
// update url
window.history.pushState({}, "", href);
// communicate to Routes that URL has changed
const navEvent = new PopStateEvent('popstate');
window.dispatchEvent(navEvent);
};
return (
<a className={className} href={href} onClick={onClick}>
{children}
</a>
);
};
export default Link;
Inside of our check above you can see event.metaKey
and event.ctrlKey
, these are boolean properties of the mouse event. You can view a full list of available mouse event properties here MDN: MouseEvent
GitHub Repo
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.