| export * from "../types/index.js"; |
+362
| 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, | ||
| }; | ||
| 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 version of useLocation to avoid redundant useRouter calls | ||
| 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, navOpts) => navigate(absolutePath(to, router.base), navOpts)), | ||
| ]; | ||
| }; | ||
| 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 | ||
| ? { keys: false, pattern: route } | ||
| : parser(route || "*", loose); | ||
| // array destructuring loses keys, so this is done in two steps | ||
| const result = pattern.exec(path) || []; | ||
| // when parser is in "loose" mode, `$base` is equal to the | ||
| // first part of the route that matches the pattern | ||
| // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`) | ||
| // we use this for route nesting | ||
| const [$base, ...matches] = result; | ||
| return $base !== undefined | ||
| ? [ | ||
| true, | ||
| (() => { | ||
| // for regex paths, `keys` will always be false | ||
| // an object with parameters matched, e.g. { foo: "bar" } for "/:foo" | ||
| // we "zip" two arrays here to construct the object | ||
| // ["foo"], ["bar"] → { foo: "bar" } | ||
| const groups = | ||
| keys !== false | ||
| ? Object.fromEntries(keys.map((key, i) => [key, matches[i]])) | ||
| : result.groups; | ||
| // convert the array to an instance of object | ||
| // this makes it easier to integrate with the existing param implementation | ||
| let obj = { ...matches }; | ||
| // merge named capture groups with matches array | ||
| groups && Object.assign(obj, groups); | ||
| return obj; | ||
| })(), | ||
| // the third value if only present when parser is in "loose" mode, | ||
| // so that we can extract the base path for nested routes | ||
| ...(loose ? [$base] : []), | ||
| ] | ||
| : [false, null]; | ||
| }; | ||
| export const useRoute = (pattern) => | ||
| matchRoute(useRouter().parser, pattern, useLocation()[0]); | ||
| /* | ||
| * 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 | ||
| const [path, search] = props.ssrPath?.split("?") ?? []; | ||
| if (search) (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({}), | ||
| 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 (prev === next && option !== next[k]) { | ||
| 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; | ||
| return (prev.current = | ||
| // Update cache if number of params changed or any value changed | ||
| Object.keys(value).length !== Object.keys(curr).length || | ||
| Object.entries(value).some(([k, v]) => v !== 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 | ||
| ); | ||
| navigate(location + "?" + tempSearchParams, options); | ||
| }); | ||
| return [searchParams, setSearchParams]; | ||
| } | ||
| export const Route = ({ path, nest, match, ...renderProps }) => { | ||
| const router = useRouter(); | ||
| const [location] = useLocationFromRouter(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 = base | ||
| ? h(Router, { base }, h_route(renderProps, params)) | ||
| : h_route(renderProps, params); | ||
| return h(ParamsCtx.Provider, { value: params, 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 */, | ||
| /* eslint-enable no-unused-vars */ | ||
| ...restProps | ||
| } = props; | ||
| const 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 | ||
| const href = router.hrefs( | ||
| targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath, | ||
| router // pass router as a second argument for convinience | ||
| ); | ||
| return asChild && isValidElement(children) | ||
| ? cloneElement(children, { onClick, href }) | ||
| : h("a", { | ||
| ...restProps, | ||
| onClick, | ||
| href, | ||
| // `className` can be a function to apply the class if this link is active | ||
| className: cls?.call ? cls(currentPath === targetPath) : cls, | ||
| children, | ||
| ref, | ||
| }); | ||
| }); | ||
| const flattenChildren = (children) => | ||
| Array.isArray(children) | ||
| ? children.flatMap((c) => | ||
| flattenChildren(c && c.type === Fragment ? c.props.children : c) | ||
| ) | ||
| : [children]; | ||
| export const Switch = ({ children, location }) => { | ||
| const router = useRouter(); | ||
| const [originalLocation] = useLocationFromRouter(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; | ||
| }; |
| export * from "../types/memory-location.js"; |
| import mitt from "mitt"; | ||
| import { useSyncExternalStore } from "./react-deps.js"; | ||
| /** | ||
| * In-memory location that supports navigation | ||
| */ | ||
| export const memoryLocation = ({ | ||
| path = "/", | ||
| searchPath = "", | ||
| 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("?"); | ||
| const history = [initialPath]; | ||
| const emitter = mitt(); | ||
| const navigateImplementation = (path, { replace = false } = {}) => { | ||
| if (record) { | ||
| if (replace) { | ||
| history.splice(history.length - 1, 1, path); | ||
| } else { | ||
| history.push(path); | ||
| } | ||
| } | ||
| [currentPath, currentSearch = ""] = path.split("?"); | ||
| emitter.emit("navigate", path); | ||
| }; | ||
| const navigate = !staticLocation ? navigateImplementation : () => null; | ||
| const subscribe = (cb) => { | ||
| emitter.on("navigate", cb); | ||
| return () => emitter.off("navigate", cb); | ||
| }; | ||
| 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.splice(0, history.length); | ||
| navigateImplementation(initialPath); | ||
| } | ||
| return { | ||
| hook: useMemoryLocation, | ||
| searchHook: useMemoryQuery, | ||
| navigate, | ||
| history: record ? history : undefined, | ||
| reset: record ? reset : undefined, | ||
| }; | ||
| }; |
+38
| /* | ||
| * Transforms `path` into its relative `base` version | ||
| * If base isn't part of the path provided returns absolute path e.g. `~/app` | ||
| */ | ||
| const _relativePath = (base, path) => | ||
| !path.toLowerCase().indexOf(base.toLowerCase()) | ||
| ? path.slice(base.length) || "/" | ||
| : "~" + path; | ||
| /** | ||
| * 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; | ||
| export const relativePath = (base = "", path) => | ||
| _relativePath(unescape(baseDefaults(base)), unescape(path)); | ||
| /* | ||
| * Removes leading question mark | ||
| */ | ||
| const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str); | ||
| /* | ||
| * decodes escape sequences such as %20 | ||
| */ | ||
| const unescape = (str) => { | ||
| try { | ||
| return decodeURI(str); | ||
| } catch (_e) { | ||
| // fail-safe mode: if string can't be decoded do nothing | ||
| return str; | ||
| } | ||
| }; | ||
| export const sanitizeSearch = (search) => unescape(stripQm(search)); |
| 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]; | ||
| }; |
| export * from "../types/use-browser-location.js"; |
| import { useSyncExternalStore } from "./react-deps.js"; | ||
| /** | ||
| * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History | ||
| */ | ||
| const eventPopstate = "popstate"; | ||
| const eventPushState = "pushState"; | ||
| const eventReplaceState = "replaceState"; | ||
| const eventHashchange = "hashchange"; | ||
| const events = [ | ||
| eventPopstate, | ||
| eventPushState, | ||
| eventReplaceState, | ||
| eventHashchange, | ||
| ]; | ||
| const subscribeToLocationUpdates = (callback) => { | ||
| for (const event of events) { | ||
| addEventListener(event, callback); | ||
| } | ||
| return () => { | ||
| for (const event of events) { | ||
| removeEventListener(event, callback); | ||
| } | ||
| }; | ||
| }; | ||
| export const useLocationProperty = (fn, ssrFn) => | ||
| useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn); | ||
| const currentSearch = () => location.search; | ||
| export const useSearch = ({ ssrSearch } = {}) => | ||
| useLocationProperty( | ||
| currentSearch, | ||
| ssrSearch != null ? () => ssrSearch : currentSearch | ||
| ); | ||
| const currentPathname = () => location.pathname; | ||
| export const usePathname = ({ ssrPath } = {}) => | ||
| useLocationProperty( | ||
| currentPathname, | ||
| ssrPath != null ? () => ssrPath : currentPathname | ||
| ); | ||
| const currentHistoryState = () => history.state; | ||
| export const useHistoryState = () => | ||
| useLocationProperty(currentHistoryState, () => null); | ||
| export const navigate = (to, { replace = false, state = null } = {}) => | ||
| history[replace ? eventReplaceState : eventPushState](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 [eventPushState, eventReplaceState]) { | ||
| 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 }); | ||
| } |
| export * from "../types/use-hash-location.js"; |
| import { useSyncExternalStore } from "./react-deps.js"; | ||
| // array of callback subscribed to hash updates | ||
| const listeners = { | ||
| v: [], | ||
| }; | ||
| const onHashChange = () => listeners.v.forEach((cb) => cb()); | ||
| // we subscribe to `hashchange` only once when needed to guarantee that | ||
| // all listeners are called synchronously | ||
| const subscribeToHashUpdates = (callback) => { | ||
| if (listeners.v.push(callback) === 1) | ||
| addEventListener("hashchange", onHashChange); | ||
| return () => { | ||
| listeners.v = listeners.v.filter((i) => i !== callback); | ||
| if (!listeners.v.length) removeEventListener("hashchange", onHashChange); | ||
| }; | ||
| }; | ||
| // leading '#' is ignored, leading '/' is optional | ||
| const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, ""); | ||
| export const navigate = (to, { state = null, replace = false } = {}) => { | ||
| const oldURL = location.href; | ||
| const [hash, search] = to.replace(/^#?\/?/, "").split("?"); | ||
| // Works for ALL protocols including data: | ||
| const url = new URL(location.href); | ||
| url.hash = `/${hash}`; | ||
| if (search) url.search = search; | ||
| const newURL = url.href; | ||
| if (replace) { | ||
| history.replaceState(state, "", newURL); | ||
| } else { | ||
| history.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; |
| export { useSyncExternalStore } from "use-sync-external-store/shim/index.js"; |
| export { useSyncExternalStore } from "use-sync-external-store/shim/index.native.js"; |
+8
-10
| { | ||
| "name": "wouter", | ||
| "version": "3.7.1", | ||
| "version": "3.8.0-beta.1", | ||
| "description": "Minimalist-friendly ~1.5KB router for React", | ||
@@ -16,23 +16,23 @@ "type": "module", | ||
| "files": [ | ||
| "esm", | ||
| "src", | ||
| "types/**/*.d.ts", | ||
| "types/*.d.ts" | ||
| ], | ||
| "main": "esm/index.js", | ||
| "main": "src/index.js", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./types/index.d.ts", | ||
| "default": "./esm/index.js" | ||
| "default": "./src/index.js" | ||
| }, | ||
| "./use-browser-location": { | ||
| "types": "./types/use-browser-location.d.ts", | ||
| "default": "./esm/use-browser-location.js" | ||
| "default": "./src/use-browser-location.js" | ||
| }, | ||
| "./use-hash-location": { | ||
| "types": "./types/use-hash-location.d.ts", | ||
| "default": "./esm/use-hash-location.js" | ||
| "default": "./src/use-hash-location.js" | ||
| }, | ||
| "./memory-location": { | ||
| "types": "./types/memory-location.d.ts", | ||
| "default": "./esm/memory-location.js" | ||
| "default": "./src/memory-location.js" | ||
| } | ||
@@ -58,5 +58,3 @@ }, | ||
| "scripts": { | ||
| "build": "rollup -c", | ||
| "watch": "rollup -c -w", | ||
| "prepublishOnly": "npm run build && cp ../../README.md ." | ||
| "prepublishOnly": "cp ../../README.md ." | ||
| }, | ||
@@ -63,0 +61,0 @@ "author": "Alexey Taktarov <molefrog@gmail.com>", |
+35
-7
@@ -10,3 +10,3 @@ <div align="center"> | ||
| <a href="https://travis-ci.org/molefrog/wouter"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/molefrog/wouter/size.yml?color=black&labelColor=888&label=2.5KB+limit" /></a> | ||
| <a href="https://codecov.io/gh/molefrog/wouter"><img alt="Coverage" src="https://img.shields.io/codecov/c/github/molefrog/wouter.svg?color=black&labelColor=888" /></a> | ||
| <a href="https://coveralls.io/github/molefrog/wouter?branch=v3"><img alt="Coverage" src="https://img.shields.io/coveralls/github/molefrog/wouter/v3.svg?color=black&labelColor=888" /></a> | ||
| <a href="https://www.npmjs.com/package/wouter"><img alt="Coverage" src="https://img.shields.io/npm/dm/wouter.svg?color=black&labelColor=888" /></a> | ||
@@ -149,3 +149,3 @@ <a href="https://pr.new/molefrog/wouter"><img alt="Edit in StackBlitz IDE" src="https://img.shields.io/badge/StackBlitz-New%20PR-black?labelColor=888" /></a> | ||
| These can be used separately from the main module and have an interface similar to `useState`. These hooks don't support nesting, base path, route matching. | ||
| These can be used separately from the main module and have an interface similar to `useState`. These hooks are standalone and don't include built-in support for nesting, base path, or route matching. However, when passed to `<Router>`, they work seamlessly with all Router features including nesting and base paths. | ||
@@ -366,3 +366,3 @@ - **[`import { useBrowserLocation } from "wouter/use-browser-location"`](https://github.com/molefrog/wouter/blob/v3/packages/wouter/src/use-browser-location.js)** — | ||
| Allow you to get and set any search parameters. The first returned value is a `URLSearchParams` object and the second returned value is a setter that accepts a `URLSearchParams` object with options. | ||
| Returns a `URLSearchParams` object and a setter function to update search parameters. The setter accepts either a value (object, URLSearchParams, string[][], etc.) or a **callback function** that receives the current params and must return the new params. | ||
@@ -380,2 +380,3 @@ ```jsx | ||
| prev.set('tab', 'settings'); | ||
| return prev; | ||
| }); | ||
@@ -394,2 +395,3 @@ | ||
| prev.set('order', 'desc'); | ||
| return prev; | ||
| }, | ||
@@ -405,2 +407,3 @@ { | ||
| prev.set('foo', 'bar'); | ||
| return prev; | ||
| }, | ||
@@ -757,3 +760,3 @@ { | ||
| **[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-strict-routes-w3xdtz)** | ||
| **[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-strict-routes-w3xdtz)** | ||
@@ -779,3 +782,3 @@ ### Are relative routes and links supported? | ||
| **[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-nested-routes-l8p23s)** | ||
| **[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-nested-routes-l8p23s)** | ||
@@ -929,6 +932,6 @@ ### Can I initiate navigation from outside a component? | ||
| // even if you call `navigate` somewhere in the app location won't change | ||
| const { hook } = memoryLocation({ path: "/user/2", static: true }); | ||
| const { hook, searchHook } = memoryLocation({ path: "/user/2", static: true }); | ||
| const { container } = render( | ||
| <Router hook={hook}> | ||
| <Router hook={hook} searchHook={searchHook}> | ||
| <Route path="/user/:id">{(params) => <>User ID: {params.id}</>}</Route> | ||
@@ -942,2 +945,16 @@ </Router> | ||
| **Note:** When you pass a `hook` prop to `Router`, it will automatically inherit the `searchHook` from the hook if available (via `hook.searchHook`). This means you don't need to explicitly pass both `hook` and `searchHook` when using `memoryLocation` - just passing `hook` is enough for `useSearch()` to work correctly with query parameters. | ||
| ```jsx | ||
| it("works with query parameters", () => { | ||
| const { hook } = memoryLocation({ path: "/products?sort=price&order=asc" }); | ||
| const { result } = renderHook(() => useSearch(), { | ||
| wrapper: ({ children }) => <Router hook={hook}>{children}</Router>, | ||
| }); | ||
| expect(result.current).toBe("sort=price&order=asc"); | ||
| }); | ||
| ``` | ||
| The hook can be configured to record navigation history. Additionally, it comes with a `navigate` function for external navigation. | ||
@@ -995,2 +1012,13 @@ | ||
| ## Contributing | ||
| **Architecture principles:** | ||
| - All code is written in JavaScript for full control over size optimization | ||
| - TypeScript definitions are maintained separately in `types/` directories | ||
| - `wouter-preact` reuses the same source except for `react-deps.js` (Preact-specific hooks) | ||
| - Type definitions are duplicated between packages (not ideal, but works for now) | ||
| **Development:** Tests run directly from source files (no build required). Run `npm run test` for interactive mode or `npm run test -- --run` for a single run. Use `npm run build` to build the distributable package before publishing. | ||
| ## Acknowledgements | ||
@@ -997,0 +1025,0 @@ |
+2
-1
@@ -13,2 +13,3 @@ // Minimum TypeScript Version: 4.1 | ||
| MouseEventHandler, | ||
| JSXElementConstructor, | ||
| } from "react"; | ||
@@ -83,3 +84,3 @@ | ||
| path?: RoutePath; | ||
| component?: ComponentType< | ||
| component?: JSXElementConstructor< | ||
| RouteComponentProps< | ||
@@ -86,0 +87,0 @@ T extends DefaultParams |
@@ -11,7 +11,11 @@ /* | ||
| 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]; | ||
| export type BaseLocationHook = { | ||
| (...args: any[]): [Path, (path: Path, ...args: any[]) => any]; | ||
| searchHook?: BaseSearchHook; | ||
| hrefs?: HrefsFormatter; | ||
| }; | ||
@@ -18,0 +22,0 @@ export type BaseSearchHook = (...args: any[]) => SearchString; |
@@ -6,2 +6,3 @@ import { | ||
| BaseSearchHook, | ||
| HrefsFormatter, | ||
| } from "./location-hook.js"; | ||
@@ -14,4 +15,2 @@ | ||
| export type HrefsFormatter = (href: string, router: RouterObject) => string; | ||
| // the object returned from `useRouter` | ||
@@ -18,0 +17,0 @@ export interface RouterObject { |
-382
| import { parse } from 'regexparam'; | ||
| import { useBrowserLocation, useSearch as useSearch$1 } from './use-browser-location.js'; | ||
| import { createContext, forwardRef, useEvent, isValidElement, cloneElement, createElement, useContext, useRef, useMemo, useIsomorphicLayoutEffect, Fragment } from './react-deps.js'; | ||
| /* | ||
| * Transforms `path` into its relative `base` version | ||
| * If base isn't part of the path provided returns absolute path e.g. `~/app` | ||
| */ | ||
| const _relativePath = (base, path) => | ||
| !path.toLowerCase().indexOf(base.toLowerCase()) | ||
| ? path.slice(base.length) || "/" | ||
| : "~" + path; | ||
| /** | ||
| * When basepath is `undefined` or '/' it is ignored (we assume it's empty string) | ||
| */ | ||
| const baseDefaults = (base = "") => (base === "/" ? "" : base); | ||
| const absolutePath = (to, base) => | ||
| to[0] === "~" ? to.slice(1) : baseDefaults(base) + to; | ||
| const relativePath = (base = "", path) => | ||
| _relativePath(unescape(baseDefaults(base)), unescape(path)); | ||
| /* | ||
| * Removes leading question mark | ||
| */ | ||
| const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str); | ||
| /* | ||
| * decodes escape sequences such as %20 | ||
| */ | ||
| const unescape = (str) => { | ||
| try { | ||
| return decodeURI(str); | ||
| } catch (_e) { | ||
| // fail-safe mode: if string can't be decoded do nothing | ||
| return str; | ||
| } | ||
| }; | ||
| const sanitizeSearch = (search) => unescape(stripQm(search)); | ||
| /* | ||
| * 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: useSearch$1, | ||
| parser: parse, | ||
| 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, | ||
| }; | ||
| const RouterCtx = createContext(defaultRouter); | ||
| // gets the closest parent router from the context | ||
| const useRouter = () => useContext(RouterCtx); | ||
| /** | ||
| * Parameters context. Used by `useParams()` to get the | ||
| * matched params from the innermost `Route` component. | ||
| */ | ||
| const Params0 = {}, | ||
| ParamsCtx = createContext(Params0); | ||
| const useParams = () => useContext(ParamsCtx); | ||
| /* | ||
| * Part 1, Hooks API: useRoute and useLocation | ||
| */ | ||
| // Internal version of useLocation to avoid redundant useRouter calls | ||
| 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, navOpts) => navigate(absolutePath(to, router.base), navOpts)), | ||
| ]; | ||
| }; | ||
| const useLocation = () => useLocationFromRouter(useRouter()); | ||
| const useSearch = () => { | ||
| const router = useRouter(); | ||
| return sanitizeSearch(router.searchHook(router)); | ||
| }; | ||
| const matchRoute = (parser, route, path, loose) => { | ||
| // if the input is a regexp, skip parsing | ||
| const { pattern, keys } = | ||
| route instanceof RegExp | ||
| ? { keys: false, pattern: route } | ||
| : parser(route || "*", loose); | ||
| // array destructuring loses keys, so this is done in two steps | ||
| const result = pattern.exec(path) || []; | ||
| // when parser is in "loose" mode, `$base` is equal to the | ||
| // first part of the route that matches the pattern | ||
| // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`) | ||
| // we use this for route nesting | ||
| const [$base, ...matches] = result; | ||
| return $base !== undefined | ||
| ? [ | ||
| true, | ||
| (() => { | ||
| // for regex paths, `keys` will always be false | ||
| // an object with parameters matched, e.g. { foo: "bar" } for "/:foo" | ||
| // we "zip" two arrays here to construct the object | ||
| // ["foo"], ["bar"] → { foo: "bar" } | ||
| const groups = | ||
| keys !== false | ||
| ? Object.fromEntries(keys.map((key, i) => [key, matches[i]])) | ||
| : result.groups; | ||
| // convert the array to an instance of object | ||
| // this makes it easier to integrate with the existing param implementation | ||
| let obj = { ...matches }; | ||
| // merge named capture groups with matches array | ||
| groups && Object.assign(obj, groups); | ||
| return obj; | ||
| })(), | ||
| // the third value if only present when parser is in "loose" mode, | ||
| // so that we can extract the base path for nested routes | ||
| ...(loose ? [$base] : []), | ||
| ] | ||
| : [false, null]; | ||
| }; | ||
| const useRoute = (pattern) => | ||
| matchRoute(useRouter().parser, pattern, useLocation()[0]); | ||
| /* | ||
| * Part 2, Low Carb Router API: Router, Route, Link, Switch | ||
| */ | ||
| 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 | ||
| const [path, search] = props.ssrPath?.split("?") ?? []; | ||
| if (search) (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; | ||
| // 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({}), | ||
| 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 (prev === next && option !== next[k]) { | ||
| 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 createElement(RouterCtx.Provider, { value, children }); | ||
| }; | ||
| const h_route = ({ children, component }, params) => { | ||
| // React-Router style `component` prop | ||
| if (component) return createElement(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; | ||
| return (prev.current = | ||
| // Update cache if number of params changed or any value changed | ||
| Object.keys(value).length !== Object.keys(curr).length || | ||
| Object.entries(value).some(([k, v]) => v !== curr[k]) | ||
| ? value // Return new value if there are changes | ||
| : curr); // Return cached value if nothing changed | ||
| }; | ||
| 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 | ||
| ); | ||
| navigate(location + "?" + tempSearchParams, options); | ||
| }); | ||
| return [searchParams, setSearchParams]; | ||
| } | ||
| const Route = ({ path, nest, match, ...renderProps }) => { | ||
| const router = useRouter(); | ||
| const [location] = useLocationFromRouter(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 = base | ||
| ? createElement(Router, { base }, h_route(renderProps, params)) | ||
| : h_route(renderProps, params); | ||
| return createElement(ParamsCtx.Provider, { value: params, children }); | ||
| }; | ||
| 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 */, | ||
| /* eslint-enable no-unused-vars */ | ||
| ...restProps | ||
| } = props; | ||
| const 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 | ||
| const href = router.hrefs( | ||
| targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath, | ||
| router // pass router as a second argument for convinience | ||
| ); | ||
| return asChild && isValidElement(children) | ||
| ? cloneElement(children, { onClick, href }) | ||
| : createElement("a", { | ||
| ...restProps, | ||
| onClick, | ||
| href, | ||
| // `className` can be a function to apply the class if this link is active | ||
| className: cls?.call ? cls(currentPath === targetPath) : cls, | ||
| children, | ||
| ref, | ||
| }); | ||
| }); | ||
| const flattenChildren = (children) => | ||
| Array.isArray(children) | ||
| ? children.flatMap((c) => | ||
| flattenChildren(c && c.type === Fragment ? c.props.children : c) | ||
| ) | ||
| : [children]; | ||
| const Switch = ({ children, location }) => { | ||
| const router = useRouter(); | ||
| const [originalLocation] = useLocationFromRouter(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; | ||
| }; | ||
| 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; | ||
| }; | ||
| export { Link, Redirect, Route, Router, Switch, matchRoute, useLocation, useParams, useRoute, useRouter, useSearch, useSearchParams }; |
| import mitt from 'mitt'; | ||
| import { useSyncExternalStore } from './react-deps.js'; | ||
| /** | ||
| * In-memory location that supports navigation | ||
| */ | ||
| const memoryLocation = ({ | ||
| path = "/", | ||
| searchPath = "", | ||
| 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("?"); | ||
| const history = [initialPath]; | ||
| const emitter = mitt(); | ||
| const navigateImplementation = (path, { replace = false } = {}) => { | ||
| if (record) { | ||
| if (replace) { | ||
| history.splice(history.length - 1, 1, path); | ||
| } else { | ||
| history.push(path); | ||
| } | ||
| } | ||
| [currentPath, currentSearch = ""] = path.split("?"); | ||
| emitter.emit("navigate", path); | ||
| }; | ||
| const navigate = !staticLocation ? navigateImplementation : () => null; | ||
| const subscribe = (cb) => { | ||
| emitter.on("navigate", cb); | ||
| return () => emitter.off("navigate", cb); | ||
| }; | ||
| const useMemoryLocation = () => [ | ||
| useSyncExternalStore(subscribe, () => currentPath), | ||
| navigate, | ||
| ]; | ||
| const useMemoryQuery = () => | ||
| useSyncExternalStore(subscribe, () => currentSearch); | ||
| function reset() { | ||
| // clean history array with mutation to preserve link | ||
| history.splice(0, history.length); | ||
| navigateImplementation(initialPath); | ||
| } | ||
| return { | ||
| hook: useMemoryLocation, | ||
| searchHook: useMemoryQuery, | ||
| navigate, | ||
| history: record ? history : undefined, | ||
| reset: record ? reset : undefined, | ||
| }; | ||
| }; | ||
| export { memoryLocation }; |
| import * as React from 'react'; | ||
| export { Fragment, cloneElement, createContext, createElement, forwardRef, isValidElement, useContext, useMemo, useRef, useState } from 'react'; | ||
| export { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'; | ||
| // React.useInsertionEffect is not available in React <18 | ||
| // This hack fixes a transpilation issue on some apps | ||
| const useBuiltinInsertionEffect = React["useInsertion" + "Effect"]; | ||
| // 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." | ||
| 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 | ||
| 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. | ||
| 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]; | ||
| }; | ||
| export { useEvent, useInsertionEffect, useIsomorphicLayoutEffect }; |
| import { useSyncExternalStore } from './react-deps.js'; | ||
| /** | ||
| * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History | ||
| */ | ||
| const eventPopstate = "popstate"; | ||
| const eventPushState = "pushState"; | ||
| const eventReplaceState = "replaceState"; | ||
| const eventHashchange = "hashchange"; | ||
| const events = [ | ||
| eventPopstate, | ||
| eventPushState, | ||
| eventReplaceState, | ||
| eventHashchange, | ||
| ]; | ||
| const subscribeToLocationUpdates = (callback) => { | ||
| for (const event of events) { | ||
| addEventListener(event, callback); | ||
| } | ||
| return () => { | ||
| for (const event of events) { | ||
| removeEventListener(event, callback); | ||
| } | ||
| }; | ||
| }; | ||
| const useLocationProperty = (fn, ssrFn) => | ||
| useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn); | ||
| const currentSearch = () => location.search; | ||
| const useSearch = ({ ssrSearch = "" } = {}) => | ||
| useLocationProperty(currentSearch, () => ssrSearch); | ||
| const currentPathname = () => location.pathname; | ||
| const usePathname = ({ ssrPath } = {}) => | ||
| useLocationProperty( | ||
| currentPathname, | ||
| ssrPath ? () => ssrPath : currentPathname | ||
| ); | ||
| const currentHistoryState = () => history.state; | ||
| const useHistoryState = () => | ||
| useLocationProperty(currentHistoryState, () => null); | ||
| const navigate = (to, { replace = false, state = null } = {}) => | ||
| history[replace ? eventReplaceState : eventPushState](state, "", to); | ||
| // the 2nd argument of the `useBrowserLocation` return value is a function | ||
| // that allows to perform a navigation. | ||
| 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 [eventPushState, eventReplaceState]) { | ||
| 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 }); | ||
| } | ||
| export { navigate, useBrowserLocation, useHistoryState, useLocationProperty, usePathname, useSearch }; |
| import { useSyncExternalStore } from './react-deps.js'; | ||
| // array of callback subscribed to hash updates | ||
| const listeners = { | ||
| v: [], | ||
| }; | ||
| const onHashChange = () => listeners.v.forEach((cb) => cb()); | ||
| // we subscribe to `hashchange` only once when needed to guarantee that | ||
| // all listeners are called synchronously | ||
| const subscribeToHashUpdates = (callback) => { | ||
| if (listeners.v.push(callback) === 1) | ||
| addEventListener("hashchange", onHashChange); | ||
| return () => { | ||
| listeners.v = listeners.v.filter((i) => i !== callback); | ||
| if (!listeners.v.length) removeEventListener("hashchange", onHashChange); | ||
| }; | ||
| }; | ||
| // leading '#' is ignored, leading '/' is optional | ||
| const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, ""); | ||
| const navigate = (to, { state = null, replace = false } = {}) => { | ||
| const [hash, search] = to.replace(/^#?\/?/, "").split("?"); | ||
| const newRelativePath = | ||
| location.pathname + (search ? `?${search}` : location.search) + `#/${hash}`; | ||
| const oldURL = location.href; | ||
| const newURL = new URL(newRelativePath, location.origin).href; | ||
| if (replace) { | ||
| history.replaceState(state, "", newRelativePath); | ||
| } else { | ||
| history.pushState(state, "", newRelativePath); | ||
| } | ||
| const event = | ||
| typeof HashChangeEvent !== "undefined" | ||
| ? new HashChangeEvent("hashchange", { oldURL, newURL }) | ||
| : new Event("hashchange", { detail: { oldURL, newURL } }); | ||
| dispatchEvent(event); | ||
| }; | ||
| const useHashLocation = ({ ssrPath = "/" } = {}) => [ | ||
| useSyncExternalStore( | ||
| subscribeToHashUpdates, | ||
| currentHashLocation, | ||
| () => ssrPath | ||
| ), | ||
| navigate, | ||
| ]; | ||
| useHashLocation.hrefs = (href) => "#" + href; | ||
| export { navigate, useHashLocation }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
70789
3.68%20
53.85%882
5.5%1019
2.83%2
100%1
Infinity%