Skip to content

Instantly share code, notes, and snippets.

@ultim8k
Last active June 15, 2020 10:42
Show Gist options
  • Save ultim8k/c825228e977676093422e544e571054d to your computer and use it in GitHub Desktop.
Save ultim8k/c825228e977676093422e544e571054d to your computer and use it in GitHub Desktop.
Dummy Router post

Hey, I made a dummy router with react and a couple Regular Expressions

Why?

Well, since the time I started playing with node.js and express I wondered how a router works under the hood. I even remember I once had a look on Backbone's router and didn't seem too hard to understand, but was wondering who is that crazy person who came up with this.

A couple days ago I wanted to build a simple markdown-notes list for fun using react. I started with a list of notes where you click one and navigate to the single note view. Ok, first I need to install React Router... Nah. That's boring. I'll just make a dummy router for now. How hard can it be? Let's see.

How?

So I had in mind something that works similar to React Router:

<Router>
  <Route path="/">
    list of items
  </Route>
  <Route path="/items/:slug">
    individual item
  </Route>
</Router>

But how does a router work inside?

I imagine that it has a render method that returns the active route if any.

export default class DummyRouter extends React.Component {
  // ...
  render() {
    return this.state.currentRoute || null;
  }
}

Then I guess we need to listen to the url change event and call a method that will update currentRoute in state. Lets start with hashchange event for now.

export default class DummyRouter extends React.Component {
  // ...
  componentDidMount() {
    // Listen on hash change:
    window.addEventListener("hashchange", this.selectRoute);
    // Run on first load:
    this.selectRoute();
  }
  // ...
}

Now in selectRoute() we need to go through all the routes which are direct children of router (routes) and check if their set path matches the url hash.

export default class DummyRouter extends React.Component {
  // ...
  selectRoute = () => {
    const currentPath = window.location.hash.slice(1) || '/';
    const { children: routes } = this.props;

    React.Children.map(routes, route => {
      if (route.props.path === currentPath) {
        this.setState({ currentRoute: React.cloneElement(route, {}) });
      }
    });
  }
  // ...
}

So that was it. Here is the router you asked:

import React from "react";

export default class DummyRouter extends React.Component {
  state = {
    currentRoute: null
  };

  selectRoute = () => {
    const currentPath = window.location.hash.slice(1) || '/';
    const { children: routes } = this.props;
    // I'd love to have a reduce or filter here.
    React.Children.map(routes, route => {
      // route matches current path
      if (route.props.path === currentPath) {
        this.setState({ currentRoute: React.cloneElement(route, {}) });
      }
    });
  }

  componentDidMount() {
    // Listen on hash change
    window.addEventListener("hashchange", this.selectRoute);
    // Run on first load
    this.selectRoute();
  }

  componentWillUnmount() {
    // Stop listening
    window.removeEventListener("hashchange", this.selectRoute);
  }

  render() {
    return this.state.currentRoute || null;
  }
}

What? This is not a real router!

I want to match patterns, not the exact string. I also want a way to grab the params like usernames or ids from the path.

That should be a simple Regular Expression, right?

Thinking... Thinking... Thinking...

We can have a Regular Expression to match patterns but then how do we know which param is which?

I know, Regular Expressions can have named groups. We can name each match with the word specified in the route path and then we can easily access it's value.

Oh no! JS doesn't support named groups in regular expressions.

But hey, I remember I read about some improvements the other day for regular expressions in ES6, thinking who is ever gonna need that. That's the irony of life.

All we need to do is to match a pattern like /mypath/:paraminpath/:otherparam and then replace it with a regular expression that collects the parameters. So /notes/:slug should be transformed to /\/notes\/\:(?<slug>\\w+)\//. This should be able to collect the slug so that we can use it to render the page with the correct data.

But how can we transform the pattern to that expression?

Using another regular expression of course. :D

/**
 * Generates Regular Expression to
 * extract parameters from path
 */
const transformPattern = pathPattern => {
  // Match text that starts with ":"
  const pathParamRX = /:(\w+)/;
  const matchedParam = pathPattern.match(pathParamRX);
  const paramName = matchedParam && matchedParam[1];
  // replace the text we matched with the right regex
  const routeRX = pathPattern.replace(pathParamRX, `(?<${paramName}>\\w+)`);

  // If unparsed params rerun or return generated regular expression
  return pathParamRX.test(routeRX)
    ? transformPattern(routeRX)
    : new RegExp(routeRX);
};

Awesome. Now all we need is to use that regular expression in order to find which route corresponds to the current location path.

/*
 * Checks if path matches route
 * and returns any parameters
 */
export const getPathPatternMatch = (pathPattern, currentPath) => {
  const patternParamsRX = transformPattern(pathPattern);
  // Try to get parameters from route path
  if (patternParamsRX) {
    const patternMatchesPath = currentPath.match(patternParamsRX);
    if (patternMatchesPath) {
      // Return match with any parameters found
      return patternMatchesPath.groups || {};
    }
  }
  return false;
};

And now we can call this function inside our Router's selectRoute method instead of the route.props.path === currentPath.

export default class DummyRouter extends React.Component {
  // ...
  selectRoute = () => {
    const currentPath = window.location.hash.slice(1) || '/';
    const { children: routes } = this.props;

    React.Children.map(routes, route => {
      const currentMatch = getPathPatternMatch(route.props.path, currentPath);

      if (currentMatch) {
        const currentRoute = React.cloneElement(route, {
          ...currentMatch
        });

        this.setState({ currentRoute });
      }
    });
  }
  // ...
}

Final result with some small improvements can be found in codesandbox and the complete Dummy Router code on github.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment