state-in-url 6.2.4 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { type JSONCompatible, type Simple } from "../utils";
1
+ import { type JSONCompatible, type Simple } from '../utils';
2
2
  /**
3
3
  * Encode any JSON serializable value to URL friendly string
4
4
  *
@@ -1 +1 @@
1
- "use strict";var n=require("../constants/constants.js");var u=require("../utils.js");const O=/['"]/g;const p={"'":"%27",'"':"'"};const v=/'|%27/g;const y={"'":'"',"%27":"'"};const g=new RegExp(`^(${n.SYMBOLS.undefined}|${n.SYMBOLS.date})`);function m(e){if(typeof e==="function"||typeof e==="symbol")return"";if(B(e)){return e}return JSON.stringify(e,c).replace(O,r=>p[r])}function c(e,r){const t=u.typeOf(r);if(t!=="object"&&t!=="array"){return d(r)}if(t==="object"){const i=r;const o={};for(const s of Object.keys(i)){o[s]=c(e,i[s])}return o}if(t==="array"){return r.map(i=>c(e,i))}return r}const d=e=>{switch(u.typeOf(e)){case"date":return n.SYMBOLS.date+new Date(e).toISOString();case"undefined":return n.SYMBOLS.undefined;default:return e}};function l(e,r){return f(e.replace(v,t=>y[t]),r)}function f(e,r){try{return JSON.parse(e,a)}catch{return r}}function a(e,r){return typeof r==="string"?S(r):r}const S=e=>{if(e===n.SYMBOLS.undefined)return void 0;if(e?.startsWith?.(n.SYMBOLS.date))return new Date(e.slice(1));return e};const B=e=>g.test(String(e));exports.decode=l;exports.decodePrimitive=S;exports.encode=m;exports.encodePrimitive=d;exports.parseJSON=f;exports.reviver=a;
1
+ "use strict";var n=require("../constants/constants.js");var u=require("../utils.js");const O=/['"]/g;const p={"'":"%27",'"':"'"};const v=/'|%27/g;const y={"'":'"',"%27":"'"};const g=new RegExp(`^(${n.SYMBOLS.undefined}|${n.SYMBOLS.date})`);function m(e){if(typeof e==="function"||typeof e==="symbol")return"";if(B(e)){return e}return JSON.stringify(c("",e)).replace(O,r=>p[r])}function c(e,r){const t=u.typeOf(r);if(t!=="object"&&t!=="array"){return d(r)}if(t==="object"){const i=r;const o={};for(const s of Object.keys(i)){o[s]=c(e,i[s])}return o}if(t==="array"){return r.map(i=>c(e,i))}return r}const d=e=>{switch(u.typeOf(e)){case"date":return n.SYMBOLS.date+new Date(e).toISOString();case"undefined":return n.SYMBOLS.undefined;default:return e}};function l(e,r){return f(e.replace(v,t=>y[t]),r)}function f(e,r){try{return JSON.parse(e,a)}catch{return r}}function a(e,r){return typeof r==="string"?S(r):r}const S=e=>{if(e===n.SYMBOLS.undefined)return void 0;if(e?.startsWith?.(n.SYMBOLS.date))return new Date(e.slice(1));return e};const B=e=>g.test(String(e));exports.decode=l;exports.decodePrimitive=S;exports.encode=m;exports.encodePrimitive=d;exports.parseJSON=f;exports.reviver=a;
@@ -1 +1 @@
1
- import{SYMBOLS as n}from"../constants/constants.mjs";import{typeOf as u}from"../utils.mjs";const y=/['"]/g;const m={"'":"%27",'"':"'"};const O=/'|%27/g;const S={"'":'"',"%27":"'"};const g=new RegExp(`^(${n.undefined}|${n.date})`);function l(t){if(typeof t==="function"||typeof t==="symbol")return"";if(b(t)){return t}return JSON.stringify(t,i).replace(y,r=>m[r])}function i(t,r){const e=u(r);if(e!=="object"&&e!=="array"){return s(r)}if(e==="object"){const o=r;const c={};for(const f of Object.keys(o)){c[f]=i(t,o[f])}return c}if(e==="array"){return r.map(o=>i(t,o))}return r}const s=t=>{switch(u(t)){case"date":return n.date+new Date(t).toISOString();case"undefined":return n.undefined;default:return t}};function v(t,r){return d(t.replace(O,e=>S[e]),r)}function d(t,r){try{return JSON.parse(t,a)}catch{return r}}function a(t,r){return typeof r==="string"?p(r):r}const p=t=>{if(t===n.undefined)return void 0;if(t?.startsWith?.(n.date))return new Date(t.slice(1));return t};const b=t=>g.test(String(t));export{v as decode,p as decodePrimitive,l as encode,s as encodePrimitive,d as parseJSON,a as reviver};
1
+ import{SYMBOLS as n}from"../constants/constants.mjs";import{typeOf as u}from"../utils.mjs";const y=/['"]/g;const m={"'":"%27",'"':"'"};const O=/'|%27/g;const S={"'":'"',"%27":"'"};const g=new RegExp(`^(${n.undefined}|${n.date})`);function l(t){if(typeof t==="function"||typeof t==="symbol")return"";if(b(t)){return t}return JSON.stringify(i("",t)).replace(y,r=>m[r])}function i(t,r){const e=u(r);if(e!=="object"&&e!=="array"){return s(r)}if(e==="object"){const o=r;const c={};for(const f of Object.keys(o)){c[f]=i(t,o[f])}return c}if(e==="array"){return r.map(o=>i(t,o))}return r}const s=t=>{switch(u(t)){case"date":return n.date+new Date(t).toISOString();case"undefined":return n.undefined;default:return t}};function v(t,r){return d(t.replace(O,e=>S[e]),r)}function d(t,r){try{return JSON.parse(t,a)}catch{return r}}function a(t,r){return typeof r==="string"?p(r):r}const p=t=>{if(t===n.undefined)return void 0;if(t?.startsWith?.(n.date))return new Date(t.slice(1));return t};const b=t=>g.test(String(t));export{v as decode,p as decodePrimitive,l as encode,s as encodePrimitive,d as parseJSON,a as reviver};
@@ -1,5 +1,5 @@
1
- import { useRouter } from "next/navigation";
2
- import { type JSONCompatible } from "../../utils";
1
+ import { useRouter } from 'next/navigation';
2
+ import { type JSONCompatible } from '../../utils';
3
3
  /**
4
4
  * NextJS hook. Returns `urlState`, `setState`, and `setUrl` functions
5
5
  *
@@ -79,7 +79,7 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, p
79
79
  }) => void;
80
80
  };
81
81
  type Router = ReturnType<typeof useRouter>;
82
- type RouterOptions = NonNullable<Parameters<Router["push"]>[1] | Parameters<Router["replace"]>[1]>;
82
+ type RouterOptions = NonNullable<Parameters<Router['push']>[1] | Parameters<Router['replace']>[1]>;
83
83
  export interface Options extends RouterOptions {
84
84
  replace?: boolean;
85
85
  }
@@ -1 +1 @@
1
- "use strict";var g=require("react");var p=require("next/navigation");var d=require("../../parseSPObj.js");var k=require("../../useUrlStateBase/useUrlStateBase.js");var s=require("../../utils.js");function q(t){return t&&t.__esModule?t:{default:t}}var a=q(g);function M(t,r){const u=a.default.useMemo(()=>r?.useHistory===void 0?true:!!r?.useHistory,[]);const l=p.useRouter();const S=a.default.useMemo(()=>({push:(...e)=>{if(u){s.routerHistory.push(...e)}else{l.push(...e)}},replace:(...e)=>{if(u){s.routerHistory.replace(...e)}else{l.replace(...e)}}}),[l]);const{state:U,updateState:o,updateUrl:n,reset:c,getState:m,pendingUrlUpdate:v}=k.useUrlStateBase(t,S,({parse:e})=>{return s.isSSR?d.parseSPObj(s.filterUnknownParams(t,r?.searchParams),t):e(s.filterUnknownParamsClient(t,r?.searchParams||s.getSearch()))});const i=p.useSearchParams();a.default.useEffect(()=>{if(!v()){o(s.filterUnknownParams(t,d.parseSPObj(Object.fromEntries([...i.entries()]),t)))}},[i]);const f=a.default.useMemo(()=>{const e={..._};if(r?.scroll!==void 0)e.scroll=r.scroll;if(r?.replace!==void 0)e.replace=r.replace;return e},[r?.scroll,r?.replace]);const P=a.default.useCallback((e,b)=>n(e,{...f,...b}),[n]);const h=a.default.useCallback(e=>{c({...f,...e})},[c]);return{setState:o,setUrl:P,urlState:U,reset:h,getState:m}}const _={replace:true,scroll:false};exports.useUrlState=M;
1
+ "use strict";var g=require("react");var h=require("next/navigation");var p=require("../../parseSPObj.js");var k=require("../../useUrlStateBase/useUrlStateBase.js");var a=require("../../utils.js");function q(t){return t&&t.__esModule?t:{default:t}}var s=q(g);function y(t,r){const o=s.default.useMemo(()=>r?.useHistory===void 0?true:!!r?.useHistory,[]);const l=h.useRouter();const d=s.default.useMemo(()=>({push:(...e)=>{if(o){a.routerHistory.push(...e)}else{l.push(...e)}},replace:(...e)=>{if(o){a.routerHistory.replace(...e)}else{l.replace(...e)}}}),[l]);const{state:S,updateState:n,updateUrl:c,reset:i,getState:U,pendingUrlUpdate:b}=k.useUrlStateBase(t,d,({parse:e})=>{return a.isSSR?p.parseSPObj(a.filterUnknownParams(t,r?.searchParams),t):e(a.filterUnknownParamsClient(t,r?.searchParams||a.getSearch()))});const u=s.default.useCallback(()=>{if(b())return;n(a.filterUnknownParams(t,p.parseSPObj(Object.fromEntries(a.getParams(a.getSearch()).entries()),t)))},[]);s.default.useEffect(()=>{u();return a.subscribeToUrl(u)},[u]);const f=s.default.useMemo(()=>{const e={..._};if(r?.scroll!==void 0)e.scroll=r.scroll;if(r?.replace!==void 0)e.replace=r.replace;return e},[r?.scroll,r?.replace]);const m=s.default.useCallback((e,P)=>c(e,{...f,...P}),[c]);const v=s.default.useCallback(e=>{i({...f,...e})},[i]);return{setState:n,setUrl:m,urlState:S,reset:v,getState:U}}const _={replace:true,scroll:false};exports.useUrlState=y;
@@ -1 +1 @@
1
- import r from"react";import{useRouter as k,useSearchParams as M}from"next/navigation";import{parseSPObj as p}from"../../parseSPObj.mjs";import{useUrlStateBase as v}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as m,filterUnknownParams as f,isSSR as y,filterUnknownParamsClient as C,getSearch as H}from"../../utils.mjs";function j(t,s){const a=r.useMemo(()=>s?.useHistory===void 0?true:!!s?.useHistory,[]);const o=k();const S=r.useMemo(()=>({push:(...e)=>{if(a){m.push(...e)}else{o.push(...e)}},replace:(...e)=>{if(a){m.replace(...e)}else{o.replace(...e)}}}),[o]);const{state:U,updateState:l,updateUrl:c,reset:n,getState:d,pendingUrlUpdate:h}=v(t,S,({parse:e})=>{return y?p(f(t,s?.searchParams),t):e(C(t,s?.searchParams||H()))});const u=M();r.useEffect(()=>{if(!h()){l(f(t,p(Object.fromEntries([...u.entries()]),t)))}},[u]);const i=r.useMemo(()=>{const e={...w};if(s?.scroll!==void 0)e.scroll=s.scroll;if(s?.replace!==void 0)e.replace=s.replace;return e},[s?.scroll,s?.replace]);const P=r.useCallback((e,g)=>c(e,{...i,...g}),[c]);const b=r.useCallback(e=>{n({...i,...e})},[n]);return{setState:l,setUrl:P,urlState:U,reset:b,getState:d}}const w={replace:true,scroll:false};export{j as useUrlState};
1
+ import r from"react";import{useRouter as y}from"next/navigation";import{parseSPObj as p}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as m,filterUnknownParams as f,getParams as v,subscribeToUrl as H,isSSR as M,filterUnknownParamsClient as j,getSearch as S}from"../../utils.mjs";function w(s,t){const l=r.useMemo(()=>t?.useHistory===void 0?true:!!t?.useHistory,[]);const o=y();const U=r.useMemo(()=>({push:(...e)=>{if(l){m.push(...e)}else{o.push(...e)}},replace:(...e)=>{if(l){m.replace(...e)}else{o.replace(...e)}}}),[o]);const{state:b,updateState:c,updateUrl:u,reset:n,getState:d,pendingUrlUpdate:h}=C(s,U,({parse:e})=>{return M?p(f(s,t?.searchParams),s):e(j(s,t?.searchParams||S()))});const a=r.useCallback(()=>{if(h())return;c(f(s,p(Object.fromEntries(v(S()).entries()),s)))},[]);r.useEffect(()=>{a();return H(a)},[a]);const i=r.useMemo(()=>{const e={...E};if(t?.scroll!==void 0)e.scroll=t.scroll;if(t?.replace!==void 0)e.replace=t.replace;return e},[t?.scroll,t?.replace]);const P=r.useCallback((e,k)=>u(e,{...i,...k}),[u]);const g=r.useCallback(e=>{n({...i,...e})},[n]);return{setState:c,setUrl:P,urlState:b,reset:g,getState:d}}const E={replace:true,scroll:false};export{w as useUrlState};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "6.2.4",
3
+ "version": "6.3.0",
4
4
  "description": "Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15, react-router@6-7, and remix@2.",
5
5
  "homepage": "https://state-in-url.dev",
6
6
  "repository": {
@@ -106,6 +106,8 @@
106
106
  "build": "wireit",
107
107
  "build:demo": "wireit",
108
108
  "start:demo": "wireit",
109
+ "lighthouse:serve": "cd packages/example-nextjs16 && pnpm exec next start --port 3012",
110
+ "test:lighthouse": "playwright test -c playwright.lighthouse.config.ts",
109
111
  "build:packages": "wireit",
110
112
  "start:demo15": "wireit",
111
113
  "start:demo15:ci": "wireit",
@@ -125,7 +127,7 @@
125
127
  "reinstall": "pnpm run cleanup && pnpm install",
126
128
  "setup": "playwright install --with-deps",
127
129
  "prepack": "pnpm run build && cp package.json dist/package.json",
128
- "pin-gh-deps": "pin-github-action -i .github/workflows/tests.yml",
130
+ "pin-gh-deps": "pin-github-action -i .github/workflows/tests.yml .github/workflows/lighthouse.yml",
129
131
  "prepare": "pnpm exec husky"
130
132
  },
131
133
  "wireit": {
@@ -472,9 +474,9 @@
472
474
  "@rollup/plugin-typescript": "^12.1.4",
473
475
  "@semantic-release/changelog": "^6.0.3",
474
476
  "@semantic-release/commit-analyzer": "^13.0.1",
477
+ "@semantic-release/exec": "^7.1.0",
475
478
  "@semantic-release/git": "^10.0.1",
476
479
  "@semantic-release/npm": "^13.1.3",
477
- "@semantic-release/exec": "^7.1.0",
478
480
  "@semantic-release/release-notes-generator": "^14.1.0",
479
481
  "@tanstack/intent": "^0.3.6",
480
482
  "@testing-library/dom": "^10.4.1",
@@ -504,12 +506,14 @@
504
506
  "glob": "^13.0.0",
505
507
  "happy-dom": "^20.0.2",
506
508
  "husky": "^9.1.7",
509
+ "lighthouse": "^13.4.1",
507
510
  "lint-staged": "^15.5.2",
508
511
  "next": "^15.5.19",
509
512
  "nyc": "^17.1.0",
510
513
  "only-allow": "^1.2.1",
511
514
  "pin-github-action": "^1.9.1",
512
515
  "playwright": "^1.55.1",
516
+ "playwright-lighthouse": "^4.0.0",
513
517
  "prettier": "^3.6.2",
514
518
  "prettier-plugin-tailwindcss": "^0.6.14",
515
519
  "react": "^19.2.6",
@@ -1 +1 @@
1
- "use strict";var P=require("react");var o=require("react-router");var _=require("../../parseSPObj.js");var k=require("../../useUrlStateBase/useUrlStateBase.js");var u=require("../../utils.js");function R(e){return e&&e.__esModule?e:{default:e}}var a=R(P);function h(e,l){const S=l?.useHistory;const s=a.default.useMemo(()=>({...j,replace:l?.replace,preventScrollReset:l?.preventScrollReset}),[]);const n=o.useNavigate();const d=a.default.useMemo(()=>S?u.routerHistory:{replace:(t,r)=>n(t,{...s,...r}),push:(t,r)=>n(t,{...s,...r})},[n]);const[c]=o.useSearchParams();const v=o.useHref("/");const{state:U,updateState:i,updateUrl:f,getState:m,reset:p,pendingUrlUpdate:b}=k.useUrlStateBase(e,d,({parse:t})=>t(u.filterUnknownParamsClient(e,c.entries())),v);const g=a.default.useCallback((t,r)=>f(t,{...s,...r}),[f]);a.default.useEffect(()=>{if(!b()){i(u.assignValue(e,u.filterUnknownParams(e,_.parseSPObj(Object.fromEntries([...c.entries()]),e))))}},[c]);const q=a.default.useCallback(t=>{p({...s,...t})},[p]);return{setState:i,setUrl:g,urlState:U,reset:q,getState:m}}const j={replace:true,preventScrollReset:true};exports.useUrlState=h;
1
+ "use strict";var P=require("react");var c=require("react-router");var _=require("../../parseSPObj.js");var k=require("../../useUrlStateBase/useUrlStateBase.js");var u=require("../../utils.js");function R(e){return e&&e.__esModule?e:{default:e}}var a=R(P);function h(e,l){const S=l?.useHistory;const s=a.default.useMemo(()=>({...j,replace:l?.replace,preventScrollReset:l?.preventScrollReset}),[]);const n=c.useNavigate();const d=a.default.useMemo(()=>S?u.routerHistory:{replace:(t,r)=>n(t,{...s,...r}),push:(t,r)=>n(t,{...s,...r})},[n]);const[o]=c.useSearchParams();const v=c.useHref("/");const{state:U,updateState:i,updateUrl:f,getState:m,reset:p,pendingUrlUpdate:b}=k.useUrlStateBase(e,d,({parse:t})=>t(u.filterUnknownParamsClient(e,o.entries())),v);const g=a.default.useCallback((t,r)=>f(t,{...s,...r}),[f]);a.default.useEffect(()=>{if(!b()){i(u.assignValue(e,u.filterUnknownParams(e,_.parseSPObj(Object.fromEntries([...o.entries()]),e))))}},[o]);const q=a.default.useCallback(t=>{p({...s,...t})},[p]);return{setState:i,setUrl:g,urlState:U,reset:q,getState:m}}const j={replace:true,preventScrollReset:true};exports.useUrlState=h;
@@ -1 +1 @@
1
- import s from"react";import{useNavigate as d,useSearchParams as k,useHref as P}from"react-router";import{parseSPObj as h}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as H,filterUnknownParamsClient as R,assignValue as j,filterUnknownParams as w}from"../../utils.mjs";function y(t,o){const i=o?.useHistory;const a=s.useMemo(()=>({...E,replace:o?.replace,preventScrollReset:o?.preventScrollReset}),[]);const n=d();const m=s.useMemo(()=>i?H:{replace:(e,r)=>n(e,{...a,...r}),push:(e,r)=>n(e,{...a,...r})},[n]);const[l]=k();const f=P("/");const{state:S,updateState:c,updateUrl:u,getState:U,reset:p,pendingUrlUpdate:g}=C(t,m,({parse:e})=>e(R(t,l.entries())),f);const b=s.useCallback((e,r)=>u(e,{...a,...r}),[u]);s.useEffect(()=>{if(!g()){c(j(t,w(t,h(Object.fromEntries([...l.entries()]),t))))}},[l]);const v=s.useCallback(e=>{p({...a,...e})},[p]);return{setState:c,setUrl:b,urlState:S,reset:v,getState:U}}const E={replace:true,preventScrollReset:true};export{y as useUrlState};
1
+ import s from"react";import{useNavigate as d,useSearchParams as k,useHref as P}from"react-router";import{parseSPObj as h}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as H,filterUnknownParamsClient as R,assignValue as j,filterUnknownParams as w}from"../../utils.mjs";function y(t,a){const i=a?.useHistory;const o=s.useMemo(()=>({...E,replace:a?.replace,preventScrollReset:a?.preventScrollReset}),[]);const n=d();const m=s.useMemo(()=>i?H:{replace:(e,r)=>n(e,{...o,...r}),push:(e,r)=>n(e,{...o,...r})},[n]);const[l]=k();const f=P("/");const{state:S,updateState:c,updateUrl:u,getState:U,reset:p,pendingUrlUpdate:g}=C(t,m,({parse:e})=>e(R(t,l.entries())),f);const b=s.useCallback((e,r)=>u(e,{...o,...r}),[u]);s.useEffect(()=>{if(!g()){c(j(t,w(t,h(Object.fromEntries([...l.entries()]),t))))}},[l]);const v=s.useCallback(e=>{p({...o,...e})},[p]);return{setState:c,setUrl:b,urlState:S,reset:v,getState:U}}const E={replace:true,preventScrollReset:true};export{y as useUrlState};
@@ -1 +1 @@
1
- "use strict";var O=require("react");var o=require("react-router-dom");var P=require("../../parseSPObj.js");var _=require("../../useUrlStateBase/useUrlStateBase.js");var l=require("../../utils.js");function k(t){return t&&t.__esModule?t:{default:t}}var a=k(O);function j(t,r){const d=r?.useHistory;const u=a.default.useMemo(()=>{const e={...S};if(r?.replace!==void 0)e.replace=r.replace;if(r?.preventScrollReset!==void 0)e.preventScrollReset=r.preventScrollReset;return e},[r?.replace,r?.preventScrollReset]);const n=o.useNavigate();const v=a.default.useMemo(()=>d?l.routerHistory:{replace:(e,s)=>n(e,{...u,...s}),push:(e,s)=>n(e,{...u,...s})},[n]);const[c]=o.useSearchParams();const U=o.useHref("/");const{state:m,updateState:i,updateUrl:p,getState:R,reset:f,pendingUrlUpdate:b}=_.useUrlStateBase(t,v,({parse:e})=>e(l.filterUnknownParamsClient(t,c.entries())),U);const g=a.default.useCallback((e,s)=>p(e,{...u,...s}),[p]);a.default.useEffect(()=>{if(!b()){i(l.assignValue(t,l.filterUnknownParams(t,P.parseSPObj(Object.fromEntries([...c.entries()]),t))))}},[c]);const q=a.default.useCallback(e=>{f({...u,...e})},[f]);return{setState:i,setUrl:g,urlState:m,reset:q,getState:R}}const S={replace:true,preventScrollReset:true};exports.defaultOpts=S;exports.useUrlState=j;
1
+ "use strict";var O=require("react");var c=require("react-router-dom");var P=require("../../parseSPObj.js");var _=require("../../useUrlStateBase/useUrlStateBase.js");var l=require("../../utils.js");function k(t){return t&&t.__esModule?t:{default:t}}var s=k(O);function j(t,r){const d=r?.useHistory;const u=s.default.useMemo(()=>{const e={...S};if(r?.replace!==void 0)e.replace=r.replace;if(r?.preventScrollReset!==void 0)e.preventScrollReset=r.preventScrollReset;return e},[r?.replace,r?.preventScrollReset]);const n=c.useNavigate();const v=s.default.useMemo(()=>d?l.routerHistory:{replace:(e,a)=>n(e,{...u,...a}),push:(e,a)=>n(e,{...u,...a})},[n]);const[o]=c.useSearchParams();const U=c.useHref("/");const{state:m,updateState:i,updateUrl:p,getState:R,reset:f,pendingUrlUpdate:b}=_.useUrlStateBase(t,v,({parse:e})=>e(l.filterUnknownParamsClient(t,o.entries())),U);const g=s.default.useCallback((e,a)=>p(e,{...u,...a}),[p]);s.default.useEffect(()=>{if(!b()){i(l.assignValue(t,l.filterUnknownParams(t,P.parseSPObj(Object.fromEntries([...o.entries()]),t))))}},[o]);const q=s.default.useCallback(e=>{f({...u,...e})},[f]);return{setState:i,setUrl:g,urlState:m,reset:q,getState:R}}const S={replace:true,preventScrollReset:true};exports.defaultOpts=S;exports.useUrlState=j;
@@ -1 +1 @@
1
- "use strict";var f=require("react");function u(e){return e&&e.__esModule?e:{default:e}}var t=u(f);const s=t.default?.useInsertionEffect||t.default.useEffect;exports.useInsertionEffect=s;
1
+ "use strict";var f=require("react");function u(e){return e&&e.__esModule?e:{default:e}}var t=u(f);const n=t.default?.useInsertionEffect||t.default.useEffect;exports.useInsertionEffect=n;
@@ -1,4 +1,4 @@
1
- import { type JSONCompatible } from "../utils";
1
+ import { type JSONCompatible } from '../utils';
2
2
  /**
3
3
  * Custom React hook for sharing state between unrelated components.
4
4
  *
@@ -1 +1 @@
1
- "use strict";var b=require("react");var r=require("../subscribers.js");var M=require("../useInsertionEffect.js");var i=require("../utils.js");function g(u){return u&&u.__esModule?u:{default:u}}var n=g(b);function v(u,a){const t=n.default.useRef(u);const[f,o]=n.default.useState(()=>{if(i.isSSR){return a?a?.():t.current}const e=r.stateMap.get(t.current);if(!e){const s=a?.()||t.current;r.stateMap.set(t.current,s);return s}else{return e}});const l=n.default.useCallback(e=>{const s=r.stateMap.get(t.current);const S=typeof e==="function";const c=S?e(s,u):{...s,...e};if(i.isEqual(s,c))return void 0;r.stateMap.set(t.current,c);r.subscribers.get(t.current).forEach(p=>p())},[]);M.useInsertionEffect(()=>{const e=()=>{o(r.stateMap.get(t.current)||t.current)};return r.subscribers.add(t.current,e)},[]);const d=n.default.useCallback(()=>r.stateMap.get(t.current)||t.current,[]);return{state:f,getState:d,setState:l}}exports.useSharedState=v;
1
+ "use strict";var b=require("react");var r=require("../subscribers.js");var M=require("../useInsertionEffect.js");var o=require("../utils.js");function g(u){return u&&u.__esModule?u:{default:u}}var a=g(b);function v(u,n){const t=a.default.useRef(u);const[i,c]=a.default.useState(()=>{if(o.isSSR){return n?n?.():t.current}const e=r.stateMap.get(t.current);if(!e){const s=n?.()||t.current;r.stateMap.set(t.current,s);return s}else{return e}});const l=a.default.useCallback(e=>{const s=r.stateMap.get(t.current);const p=typeof e==="function";const f=p?e(s,u):{...s,...e};if(o.isEqual(s,f))return void 0;r.stateMap.set(t.current,f);r.subscribers.get(t.current).forEach(S=>S())},[]);M.useInsertionEffect(()=>{const e=()=>{c(r.stateMap.get(t.current)||t.current)};return r.subscribers.add(t.current,e)},[]);a.default.useEffect(()=>{c(e=>r.stateMap.get(t.current)??e)},[]);const d=a.default.useCallback(()=>r.stateMap.get(t.current)||t.current,[]);return{state:i,getState:d,setState:l}}exports.useSharedState=v;
@@ -1 +1 @@
1
- import c from"react";import{stateMap as n,subscribers as a}from"../subscribers.mjs";import{useInsertionEffect as l}from"../useInsertionEffect.mjs";import{isSSR as b,isEqual as d}from"../utils.mjs";function E(u,s){const t=c.useRef(u);const[f,i]=c.useState(()=>{if(b){return s?s?.():t.current}const e=n.get(t.current);if(!e){const r=s?.()||t.current;n.set(t.current,r);return r}else{return e}});const m=c.useCallback(e=>{const r=n.get(t.current);const S=typeof e==="function";const o=S?e(r,u):{...r,...e};if(d(r,o))return void 0;n.set(t.current,o);a.get(t.current).forEach(g=>g())},[]);l(()=>{const e=()=>{i(n.get(t.current)||t.current)};return a.add(t.current,e)},[]);const p=c.useCallback(()=>n.get(t.current)||t.current,[]);return{state:f,getState:p,setState:m}}export{E as useSharedState};
1
+ import c from"react";import{stateMap as r,subscribers as f}from"../subscribers.mjs";import{useInsertionEffect as l}from"../useInsertionEffect.mjs";import{isSSR as b,isEqual as d}from"../utils.mjs";function E(o,u){const t=c.useRef(o);const[i,s]=c.useState(()=>{if(b){return u?u?.():t.current}const e=r.get(t.current);if(!e){const n=u?.()||t.current;r.set(t.current,n);return n}else{return e}});const m=c.useCallback(e=>{const n=r.get(t.current);const S=typeof e==="function";const a=S?e(n,o):{...n,...e};if(d(n,a))return void 0;r.set(t.current,a);f.get(t.current).forEach(g=>g())},[]);l(()=>{const e=()=>{s(r.get(t.current)||t.current)};return f.add(t.current,e)},[]);c.useEffect(()=>{s(e=>r.get(t.current)??e)},[]);const p=c.useCallback(()=>r.get(t.current)||t.current,[]);return{state:i,getState:p,setState:m}}export{E as useSharedState};
@@ -1 +1 @@
1
- "use strict";var c=require("react");var u=require("../encodeState/encodeState.js");function l(e){return e&&e.__esModule?e:{default:e}}var a=l(c);function d(e){const n=a.default.useCallback((t,s)=>{return u.encodeState(t,e,s)},[e]);const r=a.default.useCallback(t=>u.decodeState(t,e),[e]);return{parse:r,stringify:n}}exports.useUrlEncode=d;
1
+ "use strict";var c=require("react");var u=require("../encodeState/encodeState.js");function l(e){return e&&e.__esModule?e:{default:e}}var a=l(c);function d(e){const n=a.default.useCallback((t,o)=>{return u.encodeState(t,e,o)},[e]);const r=a.default.useCallback(t=>u.decodeState(t,e),[e]);return{parse:r,stringify:n}}exports.useUrlEncode=d;
@@ -1 +1 @@
1
- import o from"react";import{encodeState as s,decodeState as a}from"../encodeState/encodeState.mjs";function u(e){const n=o.useCallback((t,c)=>{return s(t,e,c)},[e]);const r=o.useCallback(t=>a(t,e),[e]);return{parse:r,stringify:n}}export{u as useUrlEncode};
1
+ import o from"react";import{encodeState as a,decodeState as s}from"../encodeState/encodeState.mjs";function u(e){const n=o.useCallback((t,c)=>{return a(t,e,c)},[e]);const r=o.useCallback(t=>s(t,e),[e]);return{parse:r,stringify:n}}export{u as useUrlEncode};
@@ -1 +1 @@
1
- "use strict";var k=require("react");var _=require("../constants/constants.js");var y=require("../useInsertionEffect.js");var L=require("../useSharedState/useSharedState.js");var P=require("../useUrlEncode/useUrlEncode.js");var c=require("../utils.js");function T(t){return t&&t.__esModule?t:{default:t}}var U=T(k);let i;let n;function b(t,e,l,d){const{parse:s,stringify:o}=P.useUrlEncode(t);const{state:f,getState:u,setState:a}=L.useSharedState(t,()=>l?.({parse:s})||t);y.useInsertionEffect(()=>{const r=()=>{a(s(c.filterUnknownParamsClient(t,c.getSearch())))};window.addEventListener(c.popstateEv,r);return()=>{window.removeEventListener(c.popstateEv,r)}},[]);const p=U.default.useCallback((r,E)=>{const h=typeof r==="function"?r(u(),t):{...u(),...r};const w=o(h,C(t));const v=I(window.location.pathname,d);const S=`${v}${w.length?"?":""}${w}${window.location.hash}`;const $=`${v}${window.location.search}${window.location.hash}`;if(S===$)return;a(h);const{replace:g,...q}=E||{};clearTimeout(i);i=void 0;n={router:e,method:g?"replace":"push",url:S,opts:q};i=setTimeout(()=>{if(n){n.router[n.method](n.url,n.opts);n=void 0}i=void 0},_.TIMEOUT)},[e,o,u]);const m=U.default.useCallback(r=>{a(t);p(t,r)},[p,a]);return{updateState:a,updateUrl:p,state:f,reset:m,getState:u,pendingUrlUpdate:()=>i!==void 0}}function C(t){const e=Object.keys(t);const l=window.location.search;const d=new URLSearchParams(l);const s=new URLSearchParams;for(const[o,f]of d){!e.includes(o)&&s.set(o,f)}return s}function I(t,e){if(!e||e==="/"){return t}return t.slice(e.length)}exports.useUrlStateBase=b;
1
+ "use strict";var k=require("react");var _=require("../constants/constants.js");var y=require("../useInsertionEffect.js");var L=require("../useSharedState/useSharedState.js");var P=require("../useUrlEncode/useUrlEncode.js");var c=require("../utils.js");function T(t){return t&&t.__esModule?t:{default:t}}var U=T(k);let i;let n;function b(t,e,l,d){const{parse:o,stringify:s}=P.useUrlEncode(t);const{state:f,getState:u,setState:a}=L.useSharedState(t,()=>l?.({parse:o})||t);y.useInsertionEffect(()=>{const r=()=>{a(o(c.filterUnknownParamsClient(t,c.getSearch())))};window.addEventListener(c.popstateEv,r);return()=>{window.removeEventListener(c.popstateEv,r)}},[]);const p=U.default.useCallback((r,E)=>{const h=typeof r==="function"?r(u(),t):{...u(),...r};const w=s(h,C(t));const v=I(window.location.pathname,d);const S=`${v}${w.length?"?":""}${w}${window.location.hash}`;const $=`${v}${window.location.search}${window.location.hash}`;if(S===$)return;a(h);const{replace:g,...q}=E||{};clearTimeout(i);i=void 0;n={router:e,method:g?"replace":"push",url:S,opts:q};i=setTimeout(()=>{if(n){n.router[n.method](n.url,n.opts);n=void 0}i=void 0},_.TIMEOUT)},[e,s,u]);const m=U.default.useCallback(r=>{a(t);p(t,r)},[p,a]);return{updateState:a,updateUrl:p,state:f,reset:m,getState:u,pendingUrlUpdate:()=>i!==void 0}}function C(t){const e=Object.keys(t);const l=window.location.search;const d=new URLSearchParams(l);const o=new URLSearchParams;for(const[s,f]of d){!e.includes(s)&&o.set(s,f)}return o}function I(t,e){if(!e||e==="/"){return t}return t.slice(e.length)}exports.useUrlStateBase=b;
@@ -1 +1 @@
1
- import S from"react";import{TIMEOUT as y}from"../constants/constants.mjs";import{useInsertionEffect as L}from"../useInsertionEffect.mjs";import{useSharedState as P}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as T}from"../useUrlEncode/useUrlEncode.mjs";import{popstateEv as U,filterUnknownParamsClient as b,getSearch as C}from"../utils.mjs";let i;let n;function R(t,e,l,u){const{parse:r,stringify:s}=T(t);const{state:d,getState:c,setState:a}=P(t,()=>l?.({parse:r})||t);L(()=>{const o=()=>{a(r(b(t,C())))};window.addEventListener(U,o);return()=>{window.removeEventListener(U,o)}},[]);const p=S.useCallback((o,v)=>{const m=typeof o==="function"?o(c(),t):{...c(),...o};const f=s(m,I(t));const w=O(window.location.pathname,u);const h=`${w}${f.length?"?":""}${f}${window.location.hash}`;const g=`${w}${window.location.search}${window.location.hash}`;if(h===g)return;a(m);const{replace:E,...k}=v||{};clearTimeout(i);i=void 0;n={router:e,method:E?"replace":"push",url:h,opts:k};i=setTimeout(()=>{if(n){n.router[n.method](n.url,n.opts);n=void 0}i=void 0},y)},[e,s,c]);const $=S.useCallback(o=>{a(t);p(t,o)},[p,a]);return{updateState:a,updateUrl:p,state:d,reset:$,getState:c,pendingUrlUpdate:()=>i!==void 0}}function I(t){const e=Object.keys(t);const l=window.location.search;const u=new URLSearchParams(l);const r=new URLSearchParams;for(const[s,d]of u){!e.includes(s)&&r.set(s,d)}return r}function O(t,e){if(!e||e==="/"){return t}return t.slice(e.length)}export{R as useUrlStateBase};
1
+ import S from"react";import{TIMEOUT as y}from"../constants/constants.mjs";import{useInsertionEffect as L}from"../useInsertionEffect.mjs";import{useSharedState as P}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as T}from"../useUrlEncode/useUrlEncode.mjs";import{popstateEv as U,filterUnknownParamsClient as b,getSearch as C}from"../utils.mjs";let i;let n;function R(t,o,l,u){const{parse:r,stringify:s}=T(t);const{state:d,getState:c,setState:a}=P(t,()=>l?.({parse:r})||t);L(()=>{const e=()=>{a(r(b(t,C())))};window.addEventListener(U,e);return()=>{window.removeEventListener(U,e)}},[]);const p=S.useCallback((e,v)=>{const m=typeof e==="function"?e(c(),t):{...c(),...e};const f=s(m,I(t));const w=O(window.location.pathname,u);const h=`${w}${f.length?"?":""}${f}${window.location.hash}`;const g=`${w}${window.location.search}${window.location.hash}`;if(h===g)return;a(m);const{replace:E,...k}=v||{};clearTimeout(i);i=void 0;n={router:o,method:E?"replace":"push",url:h,opts:k};i=setTimeout(()=>{if(n){n.router[n.method](n.url,n.opts);n=void 0}i=void 0},y)},[o,s,c]);const $=S.useCallback(e=>{a(t);p(t,e)},[p,a]);return{updateState:a,updateUrl:p,state:d,reset:$,getState:c,pendingUrlUpdate:()=>i!==void 0}}function I(t){const o=Object.keys(t);const l=window.location.search;const u=new URLSearchParams(l);const r=new URLSearchParams;for(const[s,d]of u){!o.includes(s)&&r.set(s,d)}return r}function O(t,o){if(!o||o==="/"){return t}return t.slice(o.length)}export{R as useUrlStateBase};
package/dist/utils.d.ts CHANGED
@@ -34,3 +34,13 @@ export declare const routerHistory: Router;
34
34
  export declare const isSafari: boolean;
35
35
  export declare const popstateEv = "popstate";
36
36
  export declare function getSearch(): string;
37
+ type UrlCb = () => void;
38
+ /**
39
+ * Subscribe to any URL change: back/forward, this library's own writes, or a
40
+ * router using the History API. Runs after `window.location` is updated.
41
+ *
42
+ * @param {() => void} cb - called on every URL change
43
+ * @returns {() => void} unsubscribe
44
+ */
45
+ export declare function subscribeToUrl(cb: UrlCb): () => void;
46
+ export {};
package/dist/utils.js CHANGED
@@ -1 +1 @@
1
- "use strict";const i=n=>{if(n===null)return"null";const t=typeof n;if(t!=="object")return t;if(Array.isArray(n))return"array";if(n instanceof Date)return"date";return"object"};const u=n=>{const t=i(n);return t!=="object"&&t!=="array"};const a=typeof window==="undefined";const l=n=>{if(n instanceof URLSearchParams){return n}if(typeof n==="string"){const t=n.indexOf("?");return new URLSearchParams(t===-1?n:n.slice(t+1))}return new URLSearchParams};const y=(n,t)=>{if(n.length!==t.length)return false;return n.every((r,s)=>c(r,t[s]))};const w=(n,t)=>{const r=Object.keys(n);const s=Object.keys(t);if(r.length!==s.length)return false;return r.every(e=>c(n[e],t[e]))};const c=(n,t)=>{if(n===t)return true;if(n==null||t==null)return false;const r=i(n);const s=i(t);if(r!==s)return false;if(u(n))return false;if(r==="array"){return y(n,t)}if(r==="object"){return w(n,t)}return JSON.stringify(n)===JSON.stringify(t)};function p(n,t){const r=new URLSearchParams;const s=f(n,[...new URLSearchParams(t||"").entries()]);for(const[e,o]of s){r.set(e,o)}return r.toString()}function g(n,t){const r=t instanceof URLSearchParams?[...t.entries()]:Object.entries(t||{});return Object.fromEntries(f(n,r))}function f(n,t){const r=Object.keys(n);const s=[];for(let e=0;e<t.length;e++){if(r.includes(t[e]?.[0])){s.push([t[e][0].replace(/\+/g," "),t[e][1]])}}return s}function h(n,t){const r=Object.assign({},n);for(const[s]of Object.entries(n)){const e=s;const o=t[e]!==void 0;r[e]=o?t[e]:n[e]}return r}const S={push:n=>{if(typeof window!=="undefined"){window.history.pushState(null,"",n)}},replace:n=>{if(typeof window!=="undefined"){window.history.replaceState(null,"",n)}}};const d=/apple/i.test(!a&&navigator?.userAgent||"");const m="popstate";function P(){return a?"":window.location.search}exports.assignValue=h;exports.filterUnknown=f;exports.filterUnknownParams=g;exports.filterUnknownParamsClient=p;exports.getParams=l;exports.getSearch=P;exports.isEqual=c;exports.isPrimitive=u;exports.isSSR=a;exports.isSafari=d;exports.popstateEv=m;exports.routerHistory=S;exports.typeOf=i;
1
+ "use strict";const a=n=>{if(n===null)return"null";const e=typeof n;if(e!=="object")return e;if(Array.isArray(n))return"array";if(n instanceof Date)return"date";return"object"};const w=n=>{const e=a(n);return e!=="object"&&e!=="array"};const o=typeof window==="undefined";const y=n=>{if(n instanceof URLSearchParams){return n}if(typeof n==="string"){const e=n.indexOf("?");return new URLSearchParams(e===-1?n:n.slice(e+1))}return new URLSearchParams};const p=(n,e)=>{if(n.length!==e.length)return false;return n.every((r,s)=>c(r,e[s]))};const h=(n,e)=>{const r=Object.keys(n);const s=Object.keys(e);if(r.length!==s.length)return false;return r.every(t=>c(n[t],e[t]))};const c=(n,e)=>{if(n===e)return true;if(n==null||e==null)return false;const r=a(n);const s=a(e);if(r!==s)return false;if(w(n))return false;if(r==="array"){return p(n,e)}if(r==="object"){return h(n,e)}return JSON.stringify(n)===JSON.stringify(e)};function S(n,e){const r=new URLSearchParams;const s=f(n,[...new URLSearchParams(e||"").entries()]);for(const[t,i]of s){r.set(t,i)}return r.toString()}function g(n,e){const r=e instanceof URLSearchParams?[...e.entries()]:Object.entries(e||{});return Object.fromEntries(f(n,r))}function f(n,e){const r=Object.keys(n);const s=[];for(let t=0;t<e.length;t++){if(r.includes(e[t]?.[0])){s.push([e[t][0].replace(/\+/g," "),e[t][1]])}}return s}function m(n,e){const r=Object.assign({},n);for(const[s]of Object.entries(n)){const t=s;const i=e[t]!==void 0;r[t]=i?e[t]:n[t]}return r}const b={push:n=>{if(typeof window!=="undefined"){window.history.pushState(null,"",n)}},replace:n=>{if(typeof window!=="undefined"){window.history.replaceState(null,"",n)}}};const P=/apple/i.test(!o&&navigator?.userAgent||"");const u="popstate";function U(){return o?"":window.location.search}const l=new Set;let d=false;function v(){if(d||o)return;d=true;for(const n of["pushState","replaceState"]){const e=window.history[n];window.history[n]=function r(...s){const t=e.apply(this,s);for(const i of[...l])i();return t}}}function O(n){if(o)return()=>void 0;v();l.add(n);window.addEventListener(u,n);return()=>{l.delete(n);window.removeEventListener(u,n)}}exports.assignValue=m;exports.filterUnknown=f;exports.filterUnknownParams=g;exports.filterUnknownParamsClient=S;exports.getParams=y;exports.getSearch=U;exports.isEqual=c;exports.isPrimitive=w;exports.isSSR=o;exports.isSafari=P;exports.popstateEv=u;exports.routerHistory=b;exports.subscribeToUrl=O;exports.typeOf=a;
package/dist/utils.mjs CHANGED
@@ -1 +1 @@
1
- const o=n=>{if(n===null)return"null";const t=typeof n;if(t!=="object")return t;if(Array.isArray(n))return"array";if(n instanceof Date)return"date";return"object"};const u=n=>{const t=o(n);return t!=="object"&&t!=="array"};const a=typeof window==="undefined";const l=n=>{if(n instanceof URLSearchParams){return n}if(typeof n==="string"){const t=n.indexOf("?");return new URLSearchParams(t===-1?n:n.slice(t+1))}return new URLSearchParams};const y=(n,t)=>{if(n.length!==t.length)return false;return n.every((r,s)=>c(r,t[s]))};const p=(n,t)=>{const r=Object.keys(n);const s=Object.keys(t);if(r.length!==s.length)return false;return r.every(e=>c(n[e],t[e]))};const c=(n,t)=>{if(n===t)return true;if(n==null||t==null)return false;const r=o(n);const s=o(t);if(r!==s)return false;if(u(n))return false;if(r==="array"){return y(n,t)}if(r==="object"){return p(n,t)}return JSON.stringify(n)===JSON.stringify(t)};function w(n,t){const r=new URLSearchParams;const s=f(n,[...new URLSearchParams(t||"").entries()]);for(const[e,i]of s){r.set(e,i)}return r.toString()}function h(n,t){const r=t instanceof URLSearchParams?[...t.entries()]:Object.entries(t||{});return Object.fromEntries(f(n,r))}function f(n,t){const r=Object.keys(n);const s=[];for(let e=0;e<t.length;e++){if(r.includes(t[e]?.[0])){s.push([t[e][0].replace(/\+/g," "),t[e][1]])}}return s}function d(n,t){const r=Object.assign({},n);for(const[s]of Object.entries(n)){const e=s;const i=t[e]!==void 0;r[e]=i?t[e]:n[e]}return r}const g={push:n=>{if(typeof window!=="undefined"){window.history.pushState(null,"",n)}},replace:n=>{if(typeof window!=="undefined"){window.history.replaceState(null,"",n)}}};const S=/apple/i.test(!a&&navigator?.userAgent||"");const b="popstate";function j(){return a?"":window.location.search}export{d as assignValue,f as filterUnknown,h as filterUnknownParams,w as filterUnknownParamsClient,l as getParams,j as getSearch,c as isEqual,u as isPrimitive,a as isSSR,S as isSafari,b as popstateEv,g as routerHistory,o as typeOf};
1
+ const c=n=>{if(n===null)return"null";const e=typeof n;if(e!=="object")return e;if(Array.isArray(n))return"array";if(n instanceof Date)return"date";return"object"};const w=n=>{const e=c(n);return e!=="object"&&e!=="array"};const i=typeof window==="undefined";const y=n=>{if(n instanceof URLSearchParams){return n}if(typeof n==="string"){const e=n.indexOf("?");return new URLSearchParams(e===-1?n:n.slice(e+1))}return new URLSearchParams};const p=(n,e)=>{if(n.length!==e.length)return false;return n.every((r,o)=>a(r,e[o]))};const h=(n,e)=>{const r=Object.keys(n);const o=Object.keys(e);if(r.length!==o.length)return false;return r.every(t=>a(n[t],e[t]))};const a=(n,e)=>{if(n===e)return true;if(n==null||e==null)return false;const r=c(n);const o=c(e);if(r!==o)return false;if(w(n))return false;if(r==="array"){return p(n,e)}if(r==="object"){return h(n,e)}return JSON.stringify(n)===JSON.stringify(e)};function S(n,e){const r=new URLSearchParams;const o=f(n,[...new URLSearchParams(e||"").entries()]);for(const[t,s]of o){r.set(t,s)}return r.toString()}function g(n,e){const r=e instanceof URLSearchParams?[...e.entries()]:Object.entries(e||{});return Object.fromEntries(f(n,r))}function f(n,e){const r=Object.keys(n);const o=[];for(let t=0;t<e.length;t++){if(r.includes(e[t]?.[0])){o.push([e[t][0].replace(/\+/g," "),e[t][1]])}}return o}function b(n,e){const r=Object.assign({},n);for(const[o]of Object.entries(n)){const t=o;const s=e[t]!==void 0;r[t]=s?e[t]:n[t]}return r}const m={push:n=>{if(typeof window!=="undefined"){window.history.pushState(null,"",n)}},replace:n=>{if(typeof window!=="undefined"){window.history.replaceState(null,"",n)}}};const j=/apple/i.test(!i&&navigator?.userAgent||"");const u="popstate";function O(){return i?"":window.location.search}const l=new Set;let d=false;function v(){if(d||i)return;d=true;for(const n of["pushState","replaceState"]){const e=window.history[n];window.history[n]=function r(...o){const t=e.apply(this,o);for(const s of[...l])s();return t}}}function P(n){if(i)return()=>void 0;v();l.add(n);window.addEventListener(u,n);return()=>{l.delete(n);window.removeEventListener(u,n)}}export{b as assignValue,f as filterUnknown,g as filterUnknownParams,S as filterUnknownParamsClient,y as getParams,O as getSearch,a as isEqual,w as isPrimitive,i as isSSR,j as isSafari,u as popstateEv,m as routerHistory,P as subscribeToUrl,c as typeOf};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "6.2.4",
3
+ "version": "6.3.0",
4
4
  "description": "Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15, react-router@6-7, and remix@2.",
5
5
  "homepage": "https://state-in-url.dev",
6
6
  "repository": {
@@ -106,6 +106,8 @@
106
106
  "build": "wireit",
107
107
  "build:demo": "wireit",
108
108
  "start:demo": "wireit",
109
+ "lighthouse:serve": "cd packages/example-nextjs16 && pnpm exec next start --port 3012",
110
+ "test:lighthouse": "playwright test -c playwright.lighthouse.config.ts",
109
111
  "build:packages": "wireit",
110
112
  "start:demo15": "wireit",
111
113
  "start:demo15:ci": "wireit",
@@ -125,7 +127,7 @@
125
127
  "reinstall": "pnpm run cleanup && pnpm install",
126
128
  "setup": "playwright install --with-deps",
127
129
  "prepack": "pnpm run build && cp package.json dist/package.json",
128
- "pin-gh-deps": "pin-github-action -i .github/workflows/tests.yml",
130
+ "pin-gh-deps": "pin-github-action -i .github/workflows/tests.yml .github/workflows/lighthouse.yml",
129
131
  "prepare": "pnpm exec husky"
130
132
  },
131
133
  "wireit": {
@@ -472,9 +474,9 @@
472
474
  "@rollup/plugin-typescript": "^12.1.4",
473
475
  "@semantic-release/changelog": "^6.0.3",
474
476
  "@semantic-release/commit-analyzer": "^13.0.1",
477
+ "@semantic-release/exec": "^7.1.0",
475
478
  "@semantic-release/git": "^10.0.1",
476
479
  "@semantic-release/npm": "^13.1.3",
477
- "@semantic-release/exec": "^7.1.0",
478
480
  "@semantic-release/release-notes-generator": "^14.1.0",
479
481
  "@tanstack/intent": "^0.3.6",
480
482
  "@testing-library/dom": "^10.4.1",
@@ -504,12 +506,14 @@
504
506
  "glob": "^13.0.0",
505
507
  "happy-dom": "^20.0.2",
506
508
  "husky": "^9.1.7",
509
+ "lighthouse": "^13.4.1",
507
510
  "lint-staged": "^15.5.2",
508
511
  "next": "^15.5.19",
509
512
  "nyc": "^17.1.0",
510
513
  "only-allow": "^1.2.1",
511
514
  "pin-github-action": "^1.9.1",
512
515
  "playwright": "^1.55.1",
516
+ "playwright-lighthouse": "^4.0.0",
513
517
  "prettier": "^3.6.2",
514
518
  "prettier-plugin-tailwindcss": "^0.6.14",
515
519
  "react": "^19.2.6",
@@ -14,7 +14,7 @@ sources:
14
14
  metadata:
15
15
  type: core
16
16
  library: state-in-url
17
- library_version: '6.2.4'
17
+ library_version: '6.3.0'
18
18
  ---
19
19
 
20
20
  # state-in-url — Feature state hook
@@ -41,22 +41,36 @@ export const JOBS_STATE: JobsState = {
41
41
  ```typescript
42
42
  // features/jobs/useJobsState.ts
43
43
  'use client';
44
- import { useSearchParams } from 'next/navigation';
45
44
  import { useUrlState } from 'state-in-url/next';
46
45
  import { JOBS_STATE } from './jobsState';
47
46
 
48
- export function useJobsState() {
49
- const searchParams = useSearchParams();
47
+ // `searchParams` comes from the server component at the top of the feature.
48
+ // Do not call useSearchParams() here — see the nextjs-ssr skill.
49
+ export function useJobsState(searchParams?: object) {
50
50
  return useUrlState(JOBS_STATE, { searchParams });
51
51
  }
52
52
  ```
53
53
 
54
54
  ```typescript
55
- // any component
55
+ // app/jobs/page.tsx — server component
56
+ import { JobsTabs } from './JobsTabs';
57
+
58
+ export default async function Page({
59
+ searchParams,
60
+ }: {
61
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
62
+ }) {
63
+ return <JobsTabs searchParams={await searchParams} />;
64
+ }
65
+ ```
66
+
67
+ ```typescript
68
+ // app/jobs/JobsTabs.tsx
69
+ 'use client';
56
70
  import { useJobsState } from 'features/jobs/useJobsState';
57
71
 
58
- export function JobsTabs() {
59
- const { urlState, setUrl, reset } = useJobsState();
72
+ export function JobsTabs({ searchParams }: { searchParams?: object }) {
73
+ const { urlState, setUrl, reset } = useJobsState(searchParams);
60
74
 
61
75
  return (
62
76
  <>
@@ -67,7 +81,7 @@ export function JobsTabs() {
67
81
  }
68
82
  ```
69
83
 
70
- The same `useJobsState()` called in two different components reads and writes the same URL state — no Context, no Provider.
84
+ The same `useJobsState()` called in two different components reads and writes the same URL state — no Context, no Provider. Only the component the server renders needs `searchParams`; the rest call `useJobsState()` with no argument.
71
85
 
72
86
  ## Core Patterns
73
87
 
@@ -15,7 +15,7 @@ sources:
15
15
  metadata:
16
16
  type: composition
17
17
  library: state-in-url
18
- library_version: '6.2.4'
18
+ library_version: '6.3.0'
19
19
  ---
20
20
 
21
21
  This skill builds on `state-in-url/feature-state-hook`. Read it first for the module-scoped default-state rule.
@@ -48,14 +48,13 @@ export const FILTERS_STATE: FiltersState = {
48
48
  // features/filters/FiltersForm.tsx
49
49
  'use client';
50
50
  import React from 'react';
51
- import { useSearchParams } from 'next/navigation';
52
51
  import { useForm } from 'react-hook-form';
53
52
  import { zodResolver } from '@hookform/resolvers/zod';
54
53
  import { useUrlState } from 'state-in-url/next';
55
54
  import { FILTERS_STATE, filtersSchema, type FiltersState } from './filtersState';
56
55
 
57
- export function FiltersForm() {
58
- const searchParams = useSearchParams();
56
+ // `searchParams` forwarded by the server page that renders this form
57
+ export function FiltersForm({ searchParams }: { searchParams?: object }) {
59
58
  const { urlState, setUrl } = useUrlState(FILTERS_STATE, { searchParams });
60
59
 
61
60
  const form = useForm<FiltersState>({
@@ -16,7 +16,7 @@ sources:
16
16
  metadata:
17
17
  type: core
18
18
  library: state-in-url
19
- library_version: '6.2.4'
19
+ library_version: '6.3.0'
20
20
  ---
21
21
 
22
22
  This skill builds on `state-in-url/feature-state-hook`. Read it first for the module-scoped default-state rule.
@@ -44,23 +44,37 @@ export const SEARCH_STATE: SearchState = { q: '', sort: 'name' };
44
44
  ```typescript
45
45
  // features/search/useSearchState.ts
46
46
  'use client';
47
- import { useSearchParams } from 'next/navigation';
48
47
  import { useUrlState } from 'state-in-url/next';
49
48
  import { SEARCH_STATE } from './searchState';
50
49
 
51
- export function useSearchState() {
52
- const searchParams = useSearchParams();
50
+ // `searchParams` comes from the server component at the top of the feature.
51
+ export function useSearchState(searchParams?: object) {
53
52
  return useUrlState(SEARCH_STATE, { searchParams });
54
53
  }
55
54
  ```
56
55
 
56
+ ```typescript
57
+ // app/search/page.tsx — server component
58
+ import { SearchBox } from './SearchBox';
59
+
60
+ export default async function Page({
61
+ searchParams,
62
+ }: {
63
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
64
+ }) {
65
+ return <SearchBox searchParams={await searchParams} />;
66
+ }
67
+ ```
68
+
57
69
  ## Core Patterns
58
70
 
59
71
  ### Instant input, deferred URL write (onBlur)
60
72
 
61
73
  ```typescript
62
- function SearchBox() {
63
- const { urlState, setState, setUrl } = useSearchState();
74
+ 'use client';
75
+
76
+ function SearchBox({ searchParams }: { searchParams?: object }) {
77
+ const { urlState, setState, setUrl } = useSearchState(searchParams);
64
78
 
65
79
  return (
66
80
  <input
@@ -2,21 +2,22 @@
2
2
  name: nextjs-ssr
3
3
  description: >
4
4
  SSR-safe useUrlState in Next.js App Router. Forward searchParams from server
5
- pages (awaiting the Promise in Next.js 15+), call useSearchParams() in pure
6
- client components, decide between useHistory true/false, and use a Proxy
7
- (formerly middleware) to expose query params to server layouts. App Router
8
- only — Pages Router is not supported. Load this skill for any use of
9
- state-in-url/next or anytime URL state must be correct on first paint.
5
+ pages (awaiting the Promise in Next.js 15+) rather than calling
6
+ useSearchParams(), decide between useHistory true/false, keep pages
7
+ prerenderable, and use a Proxy (formerly middleware) to expose query params to
8
+ server layouts. App Router only — Pages Router is not supported. Load this
9
+ skill for any use of state-in-url/next or anytime URL state must be correct on
10
+ first paint.
10
11
  requires:
11
12
  - feature-state-hook
12
13
  sources:
13
14
  - 'asmyshlyaev177/state-in-url:packages/urlstate/next/useUrlState/useUrlState.ts'
14
- - 'asmyshlyaev177/state-in-url:packages/example-nextjs16/src/middleware.ts'
15
+ - 'asmyshlyaev177/state-in-url:packages/example-nextjs16/src/proxy.ts'
15
16
  - 'asmyshlyaev177/state-in-url:README.md#with-server-side-rendering'
16
17
  metadata:
17
18
  type: framework
18
19
  library: state-in-url
19
- library_version: '6.2.4'
20
+ library_version: '6.3.0'
20
21
  framework: react
21
22
  ---
22
23
 
@@ -58,9 +59,15 @@ export function JobsList({ searchParams }: { searchParams: object }) {
58
59
  }
59
60
  ```
60
61
 
61
- ### Pure client component using `useSearchParams`
62
+ ### Fallback: `useSearchParams` when the prop cannot reach the component
62
63
 
63
- When you can't (or don't want to) thread `searchParams` from a server parent:
64
+ Use this only when a client component genuinely cannot receive `searchParams`
65
+ from a server parent. It is not the default and not an equivalent alternative:
66
+ on a statically rendered route `useSearchParams()` needs a `<Suspense>` boundary
67
+ — `next build` fails without one — and everything inside that boundary is
68
+ client-rendered, so it shows the fallback until hydration. On an already-dynamic
69
+ route it costs nothing extra, but it still buys nothing the server prop does not
70
+ — on the client the hook already reads `window.location.search` for itself.
64
71
 
65
72
  ```typescript
66
73
  'use client';
@@ -89,6 +96,35 @@ useUrlState(FORM_STATE, { searchParams, useHistory: false });
89
96
 
90
97
  Default is `true`. Flip to `false` only when the server page needs to re-render with the new query.
91
98
 
99
+ ### Prerendering, PPR and `cacheComponents`
100
+
101
+ The hook does not call `useSearchParams` itself. A component that uses it needs
102
+ no `<Suspense>` boundary and does not opt its page out of prerendering, so it
103
+ works under PPR and `cacheComponents: true`.
104
+
105
+ It reads the initial state from `searchParams` on the server and
106
+ `window.location.search` on the client, and tracks later changes by observing
107
+ the History API — which also catches changes Next's router never sees, including
108
+ the hook's own writes under the default `useHistory: true`, and a bare
109
+ `history.pushState` from unrelated code.
110
+
111
+ A prerendered page still renders with the default state, because at build time
112
+ there is no query string:
113
+
114
+ ```typescript
115
+ // Static: correct for the bare URL, wrong for /jobs?status='open' until hydration.
116
+ export default function Page() { return <JobsList />; }
117
+
118
+ // Dynamic: correct on first paint for any URL.
119
+ export default async function Page({ searchParams }) {
120
+ return <JobsList searchParams={await searchParams} />;
121
+ }
122
+ ```
123
+
124
+ Prerender when the bare URL is the common case and stateful URLs can settle
125
+ after hydration. Render dynamically when a shared stateful link must be right on
126
+ first paint.
127
+
92
128
  ### Decoding state on the server for data fetching
93
129
 
94
130
  ```typescript
@@ -230,6 +266,38 @@ useUrlState(FORM_STATE, { searchParams });
230
266
 
231
267
  Source: JSDoc on `useUrlState` params; vercel/next.js#59167
232
268
 
269
+ ### MEDIUM Adding `useSearchParams` + `<Suspense>` just to use the hook
270
+
271
+ Wrong:
272
+
273
+ ```typescript
274
+ // page.tsx — JobsList now renders as the skeleton until hydration
275
+ <Suspense fallback={<Skeleton />}><JobsList /></Suspense>
276
+
277
+ // JobsList.tsx
278
+ const searchParams = useSearchParams();
279
+ const { urlState } = useUrlState(JOBS_STATE, { searchParams });
280
+ ```
281
+
282
+ Correct:
283
+
284
+ ```typescript
285
+ // page.tsx
286
+ export default async function Page({ searchParams }) {
287
+ return <JobsList searchParams={await searchParams} />;
288
+ }
289
+ ```
290
+
291
+ The hook does not call `useSearchParams` internally, so it imposes no Suspense
292
+ boundary. Adding one is not free: it is narrow, so the rest of the page still
293
+ prerenders, but `JobsList` itself becomes client-rendered — a static route ships
294
+ the skeleton and fills in the real state only after hydration. Forwarding
295
+ `searchParams` from the server page is the preferred approach and keeps the
296
+ first paint correct; reach for `useSearchParams()` only when you genuinely
297
+ cannot thread the prop down.
298
+
299
+ Source: packages/urlstate/next/useUrlState/useUrlState.ts
300
+
233
301
  ### MEDIUM Trying to read `searchParams` in a server layout directly
234
302
 
235
303
  Wrong:
@@ -17,7 +17,7 @@ sources:
17
17
  metadata:
18
18
  type: framework
19
19
  library: state-in-url
20
- library_version: '6.2.4'
20
+ library_version: '6.3.0'
21
21
  framework: react
22
22
  ---
23
23
 
@@ -14,7 +14,7 @@ sources:
14
14
  metadata:
15
15
  type: core
16
16
  library: state-in-url
17
- library_version: '6.2.4'
17
+ library_version: '6.3.0'
18
18
  ---
19
19
 
20
20
  # state-in-url — Shared state without URL sync