Creating a Custom Router for SPAs in React

Akash Shyam - Mar 1 '21 - - Dev Community

Hey Guys 👋, I'm going to show you how to create a custom router in react. We will use the browser's History API.

How to detect the current url

The url information is stored in window.location. This returns a location object. The location.pathname gives us the part of the url after the domain. For example, in a url - xdfjdkf.com/abc, abc is stands for location.pathname.

Building a simple Route component

We can render different components based on the location.pathname. We take in 2 props - the route(pathname) and children(content we want to render)

export function Route({route, children}) {
    return window.location.pathname === route ? children : 
    null
}
Enter fullscreen mode Exit fullscreen mode

Building a Link component

I'm using the default html a tag. Our link takes in 3 props - href(where the link should go to), className(for styling), children(what should the link say).

export function Link({href, className, children}) {
    return (
        <a href={href} className={className}>{children}</a>
    )
}
Enter fullscreen mode Exit fullscreen mode

You might be thinking, what is the difference between using our Link component and an a tag? Well, the difference is that when we click the a tag, a page reload occurs which defeats the purpose of building a router for a SPA(Single Page Application).

To prevent this, let's setup an event listener to handle click events. We will call e.preventDefault() to prevent the default action(page reload). Instead, we will use the window.history.pushState() method to change the URL.

The history.pushState() method adds an entry to the browser's session history stack.

export function Link({href, className, children}) {
    const handleClick = (e) => {
        e.preventDefault();
        window.history.pushState({}, '', href);
    }

    return (
        <a href={href} className={className} onclick={handleClick}>{children}</a>
    )
}
Enter fullscreen mode Exit fullscreen mode

This time around, when we try it, the URL changes but the rendered component does not change. This is because the rest of the application does not realise that the URL has changed. To do this we will dispatch a PopStateEvent.

A popstate event is dispatched to the window every time the active history entry changes between two history entries for the same document.

export function Link({href, className, children}) {
    const handleClick = (e) => {
        e.preventDefault();
        window.history.pushState({}, '', href);

        const event = new PopStateEvent('popstate');
        window.dispatchEvent(event);
    }

    return (
        <a href={href} className={className} onclick={handleClick}>{children}</a>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now, we have to setup an event listener in the router component to listen to this event. I'm going to put this event listener in the useEffect hook. In class based components, I'd add this method to componentDidMount. We only want to wire this up 1 time, so I will specify an empty array for the dependencies. We will return a function from the useEffect for cleanup i.e. removing the event listener.

export function Route({route, children}) {
    useEffect(() => {
        const onLocationChange = () => {
            //    Do something
        }

        window.addEventListener('popstate', onLocationChange);

        return () => {
            window.removeEventListener('popstate', onLocationChange);
        }
    }, [])

    return window.location.pathname === route ? children :
        null
}
Enter fullscreen mode Exit fullscreen mode

When the pathname changes, we want all the route components to re-render. How do we do that? You guessed it! By using state.

const [currentPath, setCurrentPath] = useState(window.location.pathname); 
Enter fullscreen mode Exit fullscreen mode

The comparison to check if the url is correct can technically stay the same but I'm going to set it to currentPath for simplicity's sake.

    return currentPath === route ? children :
        null
Enter fullscreen mode Exit fullscreen mode

Some of you guys might be using CMD + click or CTRL + click to open links in new tabs. This is something a lot of tutorials miss out on. Let's implement this functionality in our Link component.

export function Link({href, className, children}) {
    const handleClick = (e) => {
        if(e.metaKey || e.ctrlKey) {
            return;
        }

        e.preventDefault();
        window.history.pushState({}, '', href);

        const event = new PopStateEvent('popstate');
        window.dispatchEvent(event);
    }

    return (
        <a href={href} className={className} onclick={handleClick}>{children}</a>
    )
}
Enter fullscreen mode Exit fullscreen mode

metaKey stands for CMD and ctrlKey stands for CTRL. These are basically boolean values that tell us whether a user had pressed one of these keys while clicking the link. We want to return early and let the browser do its thing.

That's it for now. I hope you guys liked this post. If you have any questions, leave them in the comments and I'll try my best to answer them. Bye for now 👋.

. . . . . . . . . . . . . . . . . . . . . . .