Skip to content

Commit

Permalink
refactor: Begin to merge location & route contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
rschristian committed Feb 15, 2025
1 parent 6491ad4 commit bb4c47e
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 98 deletions.
12 changes: 3 additions & 9 deletions src/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type NestedArray<T> = Array<T | NestedArray<T>>;

/**
* Check if a URL path matches against a URL path pattern.
*
*
* Warning: This is an internal API exported only for testing purpose. API could change in future.
* @param url - URL path (e.g. /user/12345)
* @param route - URL pattern (e.g. /user/:id)
Expand Down Expand Up @@ -38,18 +38,12 @@ export function Router(props: {
interface LocationHook {
url: string;
path: string;
query: Record<string, string>;
pathParams: Record<string, string>;
searchParams: Record<string, string>;
route: (url: string, replace?: boolean) => void;
}
export const useLocation: () => LocationHook;

interface RouteHook {
path: string;
query: Record<string, string>;
params: Record<string, string>;
}
export const useRoute: () => RouteHook;

type RoutableProps =
| { path: string; default?: false; }
| { path?: never; default: true; }
Expand Down
32 changes: 13 additions & 19 deletions src/router.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, createContext, cloneElement, toChildArray } from 'preact';
import { h, Fragment, createContext, cloneElement, toChildArray } from 'preact';
import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks';

/**
Expand Down Expand Up @@ -51,12 +51,12 @@ const UPDATE = (state, url) => {
export const exec = (url, route, matches = {}) => {
url = url.split('/').filter(Boolean);
route = (route || '').split('/').filter(Boolean);
if (!matches.params) matches.params = {};
if (!matches.pathParams) matches.pathParams = {};
for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {
let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
let [, m, pathParam, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
val = url[i];
// segment match:
if (!m && param == val) continue;
if (!m && pathParam == val) continue;
// /foo/* match
if (!m && val && flag == '*') {
matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/');
Expand All @@ -69,8 +69,8 @@ export const exec = (url, route, matches = {}) => {
if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined;
// normal/optional field:
else if (val) val = decodeURIComponent(val);
matches.params[param] = val;
if (!(param in matches)) matches[param] = val;
matches.pathParams[pathParam] = val;
if (!(pathParam in matches)) matches[pathParam] = val;
if (rest) break;
}
return matches;
Expand All @@ -87,11 +87,12 @@ export function LocationProvider(props) {
const value = useMemo(() => {
const u = new URL(url, location.origin);
const path = u.pathname.replace(/\/+$/g, '') || '/';
// @ts-ignore-next

return {
url,
path,
query: Object.fromEntries(u.searchParams),
pathParams: {},
searchParams: Object.fromEntries(u.searchParams),
route: (url, replace) => route({ url, replace }),
wasPush
};
Expand All @@ -107,7 +108,6 @@ export function LocationProvider(props) {
};
}, []);

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

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

const { url, query, wasPush, path } = useLocation();
const { rest = path, params = {} } = useContext(RouteContext);
const { url, path, pathParams, searchParams, wasPush } = useLocation();

const isLoading = useRef(false);
const prevRoute = useRef(path);
Expand All @@ -137,7 +136,7 @@ export function Router(props) {

let pathRoute, defaultRoute, matchProps;
toChildArray(props.children).some((/** @type {VNode<any>} */ vnode) => {
const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params, rest: '' }));
const matches = exec(path, vnode.props.path, (matchProps = { ...vnode.props, path, pathParams, searchParams, rest: '' }));
if (matches) return (pathRoute = cloneElement(vnode, matchProps));
if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);
});
Expand All @@ -148,7 +147,7 @@ export function Router(props) {
prev.current = cur.current;

// Only mark as an update if the route component changed.
const outgoing = prev.current && prev.current.props.children;
const outgoing = prev.current;
if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {
// This hack prevents Preact from diffing when we swap `cur` to `prev`:
if (this.__v && this.__v.__k) this.__v.__k.reverse();
Expand All @@ -160,8 +159,7 @@ export function Router(props) {

const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;
const isHydratingBool = cur.current && cur.current.__h;
// @ts-ignore
cur.current = /** @type {VNode<any>} */ (h(RouteContext.Provider, { value: matchProps }, incoming));
cur.current = incoming;
if (isHydratingSuspense) {
cur.current.__u |= MODE_HYDRATE;
cur.current.__u |= MODE_SUSPENDED;
Expand Down Expand Up @@ -263,11 +261,7 @@ Router.Provider = LocationProvider;
LocationProvider.ctx = createContext(
/** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({})
);
const RouteContext = createContext(
/** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({})
);

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

export const useLocation = () => useContext(LocationProvider.ctx);
export const useRoute = () => useContext(RouteContext);
44 changes: 22 additions & 22 deletions test/node/router-match.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,46 @@ import * as assert from 'uvu/assert';
import { exec } from '../../src/router.js';

function execPath(path, pattern, opts) {
return exec(path, pattern, { path, query: {}, params: {}, ...(opts || {}) });
return exec(path, pattern, { path, searchParams: {}, pathParams: {}, ...(opts || {}) });
}

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

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

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

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

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

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

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

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

const accurateResult2 = execPath('/user/2/foo/bar/bob', '/user/:id/*');
assert.equal(accurateResult2, {
path: '/user/2/foo/bar/bob',
params: { id: '2' },
pathParams: { id: '2' },
id: '2',
query: {},
searchParams: {},
rest: '/foo/bar/bob'
});

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

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

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

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

const matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id*');
assert.equal(matchedResultWithSlash, {
path: '/user/foo/bar',
params: { id: 'foo/bar' },
pathParams: { id: 'foo/bar' },
id: 'foo/bar',
query: {}
searchParams: {}
});

const emptyResult = execPath('/user', '/user/:id*');
assert.equal(emptyResult, {
path: '/user',
params: { id: undefined },
pathParams: { id: undefined },
id: undefined,
query: {}
searchParams: {}
});

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

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

const matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id+');
assert.equal(matchedResultWithSlash, {
path: '/user/foo/bar',
params: { id: 'foo/bar' },
pathParams: { id: 'foo/bar' },
id: 'foo/bar',
query: {}
searchParams: {}
});

const emptyResult = execPath('/user', '/user/:id+');
Expand All @@ -106,22 +106,22 @@ test('Handles leading/trailing slashes', () => {
const result = execPath('/about-late/_SEGMENT1_/_SEGMENT2_/', '/about-late/:seg1/:seg2/');
assert.equal(result, {
path: '/about-late/_SEGMENT1_/_SEGMENT2_/',
params: {
pathParams: {
seg1: '_SEGMENT1_',
seg2: '_SEGMENT2_'
},
seg1: '_SEGMENT1_',
seg2: '_SEGMENT2_',
query: {}
searchParams: {}
});
});

test('should not overwrite existing properties', () => {
const result = execPath('/foo/bar', '/:path/:query', { path: '/custom-path' });
const result = execPath('/foo/bar', '/:path/:searchParams', { path: '/custom-path' });
assert.equal(result, {
params: { path: 'foo', query: 'bar' },
pathParams: { path: 'foo', searchParams: 'bar' },
path: '/custom-path',
query: {}
searchParams: {},
});
});

Expand Down
Loading

0 comments on commit bb4c47e

Please sign in to comment.