raviger

repository·main·Indexed 41 days ago

https://github.com/kyeotic/raviger

A tiny, zero-dependency, hook-based React router (version 5.3.0) that reacts to URL changes by listening to popstate events. It provides a lightweight routing system featuring the useRoutes hook, Link and ActiveLink components, and programmatic navigation via the navigate function.

Tokens
20.3K
Snippets
86
Records
93
Agent score
49%

What's inside raviger

  1. Configure `basePath` for `<Link />` components

    main

    When a <Link /> is used within a router context (i.e., inside a component tree provided by useRoutes), it automatically inherits the basePath defined in the useRoutes configuration.

    If you need to change the base path for a specific link, you can provide a basePath prop directly to the <Link />, which will override the inherited value. The basePath is prepended to the href value.

    import { useRoutes, Link } from 'raviger'
    
    function Home () {
      return (
        <div>
          {/* If useRoutes has basePath: 'app', this resolves to /app/foo */}
          <Link href="/foo" /> 
          
          {/* This overrides the inherited path and resolves to /bar/foo */}
          <Link href="/foo" basePath="/bar" /> 
        </div>
      )
    }
    
    const routes = {
      '/': () => <Home />
    }
    
    export default function App() {
      return useRoutes(routes, { basePath: 'app' })
    }
  2. Detect how navigation was initiated

    main

    You can use the initiatedBy field in the RavigerLocation object to determine if a user navigated via the browser buttons or if the navigation was triggered programmatically.

    ValueCause
    'push'Programmatic call to navigate() (adds a history entry)
    'replace'Programmatic call to navigate({ replace: true }) (replaces current history entry)
    'pop'Browser back/forward button
    undefinedFired on initial mount or basePath change (not a navigation event)
    import { useCallback } from 'react'
    import { useLocationChange } from 'raviger'
    
    function App () {
      const onChange = useCallback(location => {
        if (location.initiatedBy === 'pop') {
          console.log('User navigated with browser back/forward')
        } else if (location.initiatedBy === 'push') {
          console.log('Programmatic navigation to', location.path)
        } else if (location.initiatedBy === 'replace') {
          console.log('Programmatic replace to', location.path)
        }
      }, [])
    
      useLocationChange(onChange)
    }
  3. Control route matching priority with an array

    main

    While useRoutes can take an object, JavaScript object key order is not strictly guaranteed. To ensure specific routes match before more generic ones (e.g., matching /comp1/view1/* before /comp1/*), pass an array of { path, fn } objects. The priority is determined by the order of elements in the array.

    // Use an array to ensure specific paths are checked before wildcards
    const routes = [
      { path: '/comp1/view2/*', fn: () => <h1>Comp-1-view-2</h1> },
      { path: '/comp1/view1/*', fn: () => <h1>Comp-1-view1</h1> },
      { path: '/comp1/*', fn: () => <h1>Just Comp-1</h1> },
      { path: '/comp2/*', fn: () => <h1>Comp-2</h1> },
    ];
    
    // In your component:
    let route = useRoutes(routes);
  4. Detecting `navigate` events

    main

    When navigating within the same origin, raviger dispatches a custom popstate event. You can distinguish these from standard browser back/forward button events by checking two properties on the event object:

    • __tag: 'raviger:navigation': Present on all events dispatched by navigate().
    • __method: 'push' | 'replace': Indicates whether history.pushState or history.replaceState was used.

    Best Practice: If you are using the useLocationChange hook, use the initiatedBy field on the RavigerLocation object to determine the cause of the change, as it already handles this distinction for you.

  5. Navigate using Link and navigate

    main

    Raviger provides two primary ways to navigate:

    1. <Link> component: The preferred method for UI navigation. It accepts all standard <a> element properties and requires an href prop. It uses history.pushState to navigate without a full page refresh.
    2. navigate function: Use the exported navigate function for programmatic navigation (e.g., inside an event handler or an effect).

    Unlike some routing libraries, raviger listens to all popstate events, meaning it will trigger updates even if navigation is triggered by external scripts or different React instances on the same page.

  6. Quick Start with raviger

    main

    To set up a basic router, define a routes object where keys are path strings (including support for parameters like :userId) and values are functions that return a React component. Use the useRoutes hook to resolve the current route based on the URL. For navigation, use the Link component with an href prop.

    import { useRoutes, Link, useQueryParams } from 'raviger'
    import { Home, About, Users } from './Pages.js'
    
    const routes = {
      '/': () => <Home />,
      '/about': () => <About />,
      '/users/:userId': ({ userId }) => <Users id={userId} />
    }
    
    export default function App() {
      let route = useRoutes(routes)
      return (
        <div>
          <div
            >
            <Link href="/">Home</Link>
            <Link href="/about">About</Link>
            <Link href="/users/1">Tom</Link>
            <Link href="/users/2">Jane</Link>
          </div>
          {route}
        </div>
      )
    }
  7. Quick Start with useRoutes and Link

    main

    To set up basic routing, define a routes object where keys are URL paths and values are functions returning React components. Use the useRoutes hook to render the current route's component and the <Link> component for navigation. Path parameters (e.g., :userId) are passed as props to the component function.

    import { useRoutes, Link, useQueryParams } from 'raviger'
    
    const routes = {
      '/': () => <Home />,
      '/about': () => <About />,
      '/users/:userId': ({ userId }) => <User id={userId} />
    }
    
    export default function App() {
      let route = useRoutes(routes)
      return (
        <div>
          <div
            >
            <Link href="/">Home</Link>
            <Link href="/about">About</Link>
            <Link href="/users/1">Tom</Link>
            <Link href="/users/2">Jane</Link>
          </div>
          {route}
        </div>
      )
    }
  8. Migrate useQueryParams setter options in v5

    main

    In Raviger v5, the options object for the useQueryParams setter has been renamed to align with useNavigate. If you are upgrading from a version prior to v5, you must update your setter calls to use the new property names.

    • Rename replace to overwrite (this controls whether the querystring is merged or overwritten).
    • Rename replaceHistory to replace (this controls whether the update causes a history push or a replace).

    New setQueryParamsOptions interface:

    export interface setQueryParamsOptions {
      /**
       * Controls whether the querystring is overwritten or merged into
       *
       * default: true
       */
      overwrite?: boolean
      /**
       * Controls whether the querystring update causes a history push or replace
       *
       * default: false
       */
      replace?: boolean
    }
    // Example of the updated v5 setter usage
    const setParams = useQueryParams();
    
    // To overwrite instead of merge, and use replaceState:
    setParams({ key: 'value' }, { overwrite: true, replace: true });
  9. Migrate navigate to use an options object

    main

    In Raviger v4, the navigate function was changed from using multiple overloads to using a single signature that accepts an options object. This change improves readability but requires updating all existing calls.

    Key changes include:

    • Boolean arguments for replacement must now be passed within an object as { replace: boolean }.
    • Query parameters must now be passed within an object as { query: object }.
    • State must now be passed within an object as { state: object }.

    Migration Examples:

    Replacement logic:

    // Old: navigate('/path', true)
    // New:
    navigate('/path', { replace: true })
    
    // Old: navigate('/path', { name: 'raviger' })
    // New:
    navigate('/path', { query: { name: 'raviger' } })
    
    // Old: navigate('/path', undefined, undefined, { name: 'raviger'})
    // New:
    navigate('/path', { state: { name: 'raviger' } })
    navigate('/path', { replace: true })
  10. Migrate useLocationChange to handle RavigerLocation objects

    main

    In Raviger v4, the useLocationChange hook's setter function was updated. Instead of receiving just a path string, it now receives a RavigerLocation object (similar to window.location). This object contains a path property and a pathname property.

    To avoid refactoring your entire application, you can use a wrapper function like usePathChange to emulate the previous behavior where only the path string was provided to the setter.

    Compatibility Wrapper:

    export function usePathChange(
      setFn: (path: string | null) => void,
      options: LocationChangeOptionParams = {}
    ): void {
      useLocationChange(
        useCallback((location: RavigerLocation) => setFn(location.path), [setFn]),
        options
      )
    }
    useLocationChange(useCallback((location: RavigerLocation) => setFn(location.path), [setFn]), options)
  11. Configure `useLocationChange` options

    main

    The useLocationChange hook accepts an optional configuration object:

    • inheritBasePath (boolean, default: true): If true, the hook inherits the basePath from the Raviger context. This takes precedence over the provided basePath option.
    • basePath (string): The fallback base path if no context is available.
    • isActive (boolean | (() => boolean)): A conditional check. If this is defined and falsey, the setFn will not be invoked during location changes.
    • onInitial (boolean, default: false): If true, the setFn is called immediately on the component's initial mount.
    useLocationChange(
      setFn,
      {
        inheritBasePath = true,
        basePath = '',
        isActive,
        onInitial = false,
      } = {}
    )