Universal Router

repository·main·Indexed 23 days ago

https://github.com/kriasoft/universal-router

An isomorphic, framework-agnostic, middleware-style router for JavaScript web applications. It supports both client-side and server-side routing and can be used with React, Vue, Hyperapp, or any other framework. Key features include nested routes, URL parameters, a synchronous mode via UniversalRouterSync, and a generateUrls add-on for dynamic URL generation. Version 10.0.2.

Tokens
8.3K
Snippets
25
Records
40
Agent score
79%

What's inside universal-router

  1. Configure Nested Routes

    main

    Routes can be nested using the children property. This allows for hierarchical URL structures.

    • A child route with path: '' matches the parent's path exactly.
    • Nested paths are relative to the parent's path.
    • Catch-all behavior: Setting children: [] (an empty array) on a route makes it act as a catch-all for any URL starting with that route's path.
    const router = new UniversalRouter({
      path: '/admin',
      children: [
        {
          path: '', // matches /admin
          action: () => 'Admin Page',
        },
        {
          path: '/users',
          children: [
            {
              path: '', // matches /admin/users
              action: () => 'User List',
            },
            {
              path: '/:username', // matches /admin/users/john
              action: () => 'User Profile',
            },
          ],
        },
      ],
    })
    
    router.resolve({ pathname: '/admin/users/john' }).then(console.log)
    // => User Profile
  2. Understand the Route Context

    main

    When a route is resolved, the action function receives a context object. This object contains both data passed manually via router.resolve() and system-provided properties.

    System-provided properties in context:

    • router: The current UniversalRouter instance.
    • route: The matched route object.
    • next: A middleware-style function to continue resolution.
    • pathname: The URL passed to resolve().
    • baseUrl: The base URL path relative to the current route.
    • path: The matched path.
    • params: The matched path parameters.
    const router = new UniversalRouter({
      path: '/hello',
      action(context) {
        // 'user' was passed in the resolve call
        return `Welcome, ${context.user}!`
      },
    })
    
    router.resolve({ pathname: '/hello', user: 'admin' }).then(console.log)
    // => Welcome, admin!
  3. How Universal Router works

    main

    Universal Router uses a middleware-inspired approach. It is centered around the UniversalRouter class and its resolve method.

    When you call router.resolve(location), the router traverses a list of route objects until it finds the first match. A route is considered a match if its path matches the provided URL path and its action method returns a value that is not null or undefined.

    Each route is a plain JavaScript object with the following properties:

    • path: The URL path string to match.
    • action: A function that executes when the route matches. It should return the result you want to render or process.
    • children (optional): Nested routes.
    import UniversalRouter from 'universal-router'
    
    const routes = [
      { path: '/one', action: () => '<h1>Page One</h1>' },
      { path: '/two', action: () => '<h1>Page Two</h1>' },
      { path: '/*all', action: () => '<h1>Not Found</h1>' },
    ]
    
    const router = new UniversalRouter(routes)
    
    router.resolve({ pathname: '/one' }).then((result) => {
      document.body.innerHTML = result
      // renders: <h1>Page One</h1>
    })
  4. Implement Middlewares using context.next()

    main

    An action function can act as middleware by calling context.next(). This allows you to execute logic before or after a child route is resolved.

    • context.next(): Iterates through child routes.
    • context.next(true): Iterates through all remaining routes in the router.

    Behavioral nuances:

    • If a middleware returns null, the router skips all nested routes and moves to the next sibling route.
    • If a middleware returns undefined (or the action is missing), the router attempts to match child routes.
    • This is useful for permission checks: return null to block access to a branch, or return a specific error page string to redirect.
    const router = new UniversalRouter({
      path: '',
      async action({ next }) {
        console.log('middleware: start')
        const child = await next()
        console.log('middleware: end')
        return child
      },
      children: [
        {
          path: '/hello',
          action() {
            return 'Hello, world!'
          },
        },
      ],
    })
    
    router.resolve({ pathname: '/hello' })
    // Prints:
    // middleware: start
    // route: return a result
    // middleware: end
  5. Implement redirects by returning a redirect object from an action

    main

    To trigger a redirect, an action method in your route configuration should return an object containing a redirect key with the target path.

    Note that UniversalRouter does not perform the actual navigation (e.g., window.location change) itself. You must check the result of router.resolve() for the redirect property and handle the navigation in your application logic.

    import UniversalRouter from 'universal-router'
    
    const router = new UniversalRouter([
      {
        path: '/redirect',
        action() {
          return { redirect: '/target' } // <== request a redirect
        },
      },
      {
        path: '/target',
        action() {
          return { content: '<h1>Content</h1>' }
        },
      },
    ])
    
    router.resolve('/redirect').then((page) => {
      if (page.redirect) {
        window.location = page.redirect // <== actual redirect here
      } else {
        document.body.innerHTML = page.content
      }
    })
  6. Implement declarative redirects using resolveRoute

    main

    For a declarative routing style, you can add custom properties (like protected: true) to your route objects. Then, use the resolveRoute option in the UniversalRouter constructor to intercept the resolution process. If a route is marked as protected and the user is not authenticated, return a redirect object. You can also include a from property in the redirect object to track where the user was redirected from.

    const routes = [
      { path: '/login', content: '<h1>Login</h1>' },
      {
        path: '/admin',
        protected: true, // <== protect current and all child routes
        children: [
          { path: '', content: '<h1>Admin: Home</h1>' },
          { path: '/users', content: '<h1>Admin: Users</h1>' },
          { path: '/posts', content: '<h1>Admin: Posts</h1>' },
        ],
      },
    ]
    
    const router = new UniversalRouter(routes, {
      resolveRoute(context) {
        if (context.route.protected && !context.user) {
          return { redirect: '/login', from: context.pathname } // <== where the redirect come from?
        }
        if (context.route.content) {
          return { content: context.route.content }
        }
        return null
      },
    })
    
    router.resolve({ pathname: '/admin/users', user: null }).then((page) => {
      if (page.redirect) {
        console.log(`Redirect from ${page.from} to ${page.redirect}`)
        window.location = page.redirect
      } else {
        document.body.innerHTML = page.content
      }
    })
  7. Protect routes using the middleware approach

    main

    You can protect a group of routes by using a parent route as a middleware. In the parent route's action, check your authorization context. If the user is unauthorized, return a redirect object. If authorized, call context.next() to allow the router to proceed to the child routes.

    const adminRoutes = {
      path: '/admin',
      action(context) {
        if (!context.user) {
          return { redirect: '/login' } // stop and redirect
        }
        return context.next() // go to children
      },
      children: [
        { path: '', action: () => ({ content: '<h1>Admin: Home</h1>' }) },
        { path: '/users', action: () => ({ content: '<h1>Admin: Users</h1>' }) },
        { path: '/posts', action: () => ({ content: '<h1>Admin: Posts</h1>' }) },
      ],
    }