Skip to content

Commit

Permalink
HashRouter hashType implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Whaileee committed Jul 24, 2024
1 parent 90ebbf9 commit 2ea44cc
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 29 deletions.
6 changes: 6 additions & 0 deletions .changeset/khaki-beers-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-router": patch
"react-router-dom": patch
---

HashRouter hashType implementation for backwards compatibility with project migrating from React-Router v4/v5
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,4 @@
- yuleicul
- zeromask1337
- zheng-chuang
- Whaileee
94 changes: 94 additions & 0 deletions docs/router-components/hash-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: HashRouter
---

# `<HashRouter>`

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

```tsx
declare function HashRouter(
props: HashRouterProps
): React.ReactElement;

interface HashRouterProps {
basename?: string;
hashType?: HashType
children?: React.ReactNode;
future?: FutureConfig;
window?: Window;
}
```

</details>

`<HashRouter>` is for use in web browsers when the URL should not (or cannot) be sent to the server for some reason. This may happen in some shared hosting scenarios where you do not have full control over the server. In these situations, `<HashRouter>` makes it possible to store the current location in the `hash` portion of the current URL, so it is never sent to the server.

```tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom";

ReactDOM.render(
<HashRouter>
{/* The rest of your app goes here */}
</HashRouter>,
root
);
```

<docs-warning>We strongly recommend you do not use `HashRouter` unless you absolutely have to.</docs-warning>

## `basename`

Configure your application to run underneath a specific basename in the URL:

```jsx
function App() {
return (
<HashRouter basename="/app">
<Routes>
<Route path="/" /> {/* 👈 Renders at /#/app/ */}
</Routes>
</HashRouter>
);
}
```

## `hashType`

Decide wether to put a slash after the '#' in the URL (default: 'slash')

```jsx
function App() {
return (
<HashRouter hashType='noslash'>
<Routes>
<Route path="/bookmark" /> {/* 👈 Renders at /#bookmark/ */}
</Routes>
</HashRouter>
);
}
```

## `future`

An optional set of [Future Flags][api-development-strategy] to enable. We recommend opting into newly released future flags sooner rather than later to ease your eventual migration to v7.

```jsx
function App() {
return (
<HashRouter future={{ v7_startTransition: true }}>
<Routes>{/*...*/}</Routes>
</HashRouter>
);
}
```

## `window`

`HashRouter` defaults to using the current [document's `defaultView`][defaultview], but it may also be used to track changes to another window's URL, in an `<iframe>`, for example.

[defaultview]: https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView
[api-development-strategy]: ../guides/api-development-strategy
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,4 @@
"@changesets/assemble-release-plan@5.2.4": "patches/@changesets__assemble-release-plan@5.2.4.patch"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,76 @@ describe("Handles concurrent mode features during navigations", () => {

await assertNavigation(container, resolve, resolveLazy);
});
// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, LazyComponent, resolve, resolveLazy } =
getComponents();

let { container } = render(
<HashRouter
window={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
hashType="noslash"
>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={
<React.Suspense fallback={<p>Loading...</p>}>
<About />
</React.Suspense>
}
/>
<Route
path="/lazy"
element={
<React.Suspense fallback={<p>Loading Lazy Component...</p>}>
<LazyComponent />
</React.Suspense>
}
/>
</Routes>
</HashRouter>
);

await assertNavigation(container, resolve, resolveLazy);
});
// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, LazyComponent, resolve, resolveLazy } =
getComponents();

let { container } = render(
<HashRouter
window={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
hashType="noslash"
>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={
<React.Suspense fallback={<p>Loading...</p>}>
<About />
</React.Suspense>
}
/>
<Route
path="/lazy"
element={
<React.Suspense fallback={<p>Loading Lazy Component...</p>}>
<LazyComponent />
</React.Suspense>
}
/>
</Routes>
</HashRouter>
);

await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("RouterProvider", async () => {
Expand Down Expand Up @@ -281,7 +351,6 @@ describe("Handles concurrent mode features during navigations", () => {
await waitFor(() => screen.getByText("Lazy"));
expect(getHtml(container)).toMatch("Lazy");
}

// eslint-disable-next-line jest/expect-expect
it("MemoryRouter", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
Expand All @@ -300,6 +369,27 @@ describe("Handles concurrent mode features during navigations", () => {
await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
getComponents();

let { container } = render(
<HashRouter
window-={getWindowImpl("/", true)}
hashType="noslash"
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/lazy" element={<LazyComponent />} />
</Routes>
</HashRouter>
);

await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("BrowserRouter", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);

testDomRouter("<DataHashRouterNoSlash>", (routes, opts) => createHashRouter(routes, { ...opts, hashType: 'noslash' }), (url) =>
getWindowImpl(url, true)
);

function testDomRouter(
name: string,
createTestRouter: typeof createBrowserRouter | typeof createHashRouter,
Expand All @@ -33,8 +37,8 @@ function testDomRouter(
let consoleError: jest.SpyInstance;

beforeEach(() => {
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
});

afterEach(() => {
Expand Down
30 changes: 20 additions & 10 deletions packages/react-router/__tests__/dom/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ import {

import getHtml from "../utils/getHtml";
import { createDeferred, tick } from "../router/utils/utils";
function createNoSlashRouter(routes, opts?) {
return opts === undefined ? createHashRouter(routes, { hashType: 'noslash' }) : createHashRouter(routes, { ...opts, hashType: 'noslash' })
}

testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);
testDomRouter("<DataBrowserRouter>", createBrowserRouter, (url) =>
getWindowImpl(url, false)
);

testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);
testDomRouter("<DataHashRouterNoSlash>", createNoSlashRouter, (url) =>
getWindowImpl(url.substring(1), true))

function testDomRouter(
name: string,
Expand All @@ -61,6 +66,9 @@ function testDomRouter(
) {
if (name === "<DataHashRouter>") {
expect(testWindow.location.hash).toEqual("#" + pathname + (search || ""));
}
else if (name === "<DataHashRouterNoSlash>") {
expect(testWindow.location.hash).toEqual("#" + pathname.slice(1).concat() + (search || ""));
} else {
expect(testWindow.location.pathname).toEqual(pathname);
if (search) {
Expand All @@ -74,8 +82,8 @@ function testDomRouter(
let consoleError: jest.SpyInstance;

beforeEach(() => {
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
});

afterEach(() => {
Expand All @@ -89,7 +97,6 @@ function testDomRouter(
createRoutesFromElements(<Route path="/" element={<h1>Home</h1>} />)
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Expand Down Expand Up @@ -4744,6 +4751,9 @@ function testDomRouter(

// Resolve Comp2 loader and complete navigation
navDfd.resolve("nav data");
// On slower machines test could find updated `/2.*idle/` but `["idle"]` hasn't updated yet. This is purely performance issue.
// Sometimes closing all apps and letting test run caused test to pass after many failures
await waitFor(() => screen.getByText(/\[\]/), { timeout: 500 });
await waitFor(() => screen.getByText(/2.*idle/));
expect(getHtml(container.querySelector("#output")!))
.toMatchInlineSnapshot(`
Expand Down Expand Up @@ -5485,9 +5495,9 @@ function testDomRouter(
expect(container.innerHTML).not.toMatch(/my-key/);
fireEvent.click(screen.getByText("Load fetchers"));
await waitFor(() =>
// React `useId()` results in either `:r28:` or `:rp:` depending on
// `DataBrowserRouter`/`DataHashRouter`
expect(container.innerHTML).toMatch(/(:r28:|:rp:),my-key/)
// React `useId()` results in either `:r28:` or `:rp:` or `:r3n:` depending on
// `DataBrowserRouter`/`DataHashRouter`/`DataHashRouterNoSlash`
expect(container.innerHTML).toMatch(/(:r28:|:rp:|:r3n:),my-key/)
);
});
});
Expand Down Expand Up @@ -7367,7 +7377,7 @@ function testDomRouter(
ready: Promise.resolve(),
finished: Promise.resolve(),
updateCallbackDone: Promise.resolve(),
skipTransition: () => {},
skipTransition: () => { },
};
});
testWindow.document.startViewTransition = spy;
Expand Down
35 changes: 35 additions & 0 deletions packages/react-router/__tests__/dom/link-href-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,23 @@ describe("<Link> href", () => {
);
});

describe("when using a hash router with noslash", () => {
it("renders proper <a href> for HashRouter", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<HashRouter hashType="noslash">
<Routes>
<Route path="/" element={<Link to="/path?search=value#hash" />} />
</Routes>
</HashRouter>
);
});
expect(renderer.root.findByType("a").props.href).toEqual(
"#path?search=value#hash"
);
});

it("renders proper <a href> for createHashRouter", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
Expand All @@ -906,7 +923,25 @@ describe("<Link> href", () => {
"#/path?search=value#hash"
);
});

it("renders proper <a href> for createHashRouter with noslash", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
let router = createHashRouter([
{
path: "/",
element: <Link to="/path?search=value#hash">Link</Link>,
},
],
{hashType: 'noslash'});
renderer = TestRenderer.create(<RouterProvider router={router} />);
});
expect(renderer.root.findByType("a").props.href).toEqual(
"#path?search=value#hash"
);
});
});
});

test("fails gracefully on invalid `to` values", () => {
let warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe("Partial Hydration Behavior", () => {
describe("createHashRouter", () => {
testPartialHydration(createHashRouter, ReactRouterDom_RouterProvider);
});
describe("createHashRouter with noslash", () => {
testPartialHydration((routes, opts) => createHashRouter(routes, {...opts, hashType:'noslash'}), ReactRouterDom_RouterProvider);
});

describe("createMemoryRouter", () => {
testPartialHydration(createMemoryRouter, ReactRouter_RouterPRovider);
Expand Down
Loading

0 comments on commit 2ea44cc

Please sign in to comment.