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

v3: move absolute and relative paths to core functionality #362

Merged
merged 10 commits into from
Oct 16, 2023
Merged
14 changes: 13 additions & 1 deletion packages/wouter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useIsomorphicLayoutEffect,
useEvent,
} from "./react-deps.js";
import { absolutePath, relativePath } from "./paths.js";

/*
* Router and router context. Router is a lightweight object that represents the current
Expand Down Expand Up @@ -41,7 +42,18 @@ export const useRouter = () => useContext(RouterCtx);
*/

// Internal version of useLocation to avoid redundant useRouter calls
const useLocationFromRouter = (router) => router.hook(router);

const useLocationFromRouter = (router) => {
const [location, navigate] = router.hook(router);

// the function reference should stay the same between re-renders, so that
// it can be passed down as an element prop without any performance concerns.
// (This is achieved via `useEvent`.)
return [
relativePath(router.base, location),
useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)),
];
};

export const useLocation = () => useLocationFromRouter(useRouter());

Expand Down
12 changes: 3 additions & 9 deletions packages/wouter/src/memory-location.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import mitt from "mitt";
import { useSyncExternalStore, useEvent } from "./react-deps.js";
import { absolutePath, relativePath } from "./paths.js";
import { useSyncExternalStore } from "./react-deps.js";

/**
* In-memory location that supports navigation
Expand Down Expand Up @@ -35,13 +34,8 @@ export const memoryLocation = ({
return () => emitter.off("navigate", cb);
};

const useMemoryLocation = ({ base } = {}) => {
const location = useSyncExternalStore(subscribe, () => currentPath);

return [
relativePath(base, location),
useEvent((to, options) => navigate(absolutePath(to, base), options)),
];
const useMemoryLocation = () => {
return [useSyncExternalStore(subscribe, () => currentPath), navigate];
};
jeetiss marked this conversation as resolved.
Show resolved Hide resolved

return {
Expand Down
12 changes: 2 additions & 10 deletions packages/wouter/src/use-browser-location.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useSyncExternalStore, useEvent } from "./react-deps.js";
import { absolutePath, relativePath } from "./paths.js";
import { useSyncExternalStore } from "./react-deps.js";

/**
* History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
Expand Down Expand Up @@ -51,14 +50,7 @@ export const navigate = (to, { replace = false, state = null } = {}) =>

// the 2nd argument of the `useBrowserLocation` return value is a function
// that allows to perform a navigation.
//
// the function reference should stay the same between re-renders, so that
// it can be passed down as an element prop without any performance concerns.
// (This is achieved via `useEvent`.)
export const useBrowserLocation = (opts = {}) => [
relativePath(opts.base, usePathname(opts)),
useEvent((to, navOpts) => navigate(absolutePath(to, opts.base), navOpts)),
];
export const useBrowserLocation = (opts = {}) => [usePathname(opts), navigate];

const patchKey = Symbol.for("wouter_v3");

Expand Down
19 changes: 7 additions & 12 deletions packages/wouter/src/use-hash-location.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useSyncExternalStore, useEvent } from "./react-deps.js";
import { absolutePath, relativePath } from "./paths.js";
import { useSyncExternalStore } from "./react-deps.js";

// fortunately `hashchange` is a native event, so there is no need to
// patch `history` object (unlike `pushState/replaceState` events)
Expand All @@ -21,15 +20,11 @@ export const navigate = (to, { state = null } = {}) => {
);
};

export const useHashLocation = ({ base, ssrPath = "/" } = {}) => [
relativePath(
base,
useSyncExternalStore(
subscribeToHashUpdates,
currentHashLocation,
() => ssrPath
)
export const useHashLocation = ({ ssrPath = "/" } = {}) => [
useSyncExternalStore(
subscribeToHashUpdates,
currentHashLocation,
() => ssrPath
),

useEvent((to, navOpts) => navigate(absolutePath(to, base), navOpts)),
navigate,
];
30 changes: 0 additions & 30 deletions packages/wouter/test/memory-location.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,6 @@ it('should return location hook that has initial path "/" by default', () => {
unmount();
});

it("should return location hook that supports `base` option for nested routing", () => {
const { hook } = memoryLocation({ path: "/nested/test" });

const { result, unmount } = renderHook(() => hook({ base: "/nested" }));
const [value] = result.current;

expect(value).toBe("/test");
unmount();
});

it("should return location hook that handle `base` option while navigation", () => {
const { hook, history } = memoryLocation({
path: "/nested/test",
record: true,
});

const { result, unmount } = renderHook(() => hook({ base: "/nested" }));

act(() => result.current[1]("/change-1"));
act(() => result.current[1]("/change-2"));

expect(history).toStrictEqual([
"/nested/test",
"/nested/change-1",
"/nested/change-2",
]);

unmount();
});

it("should return standalone `navigate` method", () => {
const { hook, navigate } = memoryLocation();

Expand Down
25 changes: 25 additions & 0 deletions packages/wouter/test/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Executes a callback and returns a promise that resolve when `hashchange` event is fired.
* Rejects after `throwAfter` milliseconds.
*/
export const waitForHashChangeEvent = async (
cb: () => void,
throwAfter = 1000
) =>
new Promise<void>((resolve, reject) => {
let timeout: ReturnType<typeof setTimeout>;

const onChange = () => {
resolve();
clearTimeout(timeout);
window.removeEventListener("hashchange", onChange);
};

window.addEventListener("hashchange", onChange);
cb();

timeout = setTimeout(() => {
reject(new Error("Timed out: `hashchange` event did not fire!"));
window.removeEventListener("hashchange", onChange);
}, throwAfter);
});
4 changes: 2 additions & 2 deletions packages/wouter/test/use-browser-location.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ describe("useBrowserLocation", () => {
assertType(navigate("/path", { unknownOption: true }));
});

it("should support base option", () => {
assertType(useBrowserLocation({ base: "/something" }));
it("should support `ssrPath` option", () => {
assertType(useBrowserLocation({ ssrPath: "/something" }));
// @ts-expect-error
assertType(useBrowserLocation({ foo: "bar" }));
});
Expand Down
51 changes: 0 additions & 51 deletions packages/wouter/test/use-browser-location.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,46 +51,6 @@ describe("`value` first argument", () => {
unmount();
});

it("returns a pathname without a basepath", () => {
const { result, unmount } = renderHook(() =>
useBrowserLocation({ base: "/app" })
);

act(() => history.pushState(null, "", "/app/dashboard"));
expect(result.current[0]).toBe("/dashboard");
unmount();
});

it("returns `/` when URL contains only a basepath", () => {
const { result, unmount } = renderHook(() =>
useBrowserLocation({ base: "/app" })
);

act(() => history.pushState(null, "", "/app"));
expect(result.current[0]).toBe("/");
unmount();
});

it("basepath should be case-insensitive", () => {
const { result, unmount } = renderHook(() =>
useBrowserLocation({ base: "/MyApp" })
);

act(() => history.pushState(null, "", "/myAPP/users/JohnDoe"));
expect(result.current[0]).toBe("/users/JohnDoe");
unmount();
});

it("returns an absolute path in case of unmatched base path", () => {
const { result, unmount } = renderHook(() =>
useBrowserLocation({ base: "/MyApp" })
);

act(() => history.pushState(null, "", "/MyOtherApp/users/JohnDoe"));
expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe");
unmount();
});

it("supports search url", () => {
// count how many times each hook is rendered
const locationRenders = { current: 0 };
Expand Down Expand Up @@ -215,15 +175,4 @@ describe("`update` second parameter", () => {
expect(updateWas).toBe(updateNow);
unmount();
});

it("supports a basepath", () => {
const { result, unmount } = renderHook(() =>
useBrowserLocation({ base: "/app" })
);
const update = result.current[1];

act(() => update("/dashboard"));
expect(location.pathname).toBe("/app/dashboard");
unmount();
});
});
6 changes: 3 additions & 3 deletions packages/wouter/test/use-hash-location.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ it("is a location hook", () => {
expectTypeOf(useHashLocation()).toMatchTypeOf<[string, Function]>();
});

it("accepts a `base` path option", () => {
useHashLocation({ base: "/foo" });
useHashLocation({ base: "" });
it("accepts a `ssrPath` path option", () => {
useHashLocation({ ssrPath: "/foo" });
useHashLocation({ ssrPath: "" });

// @ts-expect-error
useHashLocation({ base: 123 });
Expand Down
25 changes: 2 additions & 23 deletions packages/wouter/test/use-hash-location.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { renderToStaticMarkup } from "react-dom/server";

import { useHashLocation } from "wouter/use-hash-location";

import { waitForHashChangeEvent } from "./test-utils";

beforeEach(() => {
history.replaceState(null, "", "/");
location.hash = "";
Expand Down Expand Up @@ -119,26 +121,3 @@ it("is not sensitive to leading / or # when navigating", async () => {
expect(location.hash).toBe("#/look-ma-no-hashes");
expect(result.current[0]).toBe("/look-ma-no-hashes");
});

/**
* Executes a callback and returns a promise that resolve when `hashchange` event is fired.
* Rejects after `throwAfter` milliseconds.
*/
const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) =>
new Promise<void>((resolve, reject) => {
let timeout: ReturnType<typeof setTimeout>;

const onChange = () => {
resolve();
clearTimeout(timeout);
window.removeEventListener("hashchange", onChange);
};

window.addEventListener("hashchange", onChange);
cb();

timeout = setTimeout(() => {
reject(new Error("Timed out: `hashchange` event did not fire!"));
window.removeEventListener("hashchange", onChange);
}, throwAfter);
});
Loading
Loading