Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: molefrog/wouter
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.8.1
Choose a base ref
...
head repository: molefrog/wouter
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.9.0
Choose a head ref
  • 16 commits
  • 11 files changed
  • 3 contributors

Commits on Nov 15, 2022

  1. Copy the full SHA
    c99c47a View commit details
  2. Copy the full SHA
    23ca778 View commit details
  3. Copy the full SHA
    9876835 View commit details
  4. Copy the full SHA
    d15bd99 View commit details
  5. Copy the full SHA
    0fc6cdb View commit details
  6. Copy the full SHA
    cc089a4 View commit details
  7. Copy the full SHA
    0a9e9c6 View commit details
  8. Copy the full SHA
    3e8df6b View commit details
  9. Copy the full SHA
    37b62ad View commit details
  10. Use nested prop in tests.

    molefrog committed Nov 15, 2022
    Copy the full SHA
    3166302 View commit details
  11. Copy the full SHA
    74c2b3b View commit details

Commits on Nov 16, 2022

  1. Support base path with the static location hook (#268)

    * Support base path with the static location hook
    
    - Closes #266.
    
    * Fix format conflict
    
    * Fix history
    
    * Move the base param to the hook
    
    * Remove unrelated comment
    
    * Fix format conflict
    
    * fc
    
    * Fix option type
    
    * Support absolute paths
    
    * Make the options optional
    
    * Remove undefined ref
    
    * Add a test
    
    * fmt
    rojvv authored Nov 16, 2022
    Copy the full SHA
    8ccbb33 View commit details
  2. Update README.md

    rojvv authored and molefrog committed Nov 16, 2022
    Copy the full SHA
    49d60d7 View commit details

Commits on Nov 18, 2022

  1. Copy the full SHA
    f9962d9 View commit details
  2. Refactor currentPathname into relativePath.

    Also, move it back inside use-location.js to avoid modifying Preact
    build step.
    molefrog committed Nov 18, 2022
    Copy the full SHA
    63e40a5 View commit details
  3. Release v2.9.0

    molefrog committed Nov 18, 2022
    Copy the full SHA
    5533ed0 View commit details
Showing with 165 additions and 64 deletions.
  1. +20 −18 README.md
  2. +38 −25 index.js
  3. +1 −1 package.json
  4. +1 −1 preact/package.json
  5. +1 −1 preact/react-deps.js
  6. +18 −1 react-deps.js
  7. +15 −9 static-location.js
  8. +16 −1 test/ssr.test.js
  9. +9 −0 test/static-location.test.js
  10. +35 −0 test/use-router.test.js
  11. +11 −7 use-location.js
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -221,30 +221,32 @@ customize it in a `<Router />` component.
As an exercise, let's implement a simple location hook that listens to hash changes:

```js
import { useState, useEffect } from "react";
import { useState, useEffect, useSyncExternalStore } from "react";
import { Router, Route } from "wouter";

// returns the current hash location in a normalized form
// (excluding the leading '#' symbol)
const currentLocation = () => {
return window.location.hash.replace(/^#/, "") || "/";
};
const currentLocation = () => window.location.hash.replace(/^#/, "") || "/";

const navigate = (to) => (window.location.hash = to);
const navigate = (to) => {
window.location.hash = to;
};

const useHashLocation = () => {
const [loc, setLoc] = useState(currentLocation());

useEffect(() => {
// this function is called whenever the hash changes
const handler = () => setLoc(currentLocation());

// subscribe to hash changes
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);

return [loc, navigate];
// `useSyncExternalStore` is available in React 18, or you can use a shim for older versions
const location = useSyncExternalStore(
// first argument is a value subscriber: it gives us a callback that we should call
// whenever the value is changed
(onChange) => {
window.addEventListener("hashchange", onChange);
return () => window.removeEventListener("hashchange", onChange);
},

// the second argument is function to get the current value
() => currentLocation()
);

return [location, navigate];
};

const App = () => (
@@ -532,7 +534,7 @@ const App = () => (
);
```

**Note:** _the base path feature is only supported by the default `pushState` hook. If you're
**Note:** _the base path feature is only supported by the default `pushState` and `staticLocation` hooks. If you're
implementing your own location hook, you'll need to add base path support yourself._

### How do I make a default route?
63 changes: 38 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import locationHook from "./use-location.js";
import makeMatcher from "./matcher.js";
import matcherWithCache from "./matcher.js";

import {
useRef,
useLayoutEffect,
useContext,
useCallback,
createContext,
@@ -13,31 +12,31 @@ import {
Fragment,
useState,
forwardRef,
useIsomorphicLayoutEffect,
} from "./react-deps.js";

/*
* Part 1, Hooks API: useRouter, useRoute and useLocation
* Router and router context. Router is a lightweight object that represents the current
* routing options: how location is managed, base path etc.
*
* There is a default router present for most of the use cases, however it can be overriden
* via the <Router /> component.
*/

// one of the coolest features of `createContext`:
// when no value is provided — default object is used.
// allows us to use the router context as a global ref to store
// the implicitly created router (see `useRouter` below)
const RouterCtx = createContext({});
const defaultRouter = {
hook: locationHook,
matcher: matcherWithCache(),
base: "",
};

const buildRouter = ({
hook = locationHook,
base = "",
matcher = makeMatcher(),
} = {}) => ({ hook, base, matcher });
const RouterCtx = createContext(defaultRouter);

export const useRouter = () => {
const globalRef = useContext(RouterCtx);
// gets the closes parent router from the context
export const useRouter = () => useContext(RouterCtx);

// either obtain the router from the outer context (provided by the
// `<Router /> component) or create an implicit one on demand.
return globalRef.v || (globalRef.v = buildRouter());
};
/*
* Part 1, Hooks API: useRoute and useLocation
*/

export const useLocation = () => {
const router = useRouter();
@@ -62,15 +61,29 @@ const useNavigate = (options) => {
* Part 2, Low Carb Router API: Router, Route, Link, Switch
*/

export const Router = (props) => {
// this little trick allows to avoid having unnecessary
// calls to potentially expensive `buildRouter` method.
export const Router = ({ hook, matcher, base = "", parent, children }) => {
// updates the current router with the props passed down to the component
const updateRouter = (router, proto = parent || defaultRouter) => {
router.hook = hook || proto.hook;
router.matcher = matcher || proto.matcher;
router.base = proto.base + base;

// store reference to parent router
router.parent = parent;

return router;
};

// we use `useState` here, but it only catches the first render and never changes.
// https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
const [value] = useState(() => ({ v: buildRouter(props) }));
const [value] = useState(() => updateRouter({})); // create the object once...
useIsomorphicLayoutEffect(() => {
updateRouter(value);
}); // ...then update it on each render

return h(RouterCtx.Provider, {
value,
children: props.children,
children,
});
};

@@ -171,7 +184,7 @@ export const Redirect = (props) => {
const navRef = useNavigate(props);

// empty array means running the effect once, navRef is a ref so it never changes
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
navRef.current();
}, []); // eslint-disable-line react-hooks/exhaustive-deps

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wouter",
"version": "2.8.1",
"version": "2.9.0",
"description": "A minimalistic routing for React and Preact. Nothing extra, just HOOKS.",
"keywords": [
"react",
2 changes: 1 addition & 1 deletion preact/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wouter-preact",
"version": "2.8.1",
"version": "2.9.0",
"description": "A minimalistic routing for React and Preact (Preact-only version).",
"type": "module",
"files": [
2 changes: 1 addition & 1 deletion preact/react-deps.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export {
export {
useRef,
useEffect,
useLayoutEffect,
useLayoutEffect as useIsomorphicLayoutEffect,
useState,
useContext,
useCallback,
19 changes: 18 additions & 1 deletion react-deps.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useLayoutEffect } from "react";

export {
useRef,
useEffect,
useLayoutEffect,
useState,
useContext,
useCallback,
@@ -12,3 +13,19 @@ export {
Fragment,
forwardRef,
} from "react";

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.

// See Redux's source code for reference:
// https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts
export const canUseDOM = !!(
typeof window !== "undefined" &&
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined"
);

export const useIsomorphicLayoutEffect = canUseDOM
? useLayoutEffect
: useEffect;
24 changes: 15 additions & 9 deletions static-location.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { relativePath } from "./use-location";

// Generates static `useLocation` hook. The hook always
// responds with initial path provided.
// You can use this for server-side rendering.
export default (path = "/", { record = false } = {}) => {
let hook;
const navigate = (to, { replace } = {}) => {
if (record) {
if (replace) {
hook.history.pop();
const hook = ({ base = "" } = {}) => [
relativePath(base, path),
(to, { replace } = {}) => {
if (record) {
if (replace) {
hook.history.pop();
}
hook.history.push(
// handle nested routers and absolute paths
to[0] === "~" ? to.slice(1) : base + to
);
}
hook.history.push(to);
}
};
hook = () => [path, navigate];
},
];
hook.history = [path];
return hook;
};
17 changes: 16 additions & 1 deletion test/ssr.test.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";

import { Route, Router, useRoute, Link } from "../index";
import { Route, Router, useRoute, Link, Redirect } from "../index";
import staticLocationHook from "../static-location.js";

describe("server-side rendering", () => {
@@ -51,4 +51,19 @@ describe("server-side rendering", () => {
const rendered = renderToStaticMarkup(<App />);
expect(rendered).toBe(`<a href="/users/1" title="Profile">Mark</a>`);
});

it("renders redirects however they have effect only on a client-side", () => {
const App = () => (
<Router hook={staticLocationHook("/")}>
<Route path="/">
<Redirect to="/foo" />
</Route>

<Route path="/foo">You won't see that in SSR page</Route>
</Router>
);

const rendered = renderToStaticMarkup(<App />);
expect(rendered).toBe("");
});
});
9 changes: 9 additions & 0 deletions test/static-location.test.js
Original file line number Diff line number Diff line change
@@ -62,3 +62,12 @@ it("respects the 'replace' option", () => {
expect(hook.history).toEqual(["/page2"]);
expect(result.current[0]).toBe("/page1");
});

it("supports a basepath", () => {
const hook = staticLocation("/app", { record: true });
const { result } = renderHook(() => hook({ base: "/app" }));
const [, update] = result.current;

act(() => update("/dashboard"));
expect(hook.history).toEqual(["/app", "/app/dashboard"]);
});
35 changes: 35 additions & 0 deletions test/use-router.test.js
Original file line number Diff line number Diff line change
@@ -59,3 +59,38 @@ it("shares one router instance between components", () => {
];
expect(uniqRouters.length).toBe(1);
});

it("inherits base path from the parent router when parent router is provided", () => {
const NestedRouter = (props) => {
const parent = useRouter();
return <Router {...props} parent={props.nested ? parent : undefined} />;
};

const { result } = renderHook(() => useRouter(), {
wrapper: (props) => (
<Router base="/app">
<NestedRouter base="/users" nested>
{props.children}
</NestedRouter>
</Router>
),
});

const router = result.current;
expect(router.parent.base).toBe("/app");
expect(router.base).toBe("/app/users");
});

it("does not inherit base path by default", () => {
const { result } = renderHook(() => useRouter(), {
wrapper: (props) => (
<Router base="/app">
<Router base="/users">{props.children}</Router>
</Router>
),
});

const router = result.current;
expect(router.base).toBe("/users");
expect(router.parent).toBe(undefined);
});
18 changes: 11 additions & 7 deletions use-location.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { useEffect, useRef, useState, useCallback } from "./react-deps.js";

/*
* Transforms `path` into its relative `base` version
* If base isn't part of the path provided returns absolute path e.g. `~/app`
*/
export const relativePath = (base, path = location.pathname) =>
!path.toLowerCase().indexOf(base.toLowerCase())
? path.slice(base.length) || "/"
: "~" + path;

/**
* History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
*/
@@ -9,7 +18,7 @@ const eventReplaceState = "replaceState";
export const events = [eventPopstate, eventPushState, eventReplaceState];

export default ({ base = "" } = {}) => {
const [{ path }, update] = useState(() => ({ path: currentPathname(base) }));
const [{ path }, update] = useState(() => ({ path: relativePath(base) }));
// @see https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const prevHash = useRef(path + location.search);

@@ -19,7 +28,7 @@ export default ({ base = "" } = {}) => {
// unfortunately, we can't rely on `path` value here, since it can be stale,
// that's why we store the last pathname in a ref.
const checkForUpdates = () => {
const pathname = currentPathname(base);
const pathname = relativePath(base);
const hash = pathname + location.search;

if (prevHash.current !== hash) {
@@ -76,8 +85,3 @@ if (typeof history !== "undefined") {
};
}
}

const currentPathname = (base, path = location.pathname) =>
!path.toLowerCase().indexOf(base.toLowerCase())
? path.slice(base.length) || "/"
: "~" + path;