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.
- package/dist/encoder/encoder.d.ts +1 -1
- package/dist/encoder/encoder.js +1 -1
- package/dist/encoder/encoder.mjs +1 -1
- package/dist/next/useUrlState/useUrlState.d.ts +3 -3
- package/dist/next/useUrlState/useUrlState.js +1 -1
- package/dist/next/useUrlState/useUrlState.mjs +1 -1
- package/dist/package.json +7 -3
- package/dist/react-router/useUrlState/useUrlState.js +1 -1
- package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
- package/dist/react-router6/useUrlState/useUrlState.js +1 -1
- package/dist/useInsertionEffect.js +1 -1
- package/dist/useSharedState/useSharedState.d.ts +1 -1
- package/dist/useSharedState/useSharedState.js +1 -1
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlEncode/useUrlEncode.js +1 -1
- package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +7 -3
- package/skills/feature-state-hook/SKILL.md +22 -8
- package/skills/form-library-integration/SKILL.md +3 -4
- package/skills/input-handling/SKILL.md +20 -6
- package/skills/nextjs-ssr/SKILL.md +77 -9
- package/skills/react-router-remix-setup/SKILL.md +1 -1
- package/skills/shared-state-no-url/SKILL.md +1 -1
package/dist/encoder/encoder.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/encoder/encoder.mjs
CHANGED
|
@@ -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
|
|
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
|
|
2
|
-
import { type JSONCompatible } from
|
|
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[
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 +1 @@
|
|
|
1
|
-
"use strict";var b=require("react");var r=require("../subscribers.js");var M=require("../useInsertionEffect.js");var
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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+)
|
|
6
|
-
|
|
7
|
-
(formerly middleware) to expose query params to
|
|
8
|
-
only — Pages Router is not supported. Load this
|
|
9
|
-
state-in-url/next or anytime URL state must be correct on
|
|
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/
|
|
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.
|
|
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
|
-
###
|
|
62
|
+
### Fallback: `useSearchParams` when the prop cannot reach the component
|
|
62
63
|
|
|
63
|
-
|
|
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:
|