wouter icon indicating copy to clipboard operation
wouter copied to clipboard

Support relative links

Open maxsalven opened this issue 10 months ago • 2 comments

Would love to see support for relative links in Wouter!

URL: https://www.example.com/some/long/unnested/route/foo

<Link to="../bar">Takes you to example.com/some/long/unnested/route/bar</Link>

I appreciate we can nest things but that's not always an appropriate option.

maxsalven avatar Feb 21 '25 09:02 maxsalven

It's not something we have in the roadmap, but maybe this can be solved with a custom wrapper around Link component e.g.

// under the hood, takes current location from `useLocation`, resolves the absolute path and renders a regular Link
<RelLink to="../bar" />

If you get it working, please let me know, maybe we can add this to the README.

molefrog avatar Feb 21 '25 19:02 molefrog

I am not sure I understood the assignment correctly, but I have a similar issue. For me thinking in terms of "unix" paths is easier when building my routes.

For now, I've settled with this solution:

import { useCallback } from 'react';
import { useLocation, useRouter } from 'wouter';

type Return = ReturnType<typeof useLocation>;

function join(...paths: string[]): string {
  const parts = paths.flatMap(p => p.split('/'));
  const newParts: string[] = [];

  for (const part of parts) {
    if (part === '' || part === '.') {
      continue;
    }
    if (part === '..') {
      newParts.pop();
    } else {
      newParts.push(part);
    }
  }

  if (newParts.length === 0) {
    return '/';
  }

  return '/' + newParts.join('/');
}

export function useRelativeLocation(base = '/'): Return {
  const { hook } = useRouter();
  const [location, navigate] = hook(); // `useBrowserLocation` by default

  const relativeNavigate = useCallback(
    (to: string, options: Parameters<Return[1]>[1]) => {
      if (to.startsWith('.')) {
        const newPath = join(location, to);

        if (base === '/' || newPath === base || newPath.startsWith(`${base}/`)) {
          return navigate(newPath, options);
        }
      } else if (to.startsWith('/')) {
        if (base === '/' || to === base || to.startsWith(`${base}/`)) {
          return navigate(to, options);
        }
      }

      return navigate(base);
    },
    [base, location, navigate],
  );

  return [location, relativeNavigate];
}

Note that the base param is passed to useRelativeLocation because the base returned in useRouter is messed up by nesting contexts. Also when trying to navigate outside the basePath, instead of adding it automatically, it defaults on going back to the base path, which may or may not be what you wanted.

Implementing matching <Link> and <Redirect> should be easy, so I didn't include them here.

richardgarnier avatar Sep 15 '25 09:09 richardgarnier