Initial commit
This commit is contained in:
commit
89053153cc
8152 files changed
+2086461
No files matched your search
+1104
File diff suppressed because it is too large.
Load diff
+72
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "../types/index.js";
|
||||
+359
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "../types/memory-location.js";
|
||||
+76
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "../types/use-browser-location.js";
|
||||
+86
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "../types/use-hash-location.js";
|
||||
+54
@@ -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
@@ -0,0 +1 @@
|
||||
export { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { useSyncExternalStore } from "use-sync-external-store/shim/index.native.js";
|
||||
+231
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
Reference in new issue
Block a user