Skip to content

Commit

Permalink
feat: add new 'useCreateHref' and 'useResolvePath' hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
jonkoops committed Dec 6, 2022
1 parent 80bd977 commit 373bc44
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .changeset/serious-tips-wait.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 373bc44

Please sign in to comment.