"use client"; import { useContext, useState, useEffect, useRef } from "react"; import { NavigationStoreContext } from "./context.js"; import { shallowEqual } from "./shallow-equal.js"; const EMPTY_PARAMS: Record = Object.freeze({}); /** * Hook to access the current route params. * * Returns the merged route params from the matched route. * Updates when navigation completes, not during pending navigation. * * @example * ```tsx * // Route: /products/:productId * const params = useParams(); * // { productId: "123" } * * // Annotate the expected shape via a generic * const { productId } = useParams<{ productId: string }>(); * * // With selector * const productId = useParams(p => p.productId); * ``` */ // `T extends object` (not `Record`) so that // interface shapes pass the constraint — interfaces lack an implicit // index signature and would otherwise be rejected. The generic is a // shape annotation, not a runtime check; the body always returns the // underlying params map unchanged. The default and selector input use // `string | undefined` because absent optional params are omitted from // the params record at runtime — the type must reflect that so callers // don't write `p.locale.length` and crash when the segment is absent. export function useParams< T extends object = Record, >(): Readonly; export function useParams( selector: (params: Record) => T, ): T; export function useParams( selector?: (params: Record) => T, ): T | Record { const ctx = useContext(NavigationStoreContext); const [value, setValue] = useState>(() => { const params = ctx ? ctx.eventController.getParams() : EMPTY_PARAMS; return selector ? selector(params) : params; }); const prevValue = useRef(value); // Ref keeps the latest selector without re-subscribing. Event-driven by // design: value updates on store events, not on selector identity change. const selectorRef = useRef(selector); selectorRef.current = selector; useEffect(() => { if (!ctx) return; const update = () => { const params = ctx.eventController.getParams(); const next = selectorRef.current ? selectorRef.current(params) : params; if (!shallowEqual(next, prevValue.current)) { prevValue.current = next; setValue(next); } }; update(); return ctx.eventController.subscribe(update); }, []); return value; }