Skip to content
Flexible Routing + Seamless DX = πŸš€
Branch: master
Clone or download
Latest commit 22dd393 May 23, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.vscode switch overload signature for better tooling Apr 6, 2019
artwork adds splash page Apr 27, 2019
docs fixes sandbox May 23, 2019
src fixes createGroup validation May 1, 2019
website fixes sandbox May 23, 2019
.gitignore updates code examples to use codesandbox May 22, 2019
LICENSE.md initial commit Apr 4, 2019
README.md updates readme May 18, 2019
jest.config.js initial commit Apr 4, 2019
package-lock.json makes types explicit May 5, 2019
package.json makes types explicit May 5, 2019
tsconfig.json adds docusaurus Apr 29, 2019

README.md



type-route


type-route




Disclaimer: type-route has not yet reached version 1.0. The api is unstable and subject to change without warning. The library itself may never reach version 1.0. Early feedback is welcome, but using this library in its current state for anything other than a throw away project is not recommended.

Β 



Documentation


Introduction

Flexible Routing + Seamless DX = πŸš€

The APIs of existing routing libraries aren't optimized for static analysis. Users of these libraries suffer from a sub-par developer experience because of this. type-route was designed with a fully statically analyzable API in mind. This means great developer tooling out of the box when using TypeScript or an editor like VSCode whose JavaScript experience is powered by TypeScript under the hood. The API makes extensive use of type inference and almost no explicit type annotations are needed to achieve a fully typed code base. type-route is powered by the same core library as React Router. From this solid foundation type-route adds a simple and flexible API optimized for a developer experience that is second to none.

This project is in its early stages and community feedback is essential to push it further. Feel free to use the issue tracker for anything from bugs to questions, and suggestions/pain-points to positive experiences.


Getting Started

Install

npm install type-route

Sandbox

See https://codesandbox.io/s/l4z98vw559 to play with an editable example of type-route.

Code Example

import { createRouter, defineRoute } from "type-route";

const { routes, listen } = createRouter({
  home: defineRoute("/"),
  postList: defineRoute(
    {
      page: "query.param.number.optional"
    },
    p => `/post`
  ),
  post: defineRoute(
    {
      postId: "path.param.string"
    },
    p => `/post/${p.postId}`
  )
});

listen(nextRoute => {
  console.log(nextRoute);
});

routes.home.push();
// url "/"
// logs { name: "home", params: {} }

routes.postList.push();
// url "/post"
// logs { name: "postList", params: {} }

routes.postList.push({ page: 1 });
// url "/post?page=1"
// logs { name: "postList", params: { page: 1 } }

routes.post.push({ postId: "abc" });
// url "/post/abc"
// logs { name: "postId", params: { postId: "abc" } }

Code Example with React

type-route isn't coupled to any specific UI framework/library. Originally it was intended to be a specialized React routing solution. Designing the API, however, revealed that a framework agnostic approach actually led to better React integration. The resulting API works seamlessly with React but also benefits from the flexibility of not being tied to it.

import { createRouter, defineRoute, Route } from "type-route";

const { routes, listen, getCurrentRoute } = createRouter({
  home: defineRoute("/"),
  postList: defineRoute(
    {
      page: "query.param.number.optional"
    },
    () => `/post`
  ),
  post: defineRoute(
    {
      postId: "path.param.string"
    },
    p => `/post/${p.postId}`
  )
});

function App() {
  const [route, setRoute] = useState(getCurrentRoute());

  useEffect(() => {
    const listener = listen(nextRoute => {
      setRoute(nextRoute);
    });

    return () => listener.remove();
  }, []);

  return (
    <div>
      <a {...routes.home.link()}>Home</a>
      <a {...routes.postList.link()}>PostList</a>
      <a {...routes.postList.link({ page: 1 })}>PostList Page 1</a>
      <a {...routes.post.link({ postId: "abc" })}>Post abc</a>
      <Page route={route} />
    </div>
  );
}

function Page(props: { route: Route<typeof routes> }) {
  const { route } = props;

  switch (route.name) {
    case routes.home.name:
      return <HomePage />;
    case routes.postList.name:
      return <PostListPage page={route.params.page} />;
    case routes.post.name:
      return <PostPage postId={route.params.postId} />;
    default:
      return <NotFoundPage />;
  }
}

function HomePage() {
  return <div>Home</div>;
}

function PostListPage(props: { page?: number }) {
  return <div>PostList {props.page}</div>;
}

function PostPage(props: { postId: string }) {
  return <div>Post {props.postId}</div>;
}

function NotFoundPage() {
  return <div>NotFound</div>;
}

API Overview

defineRoute

defineRoute(path: string): RouteDefinitionBuilder;
defineRoute(
  params: ParameterCollection,
  path: (pathParams: PathParameterCollection) => string
): RouteDefinitionBuilder;

This method will create a route definition builder object to be consumed by createRouter. The simplified version of the call is an alias for defineRoute({}, () => path). The parameters object passed to defineRoute is a map of variable names to the following strings representing the type of parameter being declared:

  • "path.param.string" - A parameter of type string found in the pathname of the url.
  • "path.param.number" - A parameter of type number found in the pathname of the url.
  • "query.param.string" - A parameter of type string found in the query string of the url.
  • "query.param.number" - A parameter of type number found in the query string of the url.
  • "query.param.string.optional" - An optional parameter of type string found in the query string of the url.
  • "query.param.number.optional" - An optional parameter of type number found in the query string of the url.

Examples

defineRoute("/");

Defines a route matching "/"

defineRoute(
  {
    userId: "path.param.string",
    page: "query.param.number",
    search: "query.param.string.optional"
  },
  p => `/user/${p.userId}/posts`
);

Defines a route matching: "/user/some-id/posts?page=1&search=hello" or "/user/some-id/posts?page=1"


extend

const user = defineRoute(
  {
    userId: "path.param.string"
  },
  p => `/user/${p.userId}`
);

const { routes, listen } = createRouter({
  home: defineRoute("/"),
  userSummary: user.extend("/"),
  userSettings: user.extend("/settings"),
  userPostList: user.extend("/post"),
  userPost: user.extend(
    {
      postId: "path.param.string"
    },
    p => `/post/${p.postId}`
  )
});

The extend function has the exact same signature as defineRoute. Both return a RouteDefinitionBuilder object which can then be extended itself. The path parameter of the extend function is relative to the base RouteDefinitionBuilder object. In the above example the userSettings route would match the path /user/someid/settings. The parameter definitions you pass to extend are merged with with the parameter definitions from the base RouteDefinitionBuilder object. There can be no overlap in parameter definition names between the base and extended RouteDefinitionBuilder.


createRouter

createRouter(routeDefinitions: RouteDefinitionBuilderCollection): Router
createRouter(historyType: "browser" | "memory", routeDefinitions: RouteDefinitionBuilderCollection): Router

Initializes a router. By default it will create a browser history router. You may also explicitly set the history type to "browser" or "memory". Using "memory" will create an environment agnostic router. This would be useful if, for instance, you're developing a React Native application.

Example

const { routes, listen, getCurrentRoute, history } = createRouter({
  home: defineRoute("/"),
  postList: defineRoute(
    {
      page: "query.param.number.optional"
    },
    p => `/post`
  ),
  post: defineRoute(
    {
      postId: "path.param.string"
    },
    p => `/post/${p.postId}`
  )
});

createRouter will create a Router object. Immediately destructuring this Router object into the properties your application needs is the recommended style.


routes

const { routes } = createRouter({
  home: defineRoute("/")
});

routes.home.name; // "home"
routes.home.push();
routes.home.replace();
routes.home.href();
routes.home.link();
routes.home.match();

The routes property of a Router object is a map of route names to a RouteDefinition object (not to be confused with the RouteDefinitionBuilder object that defineRoute creates). The RouteDefinition object contains properties and functions for interacting with that specific route in your application.


name

const { routes, getCurrentRoute } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

const route = getCurrentRoute();

if (route.name === routes.post.name) {
  console.log(route.params.postId);
  // Here both you and the editor will know that we're on
  // the "post" route and that route.params has a property
  // called "postId" of type string.
}

The name field is a constant value used for comparing a specific Route to a particular RouteDefinition. As shown in the example above this allows you to determine which route you're dealing with.


push

const { routes } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

routes.home.push(); // returns Promise<boolean>
routes.post.push({ postId: "abc" }); // returns Promise<boolean>

The push function will push a new entry into history and if using the "browser" historyType will update the browser's url. If the route has parameters those will need to be provided to the push function. Returns a Promise which resolves to a boolean indicating whether or not the navigation completed successfully. The only instance where the navigation would not be successful would be if the handler function passed to listen returned false.


replace

const { routes } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

routes.home.replace(); // returns Promise<boolean>
routes.post.replace({ postId: "abc" }); // returns Promise<boolean>

The replace function will replace the current entry in history and if using the "browser" historyType will update the browser's url. If the route has parameters those will need to be provided to the replace function. Returns a Promise which resolves to a boolean indicating whether or not the navigation completed successfully. The only instance where the navigation would not be successful would be if the handler function passed to listen returned false.


href

const { routes } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

routes.home.href(); // returns "/"
routes.post.href({ postId: "abc" }); // returns "/post/abc"

The href function will construct a string representing the href for the route. If the route has parameters those will need to be provided to the href function.


link

const { routes } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

routes.home.link(); // returns { href: "/", onClick: Function }
routes.post.link({ postId: "abc" }); // returns { href: "/post/abc", onClick: Function }

The link function will construct an object containing both an href property and an onClick function. When called, the onClick function calls preventDefault on the event object passed to it and triggers that particular route's push function with the parameters provided to link. In React, for example, the link function may be used like this:

<a {...routes.home.link()}>Home</a>
<a {...routes.post.link({ postId: "abc" })}>Post "abc"</a>

match

const { routes } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`),
  postList: defineRoute({ page: "query.param.number.optional" }, () => `/post`)
});

routes.home.match({
  pathName: "/"
}); // returns { }
routes.home.match({
  pathName: "/abc"
}); // returns false
routes.post.match({
  pathName: "/post/abc"
}); // returns { postId: "abc" }
routes.postList.match({
  pathName: "/post"
}); // returns { }
routes.postList.match({
  pathName: "/post",
  queryString: "page=1"
}); // returns { page: 1 }

The match function takes an object with a pathName field and optionally a queryString field. It tests if the route matches the given pathName and queryString. If the test fails false is returned. If the test succeeds an object containing the values of any matched parameters is returned (if the route has no parameters an empty object { } will be returned). While this function is exposed publicly, most applications should not need to make use of it directly.


listen

const { listen } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

// Creates a new listener
const listener = listen(nextRoute => {
  console.log(nextRoute);
  // logs:
  // { name: false, params: {} }
  // or
  // { name: "home", params: {} }
  // or
  // { name: "post", params: { postId: "abc" }}
  // (where "abc" is whatever was matched from the url)
});

// Removes the listener
listener.remove();

The listen function will create a new route listener. Anytime the application route changes this function will be called with the next matching route. If the given url does not match any route in that router an object with a false value for the name property and empty object for the params property will be returned.

Returning false (or a Promise which resolves to false) from this function will abort the url change. If, for instance, there are unsaved changes on the current page or an upload is in progress you may want to make the user confirm the navigation. You may hook into this functionality by doing something like the following:

listen(nextRoute => {
  if (unsavedChanges) {
    const result = confirm("Are you sure?");
    if (result === false) {
      return false;
    }
  }

  setRoute(nextRoute);
});

It is important to note that the listen function will trigger the handler you pass to it only when your application's route changes. If your application is somehow unloaded this handler will not be triggered. Examples of when this function will not be triggered in a web browser include:

  • closing the tab your application is running in
  • triggering an action that opens an external page
  • reloading the page your application is running in

Each of the above situations can instead be intercepted using the following code:

window.addEventListener("beforeunload", event => {
  if (unsavedChanges) {
    event.preventDefault();
    event.returnValue = ""; // Legacy browsers may need this
    return ""; // Legacy browsers may need this

    // An empty returnValue message is provided because modern browsers
    // will ignore any message set in code and instead provide a
    // generic message to the user asking them to confirm the
    // navigation.
  }
});

The above code will display a generic prompt to the user asking them to confirm the navigation. Asynchronous actions cannot be performed in this code block and ultimately you cannot prevent a user from leaving your application. This technique will only force them to confirm that this navigation is indeed what they want to do.


getCurrentRoute

const { getCurrentRoute } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

console.log(getCurrentRoute());
// logs:
// { name: false, params: {} }
// or
// { name: "home", params: {} }
// or
// { name: "post", params: { postId: "abc" }}
// (where "abc" is whatever was matched from the url)

The getCurrentRoute function will return the current route. Typically, the listen function would be used to update your application's state to reflect the current route over time. The getCurrentRoute function is more useful to ensure the initial state of your application is correct. For example when using type-route with React your code may resemble this:

function App() {
  const [route, setRoute] = useState(getCurrentRoute());

  useEffect(() => {
    const listener = listen(nextRoute => {
      setRoute(nextRoute);
    });

    return () => listener.remove();
  }, []);

  return <>Route {route.name}</>;
}

The initial route is retrieved via getCurrentRoute but all updates to the route object in the application's state are managed in the handler passed to the listen function.


history

const { history } = createRouter({
  home: defineRoute("/"),
  post: defineRoute({ postId: "path.param.string" }, p => `/post/${p.postId}`)
});

history.goBack();
history.goForward();

The history property of a router provides direct access to the underlying history instance from the core library which powers type-route. Most use cases won't require using this property. If you do need to access it, do so with caution as certain uses may cause unexpected behavior.


createGroup

const user = defineRoute(
  {
    userId: "path.param.string"
  },
  p => `/user/${p.userId}`
);

const { routes, listen } = createRouter({
  home: defineRoute("/"),
  userSummary: user.extend("/"),
  userSettings: user.extend("/settings"),
  userPostList: user.extend("/post"),
  userPost: user.extend(
    {
      postId: "path.param.string"
    },
    p => `/post/${p.postId}`
  )
});

const userPostGroup = createGroup([routes.userPostList, routes.userPost]);

const userGroup = createGroup([
  routes.userSummary,
  routes.userSettings,
  userPostGroup
]);

listen(nextRoute => {
  if (userGroup.has(nextRoute)) {
    nextRoute.name; // "userSummary" | "userSettings" | "userPostList" | "userPost"
  }
});

The createGroup function is useful for composing groups of routes to make checking against htme easier elsewhere in the application with the has function. It takes an array composed of both RouteDefinition and RouteDefinitionGroup objects.


has

See above.


Route

import { Route } from "type-route";

Route<typeof routes>
Route<typeof routes.home>
Route<typeof userGroup>

The Route type is part of the TypeScript specific api. If using TypeScript you can pass various objects in your application to it to get the type of the associated routes for the given object.

You can’t perform that action at this time.