Initial commit

This commit is contained in:
mats committed 2026-10-19 16:04:17 +02:00
commit afed023152
8152 files changed
+2086461

No files matched your search

+1104
View File
File diff suppressed because it is too large. Load diff
+72
View File
@@ -0,0 +1,72 @@
{
"name": "wouter",
"version": "3.11.0",
"description": "Minimalist-friendly ~1.5KB router for React",
"type": "module",
"keywords": [
"react",
"preact",
"router",
"tiny",
"routing",
"hooks",
"useLocation"
],
"files": [
"src",
"types/**/*.d.ts",
"types/*.d.ts"
],
"main": "src/index.js",
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
},
"./use-browser-location": {
"types": "./types/use-browser-location.d.ts",
"default": "./src/use-browser-location.js"
},
"./use-hash-location": {
"types": "./types/use-hash-location.d.ts",
"default": "./src/use-hash-location.js"
},
"./memory-location": {
"types": "./types/memory-location.d.ts",
"default": "./src/memory-location.js"
}
},
"types": "types/index.d.ts",
"typesVersions": {
">=4.1": {
"types/index.d.ts": [
"types/index.d.ts"
],
"use-browser-location": [
"types/use-browser-location.d.ts"
],
"use-hash-location": [
"types/use-hash-location.d.ts"
],
"memory-location": [
"types/memory-location.d.ts"
]
}
},
"scripts": {
"prepublishOnly": "cp ../../README.md ."
},
"author": "Alexey Taktarov <molefrog@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/molefrog/wouter.git"
},
"license": "Unlicense",
"peerDependencies": {
"react": ">=16.8.0"
},
"dependencies": {
"regexparam": "^3.0.0",
"use-sync-external-store": "^1.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
export * from "../types/index.js";
+359
View File
@@ -0,0 +1,359 @@
import { parse as parsePattern } from "regexparam";
import {
useBrowserLocation,
useSearch as useBrowserSearch,
} from "./use-browser-location.js";
import {
useRef,
useContext,
createContext,
isValidElement,
cloneElement,
createElement as h,
Fragment,
forwardRef,
useIsomorphicLayoutEffect,
useEvent,
useMemo,
} from "./react-deps.js";
import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";
/*
* 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 overridden
* via the <Router /> component.
*/
const defaultRouter = {
hook: useBrowserLocation,
searchHook: useBrowserSearch,
parser: parsePattern,
base: "",
// this option is used to override the current location during SSR
ssrPath: undefined,
ssrSearch: undefined,
// optional context to track render state during SSR
ssrContext: undefined,
// customizes how `href` props are transformed for <Link />
hrefs: (x) => x,
// wraps navigate calls, useful for view transitions
aroundNav: (n, t, o) => n(t, o),
};
const RouterCtx = createContext(defaultRouter);
// gets the closest parent router from the context
export const useRouter = () => useContext(RouterCtx);
/**
* Parameters context. Used by `useParams()` to get the
* matched params from the innermost `Route` component.
*/
const Params0 = {},
ParamsCtx = createContext(Params0);
export const useParams = () => useContext(ParamsCtx);
/*
* Part 1, Hooks API: useRoute and useLocation
*/
// Internal location hooks avoid redundant context reads and navigation callbacks.
const usePathnameFromRouter = (router, base = router.base) =>
relativePath(base, router.hook(router)[0]);
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, opts) =>
router.aroundNav(navigate, absolutePath(to, router.base), opts)
),
];
};
export const useLocation = () => useLocationFromRouter(useRouter());
export const useSearch = () => {
const router = useRouter();
return sanitizeSearch(router.searchHook(router));
};
export const matchRoute = (parser, route, path, loose) => {
// if the input is a regexp, skip parsing
const { pattern, keys } =
route instanceof RegExp ? { pattern: route } : parser(route || "*", loose);
const result = pattern.exec(path);
if (!result) return [false, null];
// Keep positional captures as well as named params, with named params taking
// precedence (custom parsers can use numeric keys).
const params = {};
for (let i = 1; i < result.length; i++) params[i - 1] = result[i];
if (keys) {
for (let i = 0; i < keys.length; i++) params[keys[i]] = result[i + 1];
} else {
Object.assign(params, result.groups);
}
// In loose mode the full match is the base for nested routes:
// pattern `/a/:b` and path `/a/1/2/3` give the base `/a/1`.
return loose ? [true, params, result[0]] : [true, params];
};
export const useRoute = (pattern) => {
const router = useRouter();
const absolute = /^~\//.test(pattern);
return matchRoute(
router.parser,
absolute ? pattern.slice(1) : pattern,
usePathnameFromRouter(router, absolute ? "" : router.base)
);
};
/*
* Part 2, Low Carb Router API: Router, Route, Link, Switch
*/
export const Router = ({ children, ...props }) => {
// the router we will inherit from - it is the closest router in the tree,
// unless the custom `hook` is provided (in that case it's the default one)
const parent_ = useRouter();
const parent = props.hook ? defaultRouter : parent_;
// holds to the context value: the router object
let value = parent;
// when `ssrPath` contains a `?` character, we can extract the search from it.
// also, ensure ssrSearch is always defined when ssrPath is provided, so that
// useSearch behavior matches usePathname (proper SSR hydration when client
// renders <Router> without props after server rendered with ssrPath/ssrSearch)
if (props.ssrPath) {
const [path, search = props.ssrSearch ?? ""] = props.ssrPath.split("?");
if (path) (props.ssrSearch = search), (props.ssrPath = path);
}
// hooks can define their own `href` formatter (e.g. for hash location)
props.hrefs = props.hrefs ?? props.hook?.hrefs;
// hooks can define their own search hook (e.g. for memory location)
props.searchHook = props.searchHook ?? props.hook?.searchHook;
// what is happening below: to avoid unnecessary rerenders in child components,
// we ensure that the router object reference is stable, unless there are any
// changes that require reload (e.g. `base` prop changes -> all components that
// get the router from the context should rerender, even if the component is memoized).
// the expected behaviour is:
//
// 1) when the resulted router is no different from the parent, use parent
// 2) if the custom `hook` prop is provided, we always inherit from the
// default router instead. this resets all previously overridden options.
// 3) when the router is customized here, it should stay stable between renders
let ref = useRef(parent),
prev = ref.current,
next = prev;
for (let k in parent) {
const option =
k === "base"
? /* base is special case, it is appended to the parent's base */
parent[k] + (props[k] ?? "")
: props[k] ?? parent[k];
if (option !== next[k]) {
if (prev === next) ref.current = next = { ...next };
next[k] = option;
}
// the new router is no different from the parent or from the memoized value, use parent
if (option !== parent[k] || option !== value[k]) value = next;
}
return h(RouterCtx.Provider, { value }, children);
};
const h_route = ({ children, component }, params) => {
// React-Router style `component` prop
if (component) return h(component, { params });
// support render prop or plain children
return typeof children === "function" ? children(params) : children;
};
// Cache params object between renders if values are shallow equal
const useCachedParams = (value) => {
let prev = useRef(Params0);
const curr = prev.current,
keys = Object.keys(value);
return (prev.current =
// Update cache if number of params changed or any value changed
keys.length !== Object.keys(curr).length ||
keys.some((k) => value[k] !== curr[k])
? value // Return new value if there are changes
: curr); // Return cached value if nothing changed
};
export function useSearchParams() {
const [location, navigate] = useLocation();
const search = useSearch();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
// cached value before next render, so you can call setSearchParams multiple times
let tempSearchParams = searchParams;
const setSearchParams = useEvent((nextInit, options) => {
tempSearchParams = new URLSearchParams(
typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit
);
const paramsStr = tempSearchParams.toString();
navigate(location + (paramsStr ? "?" + paramsStr : ""), options);
});
return [searchParams, setSearchParams];
}
export const Route = ({ path, nest, match, ...renderProps }) => {
const router = useRouter();
const location = usePathnameFromRouter(router);
const [matches, routeParams, base] =
// `match` is a special prop to give up control to the parent,
// it is used by the `Switch` to avoid double matching
match ?? matchRoute(router.parser, path, location, nest);
// when `routeParams` is `null` (there was no match), the argument
// below becomes {...null} = {}, see the Object Spread specs
// https://tc39.es/proposal-object-rest-spread/#AbstractOperations-CopyDataProperties
const params = useCachedParams({ ...useParams(), ...routeParams });
if (!matches) return null;
const children = h_route(renderProps, params);
return h(
ParamsCtx.Provider,
{ value: params },
base ? h(Router, { base }, children) : children
);
};
export const Link = forwardRef((props, ref) => {
const router = useRouter();
const [currentPath, navigate] = useLocationFromRouter(router);
const {
to = "",
href: targetPath = to,
onClick: _onClick,
asChild,
children,
className: cls,
/* eslint-disable no-unused-vars */
replace /* ignore nav props */,
state /* ignore nav props */,
transition /* ignore nav props */,
/* eslint-enable no-unused-vars */
...linkProps
} = props;
linkProps.onClick = useEvent((event) => {
// ignores the navigation when clicked using right mouse button or
// by holding a special modifier key: ctrl, command, win, alt, shift
if (
event.ctrlKey ||
event.metaKey ||
event.altKey ||
event.shiftKey ||
event.button !== 0
)
return;
_onClick?.(event);
if (!event.defaultPrevented) {
event.preventDefault();
navigate(targetPath, props);
}
});
// handle nested routers and absolute paths
linkProps.href = router.hrefs(
targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath,
router // pass router as a second argument for convinience
);
// Omitted props should preserve the child's own className and ref.
if (cls !== undefined)
linkProps.className = cls?.call ? cls(currentPath === targetPath) : cls;
if (ref) linkProps.ref = ref;
return asChild && isValidElement(children)
? cloneElement(children, linkProps)
: h("a", linkProps, children);
});
const flattenChildren = (children, result = []) => {
if (Array.isArray(children)) {
for (const c of children)
flattenChildren(c && c.type === Fragment ? c.props.children : c, result);
} else result.push(children);
return result;
};
export const Switch = ({ children, location }) => {
const router = useRouter();
const originalLocation = usePathnameFromRouter(router);
for (const element of flattenChildren(children)) {
let match = 0;
if (
isValidElement(element) &&
// we don't require an element to be of type Route,
// but we do require it to contain a truthy `path` prop.
// this allows to use different components that wrap Route
// inside of a switch, for example <AnimatedRoute />.
(match = matchRoute(
router.parser,
element.props.path,
location || originalLocation,
element.props.nest
))[0]
)
return cloneElement(element, { match });
}
return null;
};
export const Redirect = (props) => {
const { to, href = to } = props;
const router = useRouter();
const [, navigate] = useLocationFromRouter(router);
const redirect = useEvent(() => navigate(to || href, props));
const { ssrContext } = router;
// redirect is guaranteed to be stable since it is returned from useEvent
useIsomorphicLayoutEffect(() => {
redirect();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (ssrContext) {
ssrContext.redirectTo = to;
}
return null;
};
+1
View File
@@ -0,0 +1 @@
export * from "../types/memory-location.js";
+76
View File
@@ -0,0 +1,76 @@
import { useSyncExternalStore } from "./react-deps.js";
/**
* In-memory location that supports navigation
*/
export const memoryLocation = ({
path = "/",
searchPath = "",
state: initialState = null,
static: staticLocation,
record,
} = {}) => {
let initialPath = path;
if (searchPath) {
// join with & if path contains search query, and ? otherwise
initialPath += path.split("?")[1] ? "&" : "?";
initialPath += searchPath;
}
let [currentPath, currentSearch = ""] = initialPath.split("?");
let currentState = initialState;
const history = [initialPath];
let listeners = [];
const navigateImplementation = (path, { replace = false, state } = {}) => {
if (record)
history[replace && history.length ? history.length - 1 : history.length] =
path;
[currentPath, currentSearch = ""] = path.split("?");
if (state !== undefined) currentState = state;
listeners.forEach((cb) => cb());
};
const navigate = !staticLocation ? navigateImplementation : () => null;
// Copy on subscription changes instead of copying on every navigation.
const subscribe = (cb) => {
listeners = [...listeners, cb];
return () => {
listeners = listeners.filter((i) => i !== cb);
};
};
// Fresh getters keep React 18's store cache current after render-phase updates.
// https://github.com/facebook/react/pull/25578
const useMemoryLocation = () => [
useSyncExternalStore(subscribe, () => currentPath),
navigate,
];
const useMemoryQuery = () =>
useSyncExternalStore(subscribe, () => currentSearch);
// Attach searchHook to the location hook for auto-inheritance in Router
useMemoryLocation.searchHook = useMemoryQuery;
function reset() {
// clean history array with mutation to preserve link
history.length = 0;
navigateImplementation(initialPath, { state: initialState });
}
return Object.defineProperty(
{
hook: useMemoryLocation,
searchHook: useMemoryQuery,
navigate,
history: record ? history : undefined,
reset: record ? reset : undefined,
},
"state",
{ enumerable: true, get: () => currentState }
);
};
+38
View File
@@ -0,0 +1,38 @@
/**
* When basepath is `undefined` or '/' it is ignored (we assume it's empty string)
*/
const baseDefaults = (base = "") => (base === "/" ? "" : base);
export const absolutePath = (to, base) =>
to[0] === "~" ? to.slice(1) : baseDefaults(base) + to;
/*
* 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) => {
base = unescape(baseDefaults(base));
path = unescape(path);
return !base || !path.toLowerCase().indexOf(base.toLowerCase())
? path.slice(base.length) || "/"
: "~" + path;
};
/*
* decodes escape sequences such as %20
*/
const unescape = (str) => {
try {
return str.includes("%") ? decodeURI(str) : str;
} catch (_e) {
// fail-safe mode: if string can't be decoded do nothing
return str;
}
};
/*
* Removes leading question mark
*/
export const sanitizeSearch = (search) =>
unescape(search[0] === "?" ? search.slice(1) : search);
+64
View File
@@ -0,0 +1,64 @@
import * as React from "react";
// React.useInsertionEffect is not available in React <18
// This hack fixes a transpilation issue on some apps
const useBuiltinInsertionEffect = React["useInsertion" + "Effect"];
export {
useMemo,
useRef,
useState,
useContext,
createContext,
isValidElement,
cloneElement,
createElement,
Fragment,
forwardRef,
} from "react";
// To resolve webpack 5 errors, while not presenting problems for native,
// we copy the approaches from https://github.com/TanStack/query/pull/3561
// and https://github.com/TanStack/query/pull/3601
// ~ Show this aging PR some love to remove the need for this hack:
// https://github.com/facebook/react/pull/25231 ~
export { useSyncExternalStore } from "./use-sync-external-store.js";
// Copied from:
// https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js
const canUseDOM = !!(
typeof window !== "undefined" &&
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined"
);
// Copied from:
// https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts
// "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."
export const useIsomorphicLayoutEffect = canUseDOM
? React.useLayoutEffect
: React.useEffect;
// useInsertionEffect is already a noop on the server.
// See: https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js
export const useInsertionEffect =
useBuiltinInsertionEffect || useIsomorphicLayoutEffect;
// Userland polyfill while we wait for the forthcoming
// https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
// Note: "A high-fidelity polyfill for useEvent is not possible because
// there is no lifecycle or Hook in React that we can use to switch
// .current at the right timing."
// So we will have to make do with this "close enough" approach for now.
export const useEvent = (fn) => {
const ref = React.useRef([fn, (...args) => ref[0](...args)]).current;
// Per Dan Abramov: useInsertionEffect executes marginally closer to the
// correct timing for ref synchronization than useLayoutEffect on React 18.
// See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
useInsertionEffect(() => {
ref[0] = fn;
});
return ref[1];
};
+1
View File
@@ -0,0 +1 @@
export * from "../types/use-browser-location.js";
+86
View File
@@ -0,0 +1,86 @@
import { useSyncExternalStore } from "./react-deps.js";
/**
* History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
*/
const events = ["popstate", "pushState", "replaceState", "hashchange"];
let listeners = [];
const onLocationChange = () => listeners.forEach((callback) => callback());
// Native events can run microtasks between listeners. Notify all subscribers
// together so React can process parent and child updates in the same batch.
const subscribeToLocationUpdates = (callback) => {
if (listeners.push(callback) === 1)
events.forEach((event) => addEventListener(event, onLocationChange));
return () => {
listeners = listeners.filter((listener) => listener !== callback);
if (!listeners.length)
events.forEach((event) => removeEventListener(event, onLocationChange));
};
};
export const useLocationProperty = (fn, ssrFn) =>
useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn);
const currentSearch = () => location.search;
export const useSearch = ({ ssrSearch } = {}) =>
useLocationProperty(
currentSearch,
// != null checks for both null and undefined, but allows empty string ""
// This allows proper hydration: server renders with ssrSearch="?foo",
// client hydrates with just <Router /> and reads from location.search
ssrSearch != null ? () => ssrSearch : currentSearch
);
const currentPathname = () => location.pathname;
export const usePathname = ({ ssrPath } = {}) =>
useLocationProperty(
currentPathname,
// != null checks for both null and undefined, but allows empty string ""
// This allows proper hydration: server renders with ssrPath="/foo",
// client hydrates with just <Router /> and reads from location.pathname
ssrPath != null ? () => ssrPath : currentPathname
);
const currentHistoryState = () => history.state;
export const useHistoryState = () =>
useLocationProperty(currentHistoryState, () => null);
export const navigate = (to, { replace = false, state = null } = {}) =>
history[replace ? "replaceState" : "pushState"](state, "", to);
// the 2nd argument of the `useBrowserLocation` return value is a function
// that allows to perform a navigation.
export const useBrowserLocation = (opts) => [usePathname(opts), navigate];
const patchKey = Symbol.for("wouter_v3");
// While History API does have `popstate` event, the only
// proper way to listen to changes via `push/replaceState`
// is to monkey-patch these methods.
//
// See https://stackoverflow.com/a/4585031
if (typeof history !== "undefined" && typeof window[patchKey] === "undefined") {
for (const type of ["pushState", "replaceState"]) {
const original = history[type];
// TODO: we should be using unstable_batchedUpdates to avoid multiple re-renders,
// however that will require an additional peer dependency on react-dom.
// See: https://github.com/reactwg/react-18/discussions/86#discussioncomment-1567149
history[type] = function () {
const result = original.apply(this, arguments);
const event = new Event(type);
event.arguments = arguments;
dispatchEvent(event);
return result;
};
}
// patch history object only once
// See: https://github.com/molefrog/wouter/issues/167
Object.defineProperty(window, patchKey, { value: true });
}
+1
View File
@@ -0,0 +1 @@
export * from "../types/use-hash-location.js";
+54
View File
@@ -0,0 +1,54 @@
import { useSyncExternalStore } from "./react-deps.js";
// array of callback subscribed to hash updates
let listeners = [];
const onHashChange = () => listeners.forEach((cb) => cb());
// we subscribe to `hashchange` only once when needed to guarantee that
// all listeners are called synchronously
const subscribeToHashUpdates = (callback) => {
if (listeners.push(callback) === 1)
addEventListener("hashchange", onHashChange);
return () => {
listeners = listeners.filter((i) => i !== callback);
if (!listeners.length) removeEventListener("hashchange", onHashChange);
};
};
// leading '#' is ignored, leading '/' is optional
const hashPrefix = /^#?\/?/;
const currentHashLocation = () => "/" + location.hash.replace(hashPrefix, "");
export const navigate = (to, { state = null, replace = false } = {}) => {
const oldURL = location.href;
const [hash, search] = to.replace(hashPrefix, "").split("?");
// Works for ALL protocols including data:
const url = new URL(oldURL);
url.hash = `/${hash}`;
if (search !== undefined) url.search = search;
const newURL = url.href;
history[replace ? "replaceState" : "pushState"](state, "", newURL);
const event =
typeof HashChangeEvent !== "undefined"
? new HashChangeEvent("hashchange", { oldURL, newURL })
: new Event("hashchange", { detail: { oldURL, newURL } });
dispatchEvent(event);
};
export const useHashLocation = ({ ssrPath = "/" } = {}) => [
useSyncExternalStore(
subscribeToHashUpdates,
currentHashLocation,
() => ssrPath
),
navigate,
];
useHashLocation.hrefs = (href) => "#" + href;
+1
View File
@@ -0,0 +1 @@
export { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
+1
View File
@@ -0,0 +1 @@
export { useSyncExternalStore } from "use-sync-external-store/shim/index.native.js";
+231
View File
@@ -0,0 +1,231 @@
// Minimum TypeScript Version: 4.1
// tslint:disable:no-unnecessary-generics
import {
AnchorHTMLAttributes,
FunctionComponent,
RefAttributes,
ComponentType,
ReactNode,
ReactElement,
MouseEventHandler,
JSXElementConstructor,
} from "react";
import {
Path,
PathPattern,
BaseLocationHook,
HookReturnValue,
HookNavigationOptions,
BaseSearchHook,
} from "./location-hook.js";
import {
BrowserLocationHook,
BrowserSearchHook,
} from "./use-browser-location.js";
import { Parser, RouterObject, RouterOptions } from "./router.js";
// these files only export types, so we can re-export them as-is
// in TS 5.0 we'll be able to use `export type * from ...`
export * from "./location-hook.js";
export * from "./router.js";
import { RouteParams } from "regexparam";
export type StringRouteParams<T extends string> = RouteParams<T> & {
[param: number]: string | undefined;
};
export type RegexRouteParams = { [key: string | number]: string | undefined };
/**
* Route patterns and parameters
*/
export interface DefaultParams {
readonly [paramName: string | number]: string | undefined;
}
export type Params<T extends DefaultParams = DefaultParams> = T;
export type MatchWithParams<T extends DefaultParams = DefaultParams> = [
true,
Params<T>
];
export type NoMatch = [false, null];
export type Match<T extends DefaultParams = DefaultParams> =
| MatchWithParams<T>
| NoMatch;
/*
* Components: <Route />
*/
export interface RouteComponentProps<T extends DefaultParams = DefaultParams> {
params: T;
}
export interface RouteProps<
T extends DefaultParams | undefined = undefined,
RoutePath extends PathPattern = PathPattern
> {
children?:
| ((
params: T extends DefaultParams
? T
: RoutePath extends string
? StringRouteParams<RoutePath>
: RegexRouteParams
) => ReactNode)
| ReactNode;
path?: RoutePath;
component?: JSXElementConstructor<
RouteComponentProps<
T extends DefaultParams
? T
: RoutePath extends string
? StringRouteParams<RoutePath>
: RegexRouteParams
>
>;
nest?: boolean;
}
export function Route<
T extends DefaultParams | undefined = undefined,
RoutePath extends PathPattern = PathPattern
>(props: RouteProps<T, RoutePath>): ReturnType<FunctionComponent>;
/*
* Components: <Link /> & <Redirect />
*/
export type NavigationalProps<
H extends BaseLocationHook = BrowserLocationHook
> = ({ to: Path; href?: never } | { href: Path; to?: never }) &
HookNavigationOptions<H>;
export type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
NavigationalProps<H> & {
children?: never;
};
export function Redirect<H extends BaseLocationHook = BrowserLocationHook>(
props: RedirectProps<H>,
context?: any
): null;
type AsChildProps<ComponentProps, DefaultElementProps> =
| ({ asChild?: false } & DefaultElementProps)
| ({ asChild: true } & ComponentProps);
type HTMLLinkAttributes = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
"className"
> & {
className?: string | undefined | ((isActive: boolean) => string | undefined);
};
export type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
NavigationalProps<H> &
AsChildProps<
Omit<HTMLLinkAttributes, "onClick"> &
RefAttributes<HTMLElement> & {
children: ReactElement;
onClick?: MouseEventHandler;
},
HTMLLinkAttributes & RefAttributes<HTMLAnchorElement>
>;
export function Link<H extends BaseLocationHook = BrowserLocationHook>(
props: LinkProps<H>,
context?: any
): ReturnType<FunctionComponent>;
/*
* Components: <Switch />
*/
export interface SwitchProps {
location?: string;
children: ReactNode;
}
export const Switch: FunctionComponent<SwitchProps>;
/*
* Components: <Router />
*/
export type RouterProps = RouterOptions & {
children: ReactNode;
};
export const Router: FunctionComponent<RouterProps>;
/*
* Hooks
*/
export function useRouter(): RouterObject;
export function useRoute<
T extends DefaultParams | undefined = undefined,
RoutePath extends PathPattern = PathPattern
>(
pattern: RoutePath
): Match<
T extends DefaultParams
? T
: RoutePath extends string
? StringRouteParams<RoutePath>
: RegexRouteParams
>;
export function useLocation<
H extends BaseLocationHook = BrowserLocationHook
>(): HookReturnValue<H>;
export function useSearch<
H extends BaseSearchHook = BrowserSearchHook
>(): ReturnType<H>;
export type URLSearchParamsInit = ConstructorParameters<
typeof URLSearchParams
>[0];
export type SetSearchParams = (
nextInit:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
options?: { replace?: boolean; state?: any }
) => void;
export function useSearchParams(): [URLSearchParams, SetSearchParams];
export function useParams<T = undefined>(): T extends string
? StringRouteParams<T>
: T extends undefined
? DefaultParams
: T;
/*
* Helpers
*/
export function matchRoute<
T extends DefaultParams | undefined = undefined,
RoutePath extends PathPattern = PathPattern
>(
parser: Parser,
pattern: RoutePath,
path: string,
loose?: boolean
): Match<
T extends DefaultParams
? T
: RoutePath extends string
? StringRouteParams<RoutePath>
: RegexRouteParams
>;
// tslint:enable:no-unnecessary-generics
+41
View File
@@ -0,0 +1,41 @@
/*
* Foundation: useLocation and paths
*/
export type Path = string;
export type PathPattern = string | RegExp;
export type SearchString = string;
export type HrefsFormatter = (href: string, router?: any) => string;
// the base useLocation hook type. Any custom hook (including the
// default one) should inherit from it.
export type BaseLocationHook = {
(...args: any[]): [Path, (path: Path, ...args: any[]) => any];
searchHook?: BaseSearchHook;
hrefs?: HrefsFormatter;
};
export type BaseSearchHook = (...args: any[]) => SearchString;
/*
* Utility types that operate on hook
*/
// Returns the type of the location tuple of the given hook.
export type HookReturnValue<H extends BaseLocationHook> = ReturnType<H>;
// Utility type that allows us to handle cases like `any` and `never`
type EmptyInterfaceWhenAnyOrNever<T> = 0 extends 1 & T
? {}
: [T] extends [never]
? {}
: T;
// Returns the type of the navigation options that hook's push function accepts.
export type HookNavigationOptions<H extends BaseLocationHook> =
EmptyInterfaceWhenAnyOrNever<
NonNullable<Parameters<HookReturnValue<H>[1]>[1]> // get's the second argument of a tuple returned by the hook
>;
+34
View File
@@ -0,0 +1,34 @@
import {
BaseLocationHook,
BaseSearchHook,
Path,
SearchString,
} from "./location-hook.js";
type Navigate<S = any> = (
to: Path,
options?: { replace?: boolean; state?: S; transition?: boolean }
) => void;
type HookReturnValue<S = unknown> = {
hook: BaseLocationHook;
searchHook: BaseSearchHook;
navigate: Navigate<S>;
readonly state: S | null;
};
type StubHistory = { history: Path[]; reset: () => void };
export function memoryLocation<S = unknown>(options?: {
path?: Path;
searchPath?: SearchString;
state?: S;
static?: boolean;
record?: false;
}): HookReturnValue<S>;
export function memoryLocation<S = unknown>(options?: {
path?: Path;
searchPath?: SearchString;
state?: S;
static?: boolean;
record: true;
}): HookReturnValue<S> & StubHistory;
+62
View File
@@ -0,0 +1,62 @@
import {
Path,
SearchString,
BaseLocationHook,
BaseSearchHook,
HrefsFormatter,
} from "./location-hook.js";
export type Parser = (
route: Path,
loose?: boolean
) => { pattern: RegExp; keys: string[] };
// Standard navigation options supported by all built-in location hooks
export type NavigateOptions<S = any> = {
replace?: boolean;
state?: S;
/** Enable view transitions for this navigation (used with aroundNav) */
transition?: boolean;
};
// Function that wraps navigate calls, useful for view transitions
export type AroundNavHandler = (
navigate: (to: Path, options?: NavigateOptions) => void,
to: Path,
options?: NavigateOptions
) => void;
// the object returned from `useRouter`
export interface RouterObject {
readonly hook: BaseLocationHook;
readonly searchHook: BaseSearchHook;
readonly base: Path;
readonly ownBase: Path;
readonly parser: Parser;
readonly ssrPath?: Path;
readonly ssrSearch?: SearchString;
readonly ssrContext?: SsrContext;
readonly hrefs: HrefsFormatter;
readonly aroundNav: AroundNavHandler;
}
// state captured during SSR render
export type SsrContext = {
// if a redirect was encountered, this will be populated with the path
redirectTo?: Path;
// HTTP status code to set for SSR response
statusCode?: number;
};
// basic options to construct a router
export type RouterOptions = {
hook?: BaseLocationHook;
searchHook?: BaseSearchHook;
base?: Path;
parser?: Parser;
ssrPath?: Path;
ssrSearch?: SearchString;
ssrContext?: SsrContext;
hrefs?: HrefsFormatter;
aroundNav?: AroundNavHandler;
};
+35
View File
@@ -0,0 +1,35 @@
import { Path, SearchString } from "./location-hook.js";
type Primitive = string | number | bigint | boolean | null | undefined | symbol;
export const useLocationProperty: <S extends Primitive>(
fn: () => S,
ssrFn?: () => S
) => S;
export type BrowserSearchHook = (options?: {
ssrSearch?: SearchString;
}) => SearchString;
export const useSearch: BrowserSearchHook;
export const usePathname: (options?: { ssrPath?: Path }) => Path;
export const useHistoryState: <T = any>() => T;
export const navigate: <S = any>(
to: string | URL,
options?: { replace?: boolean; state?: S; transition?: boolean }
) => void;
/*
* Default `useLocation`
*/
// The type of the default `useLocation` hook that wouter uses.
// It operates on current URL using History API, supports base path and can
// navigate with `pushState` or `replaceState`.
export type BrowserLocationHook = (options?: {
ssrPath?: Path;
}) => [Path, typeof navigate];
export const useBrowserLocation: BrowserLocationHook;
+10
View File
@@ -0,0 +1,10 @@
import { Path } from "./location-hook.js";
export function navigate<S = any>(
to: Path,
options?: { state?: S; replace?: boolean; transition?: boolean }
): void;
export function useHashLocation(options?: {
ssrPath?: Path;
}): [Path, typeof navigate];