Skip to content

Commit 781b472

Browse files
committed
refactor: Begin to merge location & route contexts
1 parent fbc8d38 commit 781b472

File tree

4 files changed

+81
-93
lines changed

4 files changed

+81
-93
lines changed

src/router.d.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type NestedArray<T> = Array<T | NestedArray<T>>;
99

1010
/**
1111
* Check if a URL path matches against a URL path pattern.
12-
*
12+
*
1313
* Warning: This is an internal API exported only for testing purpose. API could change in future.
1414
* @param url - URL path (e.g. /user/12345)
1515
* @param route - URL pattern (e.g. /user/:id)
@@ -38,18 +38,12 @@ export function Router(props: {
3838
interface LocationHook {
3939
url: string;
4040
path: string;
41-
query: Record<string, string>;
41+
pathParams: Record<string, string>;
42+
searchParams: Record<string, string>;
4243
route: (url: string, replace?: boolean) => void;
4344
}
4445
export const useLocation: () => LocationHook;
4546

46-
interface RouteHook {
47-
path: string;
48-
query: Record<string, string>;
49-
params: Record<string, string>;
50-
}
51-
export const useRoute: () => RouteHook;
52-
5347
type RoutableProps =
5448
| { path: string; default?: false; }
5549
| { path?: never; default: true; }

src/router.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { h, createContext, cloneElement, toChildArray } from 'preact';
1+
import { h, Fragment, createContext, cloneElement, toChildArray } from 'preact';
22
import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks';
33

44
/**
@@ -51,12 +51,12 @@ const UPDATE = (state, url) => {
5151
export const exec = (url, route, matches = {}) => {
5252
url = url.split('/').filter(Boolean);
5353
route = (route || '').split('/').filter(Boolean);
54-
if (!matches.params) matches.params = {};
54+
if (!matches.pathParams) matches.pathParams = {};
5555
for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {
56-
let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
56+
let [, m, pathParam, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
5757
val = url[i];
5858
// segment match:
59-
if (!m && param == val) continue;
59+
if (!m && pathParam == val) continue;
6060
// /foo/* match
6161
if (!m && val && flag == '*') {
6262
matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/');
@@ -69,8 +69,8 @@ export const exec = (url, route, matches = {}) => {
6969
if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined;
7070
// normal/optional field:
7171
else if (val) val = decodeURIComponent(val);
72-
matches.params[param] = val;
73-
if (!(param in matches)) matches[param] = val;
72+
matches.pathParams[pathParam] = val;
73+
if (!(pathParam in matches)) matches[pathParam] = val;
7474
if (rest) break;
7575
}
7676
return matches;
@@ -84,19 +84,22 @@ export function LocationProvider(props) {
8484
if (props.scope) scope = props.scope;
8585
const wasPush = push === true;
8686

87+
/** @type {import('./router.d.ts').LocationHook} */
8788
const value = useMemo(() => {
8889
const u = new URL(url, location.origin);
8990
const path = u.pathname.replace(/\/+$/g, '') || '/';
90-
// @ts-ignore-next
91+
9192
return {
9293
url,
9394
path,
94-
query: Object.fromEntries(u.searchParams),
95+
pathParams: {},
96+
searchParams: Object.fromEntries(u.searchParams),
9597
route: (url, replace) => route({ url, replace }),
9698
wasPush
9799
};
98100
}, [url]);
99101

102+
100103
useLayoutEffect(() => {
101104
addEventListener('click', route);
102105
addEventListener('popstate', route);
@@ -107,7 +110,6 @@ export function LocationProvider(props) {
107110
};
108111
}, []);
109112

110-
// @ts-ignore
111113
return h(LocationProvider.ctx.Provider, { value }, props.children);
112114
}
113115

@@ -116,8 +118,7 @@ const RESOLVED = Promise.resolve();
116118
export function Router(props) {
117119
const [c, update] = useReducer(c => c + 1, 0);
118120

119-
const { url, query, wasPush, path } = useLocation();
120-
const { rest = path, params = {} } = useContext(RouteContext);
121+
const { url, path, pathParams, searchParams, wasPush } = useLocation();
121122

122123
const isLoading = useRef(false);
123124
const prevRoute = useRef(path);
@@ -137,7 +138,7 @@ export function Router(props) {
137138

138139
let pathRoute, defaultRoute, matchProps;
139140
toChildArray(props.children).some((/** @type {VNode<any>} */ vnode) => {
140-
const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params, rest: '' }));
141+
const matches = exec(path, vnode.props.path, (matchProps = { ...vnode.props, path, pathParams, searchParams, rest: '' }));
141142
if (matches) return (pathRoute = cloneElement(vnode, matchProps));
142143
if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);
143144
});
@@ -148,7 +149,7 @@ export function Router(props) {
148149
prev.current = cur.current;
149150

150151
// Only mark as an update if the route component changed.
151-
const outgoing = prev.current && prev.current.props.children;
152+
const outgoing = prev.current;
152153
if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {
153154
// This hack prevents Preact from diffing when we swap `cur` to `prev`:
154155
if (this.__v && this.__v.__k) this.__v.__k.reverse();
@@ -161,7 +162,8 @@ export function Router(props) {
161162
const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;
162163
const isHydratingBool = cur.current && cur.current.__h;
163164
// @ts-ignore
164-
cur.current = /** @type {VNode<any>} */ (h(RouteContext.Provider, { value: matchProps }, incoming));
165+
// TODO: Figure out how to set `.__h` properly so that it's preserved for the next render.
166+
cur.current = h(Fragment, {}, incoming);
165167
if (isHydratingSuspense) {
166168
cur.current.__u |= MODE_HYDRATE;
167169
cur.current.__u |= MODE_SUSPENDED;
@@ -263,11 +265,7 @@ Router.Provider = LocationProvider;
263265
LocationProvider.ctx = createContext(
264266
/** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({})
265267
);
266-
const RouteContext = createContext(
267-
/** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({})
268-
);
269268

270269
export const Route = props => h(props.component, props);
271270

272271
export const useLocation = () => useContext(LocationProvider.ctx);
273-
export const useRoute = () => useContext(RouteContext);

test/node/router-match.test.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,46 @@ import * as assert from 'uvu/assert';
44
import { exec } from '../../src/router.js';
55

66
function execPath(path, pattern, opts) {
7-
return exec(path, pattern, { path, query: {}, params: {}, ...(opts || {}) });
7+
return exec(path, pattern, { path, searchParams: {}, pathParams: {}, ...(opts || {}) });
88
}
99

1010
test('Base route', () => {
1111
const accurateResult = execPath('/', '/');
12-
assert.equal(accurateResult, { path: '/', params: {}, query: {} });
12+
assert.equal(accurateResult, { path: '/', pathParams: {}, searchParams: {} });
1313

1414
const inaccurateResult = execPath('/user/1', '/');
1515
assert.equal(inaccurateResult, undefined);
1616
});
1717

1818
test('Param route', () => {
1919
const accurateResult = execPath('/user/2', '/user/:id');
20-
assert.equal(accurateResult, { path: '/user/2', params: { id: '2' }, id: '2', query: {} });
20+
assert.equal(accurateResult, { path: '/user/2', pathParams: { id: '2' }, id: '2', searchParams: {} });
2121

2222
const inaccurateResult = execPath('/', '/user/:id');
2323
assert.equal(inaccurateResult, undefined);
2424
});
2525

2626
test('Param rest segment', () => {
2727
const accurateResult = execPath('/user/foo', '/user/*');
28-
assert.equal(accurateResult, { path: '/user/foo', params: {}, query: {}, rest: '/foo' });
28+
assert.equal(accurateResult, { path: '/user/foo', pathParams: {}, searchParams: {}, rest: '/foo' });
2929

3030
const accurateResult2 = execPath('/user/foo/bar/baz', '/user/*');
31-
assert.equal(accurateResult2, { path: '/user/foo/bar/baz', params: {}, query: {}, rest: '/foo/bar/baz' });
31+
assert.equal(accurateResult2, { path: '/user/foo/bar/baz', pathParams: {}, searchParams: {}, rest: '/foo/bar/baz' });
3232

3333
const inaccurateResult = execPath('/user', '/user/*');
3434
assert.equal(inaccurateResult, undefined);
3535
});
3636

3737
test('Param route with rest segment', () => {
3838
const accurateResult = execPath('/user/2/foo', '/user/:id/*');
39-
assert.equal(accurateResult, { path: '/user/2/foo', params: { id: '2' }, id: '2', query: {}, rest: '/foo' });
39+
assert.equal(accurateResult, { path: '/user/2/foo', pathParams: { id: '2' }, id: '2', searchParams: {}, rest: '/foo' });
4040

4141
const accurateResult2 = execPath('/user/2/foo/bar/bob', '/user/:id/*');
4242
assert.equal(accurateResult2, {
4343
path: '/user/2/foo/bar/bob',
44-
params: { id: '2' },
44+
pathParams: { id: '2' },
4545
id: '2',
46-
query: {},
46+
searchParams: {},
4747
rest: '/foo/bar/bob'
4848
});
4949

@@ -53,30 +53,30 @@ test('Param route with rest segment', () => {
5353

5454
test('Optional param route', () => {
5555
const accurateResult = execPath('/user', '/user/:id?');
56-
assert.equal(accurateResult, { path: '/user', params: { id: undefined }, id: undefined, query: {} });
56+
assert.equal(accurateResult, { path: '/user', pathParams: { id: undefined }, id: undefined, searchParams: {} });
5757

5858
const inaccurateResult = execPath('/', '/user/:id?');
5959
assert.equal(inaccurateResult, undefined);
6060
});
6161

6262
test('Optional rest param route "/:x*"', () => {
6363
const matchedResult = execPath('/user', '/user/:id*');
64-
assert.equal(matchedResult, { path: '/user', params: { id: undefined }, id: undefined, query: {} });
64+
assert.equal(matchedResult, { path: '/user', pathParams: { id: undefined }, id: undefined, searchParams: {} });
6565

6666
const matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id*');
6767
assert.equal(matchedResultWithSlash, {
6868
path: '/user/foo/bar',
69-
params: { id: 'foo/bar' },
69+
pathParams: { id: 'foo/bar' },
7070
id: 'foo/bar',
71-
query: {}
71+
searchParams: {}
7272
});
7373

7474
const emptyResult = execPath('/user', '/user/:id*');
7575
assert.equal(emptyResult, {
7676
path: '/user',
77-
params: { id: undefined },
77+
pathParams: { id: undefined },
7878
id: undefined,
79-
query: {}
79+
searchParams: {}
8080
});
8181

8282
const inaccurateResult = execPath('/', '/user/:id*');
@@ -85,14 +85,14 @@ test('Optional rest param route "/:x*"', () => {
8585

8686
test('Rest param route "/:x+"', () => {
8787
const matchedResult = execPath('/user/foo', '/user/:id+');
88-
assert.equal(matchedResult, { path: '/user/foo', params: { id: 'foo' }, id: 'foo', query: {} });
88+
assert.equal(matchedResult, { path: '/user/foo', pathParams: { id: 'foo' }, id: 'foo', searchParams: {} });
8989

9090
const matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id+');
9191
assert.equal(matchedResultWithSlash, {
9292
path: '/user/foo/bar',
93-
params: { id: 'foo/bar' },
93+
pathParams: { id: 'foo/bar' },
9494
id: 'foo/bar',
95-
query: {}
95+
searchParams: {}
9696
});
9797

9898
const emptyResult = execPath('/user', '/user/:id+');
@@ -106,22 +106,22 @@ test('Handles leading/trailing slashes', () => {
106106
const result = execPath('/about-late/_SEGMENT1_/_SEGMENT2_/', '/about-late/:seg1/:seg2/');
107107
assert.equal(result, {
108108
path: '/about-late/_SEGMENT1_/_SEGMENT2_/',
109-
params: {
109+
pathParams: {
110110
seg1: '_SEGMENT1_',
111111
seg2: '_SEGMENT2_'
112112
},
113113
seg1: '_SEGMENT1_',
114114
seg2: '_SEGMENT2_',
115-
query: {}
115+
searchParams: {}
116116
});
117117
});
118118

119119
test('should not overwrite existing properties', () => {
120-
const result = execPath('/foo/bar', '/:path/:query', { path: '/custom-path' });
120+
const result = execPath('/foo/bar', '/:path/:searchParams', { path: '/custom-path' });
121121
assert.equal(result, {
122-
params: { path: 'foo', query: 'bar' },
122+
pathParams: { path: 'foo', searchParams: 'bar' },
123123
path: '/custom-path',
124-
query: {}
124+
searchParams: {},
125125
});
126126
});
127127

0 commit comments

Comments
 (0)