Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new 'useCreateHref' and 'useResolvePath' hooks #9161

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/serious-tips-wait.md
@@ -0,0 +1,8 @@
---
"react-router": minor
"react-router-dom": minor
"react-router-dom-v5-compat": minor
"react-router-native": minor
---

Add new `useCreateHref` and `useResolvePath` hooks for deferred creation of hrefs and paths.
24 changes: 24 additions & 0 deletions docs/hooks/use-create-href.md
@@ -0,0 +1,24 @@
---
title: useCreateHref
---

# `useCreateHref`

<details>
<summary>Type declaration</summary>

```tsx
declare function useCreateHref(): (
to: To,
options?: { relative?: RelativeRoutingType }
): string;
```

</details>

The `useCreateHref` hook returns function which will return a URL when called that may be used to link to the given `to` location, even outside of React Router.

> **Tip:**
>
> You may be interested in taking a look at the source for the `useHref`
> component in `react-router` to see how it uses `useCreateHref` internally.
24 changes: 24 additions & 0 deletions docs/hooks/use-resolve-path.md
@@ -0,0 +1,24 @@
---
title: useResolvePath
---

# `useResolvePath`

<details>
<summary>Type declaration</summary>

```tsx
declare function useResolvePath(): (
to: To,
options?: { relative?: RelativeRoutingType }
): Path;
```

</details>

This hook returns a function that resolves the `pathname` of the location in the given `to` value against the pathname of the current location.

> **Tip:**
>
> You may be interested in taking a look at the source for the `useResolvedPath`
> component in `react-router` to see how it uses `useResolvePath` internally.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -113,7 +113,7 @@
"none": "12.5 kB"
},
"packages/react-router/dist/umd/react-router.production.min.js": {
"none": "14.5 kB"
"none": "14.7 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "10.5 kB"
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router-dom-v5-compat/index.ts
Expand Up @@ -97,6 +97,7 @@ export {
resolvePath,
unstable_HistoryRouter,
useHref,
useCreateHref,
useInRouterContext,
useLinkClickHandler,
useLocation,
Expand All @@ -107,6 +108,7 @@ export {
useOutletContext,
useParams,
useResolvedPath,
useResolvePath,
useRoutes,
useSearchParams,
} from "./react-router-dom";
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router-dom/index.tsx
Expand Up @@ -142,6 +142,7 @@ export {
useAsyncError,
useAsyncValue,
useHref,
useCreateHref,
useInRouterContext,
useLoaderData,
useLocation,
Expand All @@ -154,6 +155,7 @@ export {
useOutletContext,
useParams,
useResolvedPath,
useResolvePath,
useRevalidator,
useRouteError,
useRouteLoaderData,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router-native/index.tsx
Expand Up @@ -90,6 +90,7 @@ export {
useAsyncError,
useAsyncValue,
useHref,
useCreateHref,
useInRouterContext,
useLoaderData,
useLocation,
Expand All @@ -102,6 +103,7 @@ export {
useOutletContext,
useParams,
useResolvedPath,
useResolvePath,
useRevalidator,
useRouteError,
useRouteLoaderData,
Expand Down
34 changes: 34 additions & 0 deletions packages/react-router/__tests__/useCreateHref-test.tsx
@@ -0,0 +1,34 @@
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import { MemoryRouter, Routes, Route, useCreateHref } from "react-router";

function ShowHref({ to }: { to: string }) {
const createHref = useCreateHref();
return <pre>{createHref(to)}</pre>;
}

describe("useCreateHref", () => {
describe("to a child route", () => {
it("returns the correct href", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/courses"]}>
<Routes>
<Route
path="courses"
element={<ShowHref to="advanced-react" />}
/>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<pre>
/courses/advanced-react
</pre>
`);
});
});
});
33 changes: 33 additions & 0 deletions packages/react-router/__tests__/useResolvePath-test.tsx
@@ -0,0 +1,33 @@
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import type { Path } from "react-router";
import { MemoryRouter, Routes, Route, useResolvePath } from "react-router";

function ShowResolvedPath({ path }: { path: string | Path }) {
const resolvePath = useResolvePath();
return <pre>{JSON.stringify(resolvePath(path))}</pre>;
}

describe("useResolvePath", () => {
it("path string resolves correctly", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/"]}>
<Routes>
<Route
path="/"
element={<ShowResolvedPath path="/home?user=mj#welcome" />}
/>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<pre>
{"pathname":"/home","search":"?user=mj","hash":"#welcome"}
</pre>
`);
});
});
4 changes: 4 additions & 0 deletions packages/react-router/index.ts
Expand Up @@ -84,6 +84,7 @@ import {
import type { NavigateFunction } from "./lib/hooks";
import {
useHref,
useCreateHref,
useInRouterContext,
useLocation,
useMatch,
Expand All @@ -93,6 +94,7 @@ import {
useOutletContext,
useParams,
useResolvedPath,
useResolvePath,
useRoutes,
useActionData,
useAsyncError,
Expand Down Expand Up @@ -181,6 +183,7 @@ export {
useAsyncError,
useAsyncValue,
useHref,
useCreateHref,
useInRouterContext,
useLoaderData,
useLocation,
Expand All @@ -193,6 +196,7 @@ export {
useOutletContext,
useParams,
useResolvedPath,
useResolvePath,
useRevalidator,
useRouteError,
useRouteLoaderData,
Expand Down
77 changes: 63 additions & 14 deletions packages/react-router/lib/hooks.tsx
Expand Up @@ -58,21 +58,53 @@ export function useHref(
`useHref() may be used only in the context of a <Router> component.`
);

let createHref = useCreateHref();

return React.useMemo(
() => createHref(to, { relative }),
[to, relative, createHref]
);
}

/**
* Returns a function that creates the full href for the given "to" value. This is useful for building
* custom links that are also accessible and preserve right-click behavior.
*
* @see https://reactrouter.com/docs/en/v6/hooks/use-create-href
*/
export function useCreateHref(): (
to: To,
options?: { relative?: RelativeRoutingType }
) => string {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useCreateHref() may be used only in the context of a <Router> component.`
);

let { basename, navigator } = React.useContext(NavigationContext);
let { hash, pathname, search } = useResolvedPath(to, { relative });
let resolvePath = useResolvePath();

let joinedPathname = pathname;
return React.useCallback(
(to, options) => {
let { hash, pathname, search } = resolvePath(to, options);

// If we're operating within a basename, prepend it to the pathname prior
// to creating the href. If this is a root navigation, then just use the raw
// basename which allows the basename to have full control over the presence
// of a trailing slash on root links
if (basename !== "/") {
joinedPathname =
pathname === "/" ? basename : joinPaths([basename, pathname]);
}
let joinedPathname = pathname;

return navigator.createHref({ pathname: joinedPathname, search, hash });
// If we're operating within a basename, prepend it to the pathname prior
// to creating the href. If this is a root navigation, then just use the raw
// basename which allows the basename to have full control over the presence
// of a trailing slash on root links
if (basename !== "/") {
joinedPathname =
pathname === "/" ? basename : joinPaths([basename, pathname]);
}

return navigator.createHref({ pathname: joinedPathname, search, hash });
},
[resolvePath, basename, navigator]
);
}

/**
Expand Down Expand Up @@ -272,22 +304,39 @@ export function useResolvedPath(
to: To,
{ relative }: { relative?: RelativeRoutingType } = {}
): Path {
let resolvePath = useResolvePath();

return React.useMemo(
() => resolvePath(to, { relative }),
[to, relative, resolvePath]
);
}

/**
* Returns a function that resolves the pathname of the given `to` value against the current location.
*
* @see https://reactrouter.com/docs/en/v6/hooks/use-resolve-path
*/
export function useResolvePath(): (
to: To,
options?: { relative?: RelativeRoutingType }
) => Path {
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();

let routePathnamesJson = JSON.stringify(
getPathContributingMatches(matches).map((match) => match.pathnameBase)
);

return React.useMemo(
() =>
return React.useCallback(
(to, { relative } = {}) =>
resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname,
relative === "path"
),
[to, routePathnamesJson, locationPathname, relative]
[routePathnamesJson, locationPathname]
);
}

Expand Down