Creating a Custom Router for SPAs in React
Tags: javascript, react
Published: 2021-03-01 15:12:55

Hey Guys 👋, I'm going to show you how to create a custom router in react. We will use the browser's [History] (https://developer.mozilla.org/en-US/docs/Web/API/History) API.

###How to detect the current url
The url information is stored in `window.location`. This returns a [location] (https://developer.mozilla.org/en-US/docs/Web/API/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
}
```


###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>
    )
}
```

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()](https://developer.mozilla.org/en-US/docs/Web/API/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>
    )
}
```

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](https://developer.mozilla.org/en-US/docs/Web/API/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>
    )
}
```

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
}
```

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); 
```

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
```


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>
    )
}
```

`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 👋.