rutter Svelte Themes

Rutter

Type-safe framework-agnostic Router, built with URLPattern & History API.

About

Rutter is a framework-agnostic, lightweight router. Built with URLPattern & History API. Internal reactivity is powered by Signal.

This library doesn't ship polyfill for URLPattern. You may consider installing urlpattern-polyfill.

Usage

VanillaJS

import { CreateHistory } from "rutter";

const router = new CreateHistory({
  routes: {
    index: {
      pathname: "",
    },
    about: {
      pathname: "/about",
    },
    blog: {
      pathname: "/blog",
    },
    blogDetail: {
      pathname: "/blog/:id",
    },
  },
});

router.on("index"); // boolean
router.onOneOf(["index", "about"]); // boolean

React bindings: via useState/context

// router.(tsx|jsx)

import { FC, PropsWithChildren, createContext, useContext, useEffect, useState } from "react";

import { CreateHistory } from "rutter";

export const { redirect, on, summaryState, routeState, watchSummaryState, watchRouteState } =
  new CreateHistory({
    routes: {
      index: {
        pathname: "",
      },
      about: {
        pathname: "/about",
      },
      blog: {
        pathname: "/blog",
      },
      blogDetail: {
        pathname: "/blog/:id",
      },
    },
  });

/**
 * Although using with `context` is recommended for performance reason, you can directly use this hook if you don't want to store all the states in `context` tree.
 */
export const useRouterValues = () => {
  const [routeStateValue, setRouteStateState] = useState(routeState);
  const [summaryStateValue, setSummaryStateState] = useState(summaryState);

  useEffect(() => watchRouteState(setRouteStateState), []);
  useEffect(() => watchSummaryState(setSummaryStateState), []);

  return {
    routeState: routeStateValue,
    summaryState: summaryStateValue,
  };
};

const context = createContext({
  routeState,
  summaryState,
});

const useRouterContext = () => useContext(context);

export const RouterProvider: FC<PropsWithChildren> = ({ children }) => {
  const value = useRouterValues();

  return <context.Provider value={value}>{children}</context.Provider>;
};

export const useRoute = () => {
  const { routeState } = useRouterContext();

  return routeState;
};
// app.(tsx|jsx)

import { FC } from "react";

import { on, redirect, useRoute, RouterProvider } from "./router";

const Routing: FC = () => {
  const { is404, ...restStates } = useRoute();

  return (
    <>
      <nav>
        <button onClick={() => redirect("index")}>Index</button>

        <button onClick={() => redirect("blog")}>Blog</button>

        <a href="/invalid-url">
          <button>404</button>
        </a>
      </nav>

      <fieldset>
        <legend>Body:</legend>

        <div>
          {is404 ? (
            <h1>404 Page</h1>
          ) : (
            <>
              {on("index") && <h1>Index Page</h1>}

              {on("about") && <h1>About Page</h1>}

              {on("blog") && (
                <>
                  <h1>Blog Page</h1>

                  <button
                    onClick={() =>
                      redirect("blogDetail", {
                        params: {
                          id: 123,
                        },
                      })
                    }
                  >
                    Blog Detail
                  </button>
                </>
              )}

              {on("blogDetail") && <h1>Blog Detail Page</h1>}
            </>
          )}
        </div>
      </fieldset>

      <fieldset>
        <legend>Current route detail:</legend>

        <code>
          <pre>{JSON.stringify(restStates, null, 2)}</pre>
        </code>
      </fieldset>
    </>
  );
};

const App: FC = () => (
  <RouterProvider>
    <Routing />
  </RouterProvider>
);

Vue bindings: via shallowRef/computed

// router.(ts|js)

import { computed, shallowRef } from "vue";
import { CreateHistory } from "rutter";

import { mapValues } from "lodash-es";

const router = new CreateHistory({
  routes: {
    index: {
      pathname: "",
    },
    about: {
      pathname: "/about",
    },
    blog: {
      pathname: "/blog",
    },
    blogDetail: {
      pathname: "/blog/:id",
    },
  },
});

const {
  //
  summaryState,
  routeState,
  watchSummaryState,
  watchRouteState,
  on,
} = router;

export const { redirect } = router;

export const routerState = shallowRef(summaryState);
export const route = shallowRef(routeState);

export const is404 = computed(() => route.value.is404);

export const matches = computed(() => {
  const { details } = routerState.value;

  type RouteNames = keyof typeof details;

  return mapValues(details, (_, name) => on(name as RouteNames));
});

watchSummaryState((state) => {
  routerState.value = state;
});

watchRouteState((state) => {
  route.value = state;
});
<script setup lang="ts">
// app.vue
import { redirect, route, matches, is404 } from "./router";
</script>

<template>
  <nav>
    <button @click="() => redirect('index')">Index</button>

    <button @click="() => redirect('blog')">Blog</button>

    <a href="/invalid-url">
      <button>404</button>
    </a>
  </nav>

  <fieldset>
    <legend>Body:</legend>
    <div>
      <h1 v-if="is404">404 Page</h1>

      <template v-else>
        <h1 v-if="matches.index">Index Page</h1>

        <h1 v-if="matches.about">About Page</h1>

        <template v-if="matches.blog">
          <h1>Blog Page</h1>

          <button @click="() => redirect('blogDetail', { params: { id: 123 } })">
            Blog Detail
          </button>
        </template>

        <h1 v-if="matches.blogDetail">Blog Detail Page</h1>
      </template>
    </div>
  </fieldset>

  <fieldset>
    <legend>Current route detail:</legend>

    <code>
      <pre>{{ route }}</pre>
    </code>
  </fieldset>
</template>

Svelte bindings: via readable/derived

// router.(ts|js)

import { readable, derived } from "svelte/store";
import { CreateHistory } from "rutter";

import { mapValues } from "lodash-es";

const router = new CreateHistory({
  routes: {
    index: {
      pathname: "",
    },
    about: {
      pathname: "/about",
    },
    blog: {
      pathname: "/blog",
    },
    blogDetail: {
      pathname: "/blog/:id",
    },
  },
});

const { summaryState, routeState, watchSummaryState, watchRouteState } = router;

export const { redirect, on, onOneOf } = router;

export const route = readable(routeState, watchRouteState);
export const routerState = readable(summaryState, watchSummaryState);

export const matches = derived(routerState, ({ details }) =>
  mapValues(details, (_, name) => on(name as keyof typeof details)),
);
<script lang="ts">
  // app.svelte

  import { redirect, route, matches } from './router'

  $: ({ is404, ...restState } = $route)
  $: data = JSON.stringify(restState, null, 2)
</script>

<nav>
  <button on:click={() => redirect('index')}>Index</button>

  <button on:click={() => redirect('blog')}>Blog</button>

  <a href="/invalid-url">
    <button>404</button>
  </a>
</nav>

<fieldset>
  <legend>Body:</legend>

  <div>
    {#if is404}
      <h1>404 Page</h1>
    {:else}
      {#if $matches.index}
        <h1>Index Page</h1>
      {/if}

      {#if $matches.about}
        <h1>About Page</h1>
      {/if}

      {#if $matches.blog}
        <h1>Blog Page</h1>

        <button
          on:click={() => redirect('blogDetail', { params: { id: 123 } })}
        >
          Blog Detail
        </button>
      {/if}

      {#if $matches.blogDetail}
        <h1>Blog Detail Page</h1>
      {/if}
    {/if}
  </div>
</fieldset>

<fieldset>
  <legend>Current route detail:</legend>

  <code>
    <pre>{data}</pre>
  </code>
</fieldset>

Documentation

Type API: https://paka.dev/npm/rutter/api

Development

pnpm i
pnpm dev

Top categories

Loading Svelte Themes