Skip to content

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

License

Notifications You must be signed in to change notification settings

thihathit/rutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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