Skip to content

Commit 7077b2e

Browse files
committed
Round of performance and size optimisations.
1 parent e3178f3 commit 7077b2e

13 files changed

Lines changed: 272 additions & 140 deletions

bun.lock

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wouter-preact/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"preact": "^10.0.0"
6767
},
6868
"dependencies": {
69-
"mitt": "^3.0.1",
7069
"regexparam": "^3.0.0"
7170
},
7271
"devDependencies": {

packages/wouter/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"react": ">=16.8.0"
6767
},
6868
"dependencies": {
69-
"mitt": "^3.0.1",
7069
"regexparam": "^3.0.0",
7170
"use-sync-external-store": "^1.0.0"
7271
}

packages/wouter/src/index.js

Lines changed: 55 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ export const useParams = () => useContext(ParamsCtx);
6363
* Part 1, Hooks API: useRoute and useLocation
6464
*/
6565

66-
// Internal version of useLocation to avoid redundant useRouter calls
66+
// Internal location hooks avoid redundant context reads and navigation callbacks.
67+
68+
const usePathnameFromRouter = (router) =>
69+
relativePath(router.base, router.hook(router)[0]);
6770

6871
const useLocationFromRouter = (router) => {
6972
const [location, navigate] = router.hook(router);
@@ -89,53 +92,31 @@ export const useSearch = () => {
8992
export const matchRoute = (parser, route, path, loose) => {
9093
// if the input is a regexp, skip parsing
9194
const { pattern, keys } =
92-
route instanceof RegExp
93-
? { keys: false, pattern: route }
94-
: parser(route || "*", loose);
95-
96-
// array destructuring loses keys, so this is done in two steps
97-
const result = pattern.exec(path) || [];
98-
99-
// when parser is in "loose" mode, `$base` is equal to the
100-
// first part of the route that matches the pattern
101-
// (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)
102-
// we use this for route nesting
103-
const [$base, ...matches] = result;
104-
105-
return $base !== undefined
106-
? [
107-
true,
108-
109-
(() => {
110-
// for regex paths, `keys` will always be false
111-
112-
// an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
113-
// we "zip" two arrays here to construct the object
114-
// ["foo"], ["bar"] → { foo: "bar" }
115-
const groups =
116-
keys !== false
117-
? Object.fromEntries(keys.map((key, i) => [key, matches[i]]))
118-
: result.groups;
119-
120-
// convert the array to an instance of object
121-
// this makes it easier to integrate with the existing param implementation
122-
let obj = { ...matches };
123-
124-
// merge named capture groups with matches array
125-
groups && Object.assign(obj, groups);
126-
127-
return obj;
128-
})(),
129-
130-
// the third value if only present when parser is in "loose" mode,
131-
// so that we can extract the base path for nested routes
132-
...(loose ? [$base] : []),
133-
]
134-
: [false, null];
95+
route instanceof RegExp ? { pattern: route } : parser(route || "*", loose);
96+
97+
const result = pattern.exec(path);
98+
99+
if (!result) return [false, null];
100+
101+
// Keep positional captures as well as named params, with named params taking
102+
// precedence (custom parsers can use numeric keys).
103+
const params = {};
104+
for (let i = 1; i < result.length; i++) params[i - 1] = result[i];
105+
if (keys) {
106+
for (let i = 0; i < keys.length; i++) params[keys[i]] = result[i + 1];
107+
} else {
108+
Object.assign(params, result.groups);
109+
}
110+
111+
// In loose mode the full match is the base for nested routes:
112+
// pattern `/a/:b` and path `/a/1/2/3` give the base `/a/1`.
113+
return loose ? [true, params, result[0]] : [true, params];
135114
};
136115

137-
export const useRoute = (pattern) =>
138-
matchRoute(useRouter().parser, pattern, useLocation()[0]);
116+
export const useRoute = (pattern) => {
117+
const router = useRouter();
118+
return matchRoute(router.parser, pattern, usePathnameFromRouter(router));
119+
};
139120

140121
/*
141122
* Part 2, Low Carb Router API: Router, Route, Link, Switch
@@ -154,9 +135,10 @@ export const Router = ({ children, ...props }) => {
154135
// also, ensure ssrSearch is always defined when ssrPath is provided, so that
155136
// useSearch behavior matches usePathname (proper SSR hydration when client
156137
// renders <Router> without props after server rendered with ssrPath/ssrSearch)
157-
const [path, search = props.ssrSearch ?? ""] =
158-
props.ssrPath?.split("?") ?? [];
159-
if (path) (props.ssrSearch = search), (props.ssrPath = path);
138+
if (props.ssrPath) {
139+
const [path, search = props.ssrSearch ?? ""] = props.ssrPath.split("?");
140+
if (path) (props.ssrSearch = search), (props.ssrPath = path);
141+
}
160142

161143
// hooks can define their own `href` formatter (e.g. for hash location)
162144
props.hrefs = props.hrefs ?? props.hook?.hrefs;
@@ -174,7 +156,7 @@ export const Router = ({ children, ...props }) => {
174156
// 2) if the custom `hook` prop is provided, we always inherit from the
175157
// default router instead. this resets all previously overridden options.
176158
// 3) when the router is customized here, it should stay stable between renders
177-
let ref = useRef({}),
159+
let ref = useRef(parent),
178160
prev = ref.current,
179161
next = prev;
180162

@@ -185,17 +167,16 @@ export const Router = ({ children, ...props }) => {
185167
parent[k] + (props[k] ?? "")
186168
: props[k] ?? parent[k];
187169

188-
if (prev === next && option !== next[k]) {
189-
ref.current = next = { ...next };
170+
if (option !== next[k]) {
171+
if (prev === next) ref.current = next = { ...next };
172+
next[k] = option;
190173
}
191174

192-
next[k] = option;
193-
194175
// the new router is no different from the parent or from the memoized value, use parent
195176
if (option !== parent[k] || option !== value[k]) value = next;
196177
}
197178

198-
return h(RouterCtx.Provider, { value, children });
179+
return h(RouterCtx.Provider, { value }, children);
199180
};
200181

201182
const h_route = ({ children, component }, params) => {
@@ -209,11 +190,12 @@ const h_route = ({ children, component }, params) => {
209190
// Cache params object between renders if values are shallow equal
210191
const useCachedParams = (value) => {
211192
let prev = useRef(Params0);
212-
const curr = prev.current;
193+
const curr = prev.current,
194+
keys = Object.keys(value);
213195
return (prev.current =
214196
// Update cache if number of params changed or any value changed
215-
Object.keys(value).length !== Object.keys(curr).length ||
216-
Object.entries(value).some(([k, v]) => v !== curr[k])
197+
keys.length !== Object.keys(curr).length ||
198+
keys.some((k) => value[k] !== curr[k])
217199
? value // Return new value if there are changes
218200
: curr); // Return cached value if nothing changed
219201
};
@@ -240,7 +222,7 @@ export function useSearchParams() {
240222

241223
export const Route = ({ path, nest, match, ...renderProps }) => {
242224
const router = useRouter();
243-
const [location] = useLocationFromRouter(router);
225+
const location = usePathnameFromRouter(router);
244226

245227
const [matches, routeParams, base] =
246228
// `match` is a special prop to give up control to the parent,
@@ -254,11 +236,13 @@ export const Route = ({ path, nest, match, ...renderProps }) => {
254236

255237
if (!matches) return null;
256238

257-
const children = base
258-
? h(Router, { base }, h_route(renderProps, params))
259-
: h_route(renderProps, params);
239+
const children = h_route(renderProps, params);
260240

261-
return h(ParamsCtx.Provider, { value: params, children });
241+
return h(
242+
ParamsCtx.Provider,
243+
{ value: params },
244+
base ? h(Router, { base }, children) : children
245+
);
262246
};
263247

264248
export const Link = forwardRef((props, ref) => {
@@ -319,16 +303,17 @@ export const Link = forwardRef((props, ref) => {
319303
});
320304
});
321305

322-
const flattenChildren = (children) =>
323-
Array.isArray(children)
324-
? children.flatMap((c) =>
325-
flattenChildren(c && c.type === Fragment ? c.props.children : c)
326-
)
327-
: [children];
306+
const flattenChildren = (children, result = []) => {
307+
if (Array.isArray(children)) {
308+
for (const c of children)
309+
flattenChildren(c && c.type === Fragment ? c.props.children : c, result);
310+
} else result.push(children);
311+
return result;
312+
};
328313

329314
export const Switch = ({ children, location }) => {
330315
const router = useRouter();
331-
const [originalLocation] = useLocationFromRouter(router);
316+
const originalLocation = usePathnameFromRouter(router);
332317

333318
for (const element of flattenChildren(children)) {
334319
let match = 0;
Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import mitt from "mitt";
21
import { useSyncExternalStore } from "./react-deps.js";
32

43
/**
@@ -8,12 +7,11 @@ import { useSyncExternalStore } from "./react-deps.js";
87
export const memoryLocation = ({
98
path = "/",
109
searchPath = "",
11-
state = null,
10+
state: initialState = null,
1211
static: staticLocation,
1312
record,
1413
} = {}) => {
1514
let initialPath = path;
16-
const initialState = state;
1715
if (searchPath) {
1816
// join with & if path contains search query, and ? otherwise
1917
initialPath += path.split("?")[1] ? "&" : "?";
@@ -23,58 +21,56 @@ export const memoryLocation = ({
2321
let [currentPath, currentSearch = ""] = initialPath.split("?");
2422
let currentState = initialState;
2523
const history = [initialPath];
26-
const emitter = mitt();
24+
let listeners = [];
2725

2826
const navigateImplementation = (path, { replace = false, state } = {}) => {
29-
if (record) {
30-
if (replace) {
31-
history.splice(history.length - 1, 1, path);
32-
} else {
33-
history.push(path);
34-
}
35-
}
27+
if (record)
28+
history[replace && history.length ? history.length - 1 : history.length] =
29+
path;
3630

3731
[currentPath, currentSearch = ""] = path.split("?");
3832
if (state !== undefined) currentState = state;
39-
emitter.emit("navigate", path);
33+
listeners.forEach((cb) => cb());
4034
};
4135

4236
const navigate = !staticLocation ? navigateImplementation : () => null;
4337

38+
// Copy on subscription changes instead of copying on every navigation.
4439
const subscribe = (cb) => {
45-
emitter.on("navigate", cb);
46-
return () => emitter.off("navigate", cb);
40+
listeners = [...listeners, cb];
41+
return () => {
42+
listeners = listeners.filter((i) => i !== cb);
43+
};
4744
};
4845

46+
const getPath = () => currentPath;
47+
const getSearch = () => currentSearch;
48+
4949
const useMemoryLocation = () => [
50-
useSyncExternalStore(subscribe, () => currentPath),
50+
useSyncExternalStore(subscribe, getPath),
5151
navigate,
5252
];
5353

54-
const useMemoryQuery = () =>
55-
useSyncExternalStore(subscribe, () => currentSearch);
54+
const useMemoryQuery = () => useSyncExternalStore(subscribe, getSearch);
5655

5756
// Attach searchHook to the location hook for auto-inheritance in Router
5857
useMemoryLocation.searchHook = useMemoryQuery;
5958

6059
function reset() {
6160
// clean history array with mutation to preserve link
62-
history.splice(0, history.length);
61+
history.length = 0;
6362
navigateImplementation(initialPath, { state: initialState });
6463
}
6564

66-
const memoryLocationResult = {
67-
hook: useMemoryLocation,
68-
searchHook: useMemoryQuery,
69-
navigate,
70-
history: record ? history : undefined,
71-
reset: record ? reset : undefined,
72-
};
73-
74-
Object.defineProperty(memoryLocationResult, "state", {
75-
enumerable: true,
76-
get: () => currentState,
77-
});
78-
79-
return memoryLocationResult;
65+
return Object.defineProperty(
66+
{
67+
hook: useMemoryLocation,
68+
searchHook: useMemoryQuery,
69+
navigate,
70+
history: record ? history : undefined,
71+
reset: record ? reset : undefined,
72+
},
73+
"state",
74+
{ enumerable: true, get: () => currentState }
75+
);
8076
};

0 commit comments

Comments
 (0)