Building a client side router in React with event hooks (pt1: events)
Tags: react, javascript, programming, tutorial
Published: 2021-09-08 14:58:29

## TLDR;

I'm making a pluggable widget component with front end and serverless back end parts.  This article is the first in the series and covers the usage of custom events in React to build a router.

* Handling events in React
* Raising custom events

## Overview

I'm embarking on a collaborative project to build a serverless widget for the [4C](https://4c.rocks) content creator community that I've recently joined.

The first thing that this project needs is a router on the client side and as I thought I'd use React, the first thing I thought of was React Router.  But then I thought, it's just a client side router and that might make an interesting opportunity to get into the heart of that problem and allow me to understand Routers more.

There's also a thing about React Router I don't like so much.  I always end up writing a wrapper around it so I can dynamically register routes in a declarative fashion rather than imperatively writing them inside the JSX.

```jsx
// What I want

import "./something-that-declares-routes.js"

register("/some/route/:id", <SomeComponent color="blue"/>)

export default function App() {
    return <Router />
}

```

```jsx
// Rather than

import "./something-that-declares-routes.js"
import {declaredRoutes} from "./declared-routes.js"

export default function App() {
     return <Router>
         <SomeComponent color="blue" path="/some/route/:id" />
         {declaredRoutes.map((route) => (<route.Component 
            key={route.path} path={route.path}/>)}
    </Router>
}
```
## What is a router?

So ok, what do we want from a router?  We want to be able to specify a pattern of URLs supplied to our app in order to convert them into some function to be called.  The function should also be able to take parameters from a route so:

```
   /some/:id/route?search&sort
```

Calls some registered function or component with the `id`, `search` and `sort` parameters from a url like this `/some/abc123/route?search=something&sort=name,desc`

```jsx
register("/some/:id/route?search&sort", <ShowInfo color="blue"/>)

function ShowInfo({id, search, sort, color}) {
   return /* something */
}
```

## The URL

So for routes to work we have to deal with the `window.location` object and know when it changes... either because we've navigated ourselves or the user has pressed the *Back* or *Forward* buttons.

From the `location` we will need to match routes based on the `pathname` and extract variables from the `pathname` and `search` properties to pass to our component.

The browser gives us an `onpopstate` event when the user navigates using the buttons, but there is no event for the navigation to a new URL so we are going to have to deal with that ourselves. 

## Events
>Let's keep our code simple by faking `onpopstate` events when the user navigates around our app using links.

I like events, I use events everywhere in my code to loosely couple components.  We've seen above that we will need to raise and handle events quite frequently so the first step on the journey is to build some tools to aid with that process.  

In this first part of the article we will create some useful functions to raise and handle events both inside and outside React components.

### The Plan

Because we are working with browser standard events I decided to just press the existing methods on `window` into service.  However, I want to be able to pass custom properties to a handler function as additional parameters, rather than creating dozens of custom events, so we will decorate up standard `Event` instances with the parameters passed along with the event, we'll do this so we don't accidentally conflict with any standard properties.

### Handling events

Our first function is then: one to attach a handler and deal with these extra properties, returning a method to detach the handler later.

```jsx
export function handle(eventName, handler) {
  const innerHandler = (e) => handler(e, ...(e._parameters || []))
  window.addEventListener(eventName, innerHandler)
  return () => window.removeEventListener(eventName, innerHandler)
}
```

Here we create an inner handler that uses a `_parameters` property on the event object to pass additional parameters to the handler.

Turning this into a hook for React is then child's play:

```jsx
export function useEvent(eventName, handler) {
  useLayoutEffect(() => {
    return handle(eventName, handler)
  }, [eventName, handler])
}
```

### Raising events

Writing a function to raise these events with custom parameters is also pretty easy:

```jsx
export function raise(eventName, ...params) {
  const event = new Event(eventName)
  event._parameters = params
  window.dispatchEvent(event)
  return params[0]
}
```
Note how we return the first parameter - that's an Inversion of Control helper, we might be raising events looking for return values, and this gives us an easy way of doing that.

```jsx
handle("get-stuff", (list)=>list.push("I'm here"))
// ...
handle("get-stuff", (list)=>list.push("Another choice"))
// ...
for(let stuff of raise("get-stuff", [])) {
   console.log(stuff)
}
```

By returning the first parameter we write a lot less boilerplate.

When we are working with events like `onPopState` we also want to decorate the event object with parameters (like the `state` for the `location`) so we do need another function to deal with this circumstance, that we will use every now and again:

```jsx
export function raiseWithOptions(eventName, options, ...params) {
  const event = new Event(eventName)
  Object.assign(event, options)
  event._parameters = params
  window.dispatchEvent(event)
  return params[0]
}
```

This one is very similar, just it decorates the custom event with the options object passed in.

## Bonus: Redrawing things when events happen

We may well want to get our React components to redraw based on events that have changed some global state.  There's an easy way to do that with a `useRefresh` hook that can either cause a refresh or register a function that will refresh after a sub function is called.

```jsx
import { useEffect, useMemo, useRef, useState } from "react"

export function useRefresh(...functions) {
    const [, refresh] = useState(0)
    const mounted = useRef(true)
    useEffect(() => {
        mounted.current = true
        return () => (mounted.current = false)
    }, [])
    const refreshFunction = useMemo(
        () =>
            (...params) => {
                if (params.length === 1 && typeof params[0] === "function") {
                    return async (...subParams) => {
                        await params[0](...subParams)
                        refreshFunction()
                    }
                }
                for (let fn of functions) {
                    if (fn) {
                        fn(...params)
                    }
                }
                if (mounted.current) {
                    refresh((i) => i + 1)
                }
            },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [...functions]
    )
    return refreshFunction
}

```

This creates us a utility function that causes React to redraw the component.  It's handy for lots of things but here we can just use it to do a refresh on an event:

```jsx
function Component() {
   const refresh = useRefresh()
   useEvent("onPopState", refresh)
   return null
}
```

The `useRefresh` function takes a list of other functions to call.  This is sometimes useful, especially for debugging

```jsx
    const refresh = useRefresh(()=>console.log("Redrawing X"))
```

And the returned function can be made to wrap a refresh around something:

```jsx
function Component() {
     const refresh = useRefresh()
     // do something with global state on window.location.search
     return <button onClick={refresh(()=>window.location.search = "?x"}>Set X</button>
}
```
## Conclusion

In this first part we've seen how to easily raise and handle events in React.  Below is the running widget that uses these techniques.  

{% github miketalbot/cccc-widget no-readme %}

{% codesandbox n4dpo initialpath=/BIr6y6QZhjz1fEJ-h4wJ_/embed runonclick=0 %}