diff --git a/CHANGELOG.md b/CHANGELOG.md index f6068eb..826ef38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [4.0.0](https://github.com/asmyshlyaev177/state-in-url/compare/v3.1.0...v4.0.0) (2024-11-02) + + +### Features + +* `useHistory` option to use native window.history instead of Next.js router, true by default ([6cb1a41](https://github.com/asmyshlyaev177/state-in-url/commit/6cb1a41ee1ab43e9a9c2b0e18f85d40054a60405)) + + +### BREAKING CHANGES + +* For Next.js, `useUrlState` hook will use `window.history` for navigation by +default, to opt out pass `useUrlState({ useHistory: false })` + # [3.1.0](https://github.com/asmyshlyaev177/state-in-url/compare/v3.0.7...v3.1.0) (2024-11-02) diff --git a/dist/next/useUrlState/useUrlState.d.ts b/dist/next/useUrlState/useUrlState.d.ts index 9e84587..459ddfa 100644 --- a/dist/next/useUrlState/useUrlState.d.ts +++ b/dist/next/useUrlState/useUrlState.d.ts @@ -5,6 +5,7 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils"; * * @param {JSONCompatible} [defaultState] Fallback (default) values for state * @param {?SearchParams} [searchParams] searchParams from Next server component + * @param {boolean} [useHistory] use window.history for navigation, no _rsc requests https://github.com/vercel/next.js/discussions/59167 * * * Example: * ```ts @@ -22,11 +23,12 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils"; * * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#api} */ -export declare function useUrlState({ defaultState, searchParams, ...opts }: { +export declare function useUrlState({ defaultState, searchParams, useHistory, ...opts }: { defaultState: T; searchParams?: object; replace?: boolean; scroll?: boolean; + useHistory?: boolean; }): { /** * * Example: diff --git a/dist/next/useUrlState/useUrlState.mjs b/dist/next/useUrlState/useUrlState.mjs index 27a691b..e642469 100644 --- a/dist/next/useUrlState/useUrlState.mjs +++ b/dist/next/useUrlState/useUrlState.mjs @@ -1 +1 @@ -import{useRouter as t,useSearchParams as e}from"next/navigation";import a from"react";import{parseSPObj as r}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{isSSR as o,filterUnknownParams as m,filterUnknownParamsClient as l}from"../../utils.mjs";function p({defaultState:p,searchParams:c,...i}){const n=t(),{state:f,updateState:S,updateUrl:d,getState:j}=s(p,n,(({parse:t})=>o()?r(m(p,c),p):t(l(p)))),U=a.useCallback(((t,e)=>{const a={...u,...i,...e};d(t,a)}),[d,i]),b=e();return a.useEffect((()=>{S(m(p,r(Object.fromEntries([...b.entries()]),p)))}),[b]),{setState:S,updateState:S,setUrl:U,updateUrl:U,urlState:f,state:f,getState:j}}const u={replace:!0,scroll:!1};export{p as useUrlState}; +import{useRouter as t,useSearchParams as e}from"next/navigation";import r from"react";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as o,isSSR as m,filterUnknownParams as u,filterUnknownParamsClient as i}from"../../utils.mjs";function l({defaultState:l,searchParams:c,useHistory:n,...f}){const S=void 0===n||n?o:t(),{state:d,updateState:j,updateUrl:U,getState:b}=s(l,S,(({parse:t})=>m()?a(u(l,c),l):t(i(l)))),g=r.useCallback(((t,e)=>{const r={...p,...f,...e};U(t,r)}),[U,f]),v=e();return r.useEffect((()=>{j(u(l,a(Object.fromEntries([...v.entries()]),l)))}),[v]),{setState:j,updateState:j,setUrl:g,updateUrl:g,urlState:d,state:d,getState:b}}const p={replace:!0,scroll:!1};export{l as useUrlState}; diff --git a/dist/react-router/useUrlState/useUrlState.d.ts b/dist/react-router/useUrlState/useUrlState.d.ts index cab67d4..88a457c 100644 --- a/dist/react-router/useUrlState/useUrlState.d.ts +++ b/dist/react-router/useUrlState/useUrlState.d.ts @@ -5,6 +5,7 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils"; * * @param {JSONCompatible} [defaultState] Fallback (default) values for state * @param {NavigateOptions} [NavigateOptions] See type from `react-router-dom` + * @param {boolean} [useHistory] use window.history for navigation * * Example: * ```ts * export const form = { name: '', age: 0 }; @@ -18,8 +19,9 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils"; * * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#api} */ -export declare function useUrlState({ defaultState, ...initOpts }: { +export declare function useUrlState({ defaultState, useHistory, ...initOpts }: { defaultState: T; + useHistory?: boolean; } & NavigateOptions): { /** * * Example: diff --git a/dist/react-router/useUrlState/useUrlState.mjs b/dist/react-router/useUrlState/useUrlState.mjs index fd48b96..5e258aa 100644 --- a/dist/react-router/useUrlState/useUrlState.mjs +++ b/dist/react-router/useUrlState/useUrlState.mjs @@ -1 +1 @@ -import t from"react";import{useNavigate as e,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{filterUnknownParamsClient as o,assignValue as m,filterUnknownParams as p}from"../../utils.mjs";function u({defaultState:u,...c}){const S=e(),f=t.useMemo((()=>({replace:(t,e)=>S(t,{...l,...c,...e}),push:(t,e)=>S(t,{...l,...c,...e})})),[S,c]),{state:i,updateState:n,updateUrl:d,getState:j}=s(u,f,(({parse:t})=>t(o(u)))),U=t.useCallback(((t,e)=>{const r={...l,...c,...e};d(t,r)}),[c]),[b]=r();return t.useEffect((()=>{n(m(u,i,p(u,a(Object.fromEntries([...b.entries()]),u))))}),[b]),{setState:n,updateState:n,setUrl:U,updateUrl:U,urlState:i,state:i,getState:j}}const l={replace:!0,preventScrollReset:!0};export{u as useUrlState}; +import t from"react";import{useNavigate as e,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as o,filterUnknownParamsClient as u,assignValue as m,filterUnknownParams as p}from"../../utils.mjs";function l({defaultState:l,useHistory:S,...f}){const i=e(),n=t.useMemo((()=>S?o:{replace:(t,e)=>i(t,{...c,...f,...e}),push:(t,e)=>i(t,{...c,...f,...e})}),[i,f]),{state:d,updateState:j,updateUrl:U,getState:b}=s(l,n,(({parse:t})=>t(u(l)))),g=t.useCallback(((t,e)=>{const r={...c,...f,...e};U(t,r)}),[f]),[B]=r();return t.useEffect((()=>{j(m(l,d,p(l,a(Object.fromEntries([...B.entries()]),l))))}),[B]),{setState:j,updateState:j,setUrl:g,updateUrl:g,urlState:d,state:d,getState:b}}const c={replace:!0,preventScrollReset:!0};export{l as useUrlState}; diff --git a/dist/useUrlStateBase/useUrlStateBase.d.ts b/dist/useUrlStateBase/useUrlStateBase.d.ts index 1ebc09a..5b7e03e 100644 --- a/dist/useUrlStateBase/useUrlStateBase.d.ts +++ b/dist/useUrlStateBase/useUrlStateBase.d.ts @@ -1,5 +1,5 @@ import { useUrlEncode } from "../useUrlEncode"; -import { type DeepReadonly, type JSONCompatible } from "../utils"; +import { type DeepReadonly, type JSONCompatible, type Router } from "../utils"; /** * A custom React hook to create custom hooks. * @@ -35,8 +35,4 @@ interface OptionsObject { export interface Options extends OptionsObject { replace?: boolean; } -type Router = { - push: (href: string, opts: object) => void; - replace: (href: string, opts: object) => void; -}; export {}; diff --git a/dist/useUrlStateBase/useUrlStateBase.mjs b/dist/useUrlStateBase/useUrlStateBase.mjs index 1c5111e..7e475ca 100644 --- a/dist/useUrlStateBase/useUrlStateBase.mjs +++ b/dist/useUrlStateBase/useUrlStateBase.mjs @@ -1 +1 @@ -import t from"react";import{useInsertionEffect as e}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as o}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as r}from"../utils.mjs";function s(s,c,i){const{parse:u,stringify:f}=o(s),{state:h,getState:l,setState:m}=n(s,(()=>i?.({parse:u})||s));e((()=>{const t=()=>{const t=u(r(s));m(t)};return window.addEventListener(a,t),()=>{window.removeEventListener(a,t)}}),[m]);const p=t.useRef([]),d=t.useCallback(((t,e)=>{const n="function"==typeof t?t(l()):t?{...l(),...t}:l(),o=f(n,function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(s)),r=`${window.location.pathname}${o.length?"?":""}${o}${window.location.hash}`;if(r===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let a;m(n);const{replace:i,...u}=e||{};p.current.push([i?"replace":"push",r,u]),1===p.current.length&&queueMicrotask((()=>{for(;p.current.length;){const t=p.current.shift();if(!t)break;t?.[1]!==a?.[1]&&(a=t)}if(!a)return;const[t,e,n]=a;a=void 0,c[t](e,n)}))}),[c,f,l]);return{updateState:m,updateUrl:d,state:h,getState:l}}const a="popstate";export{s as useUrlStateBase}; +import t from"react";import{useInsertionEffect as e}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as o}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as r}from"../utils.mjs";function s(s,c,i){const{parse:u,stringify:h}=o(s),{state:l,getState:m,setState:p}=n(s,(()=>i?.({parse:u})||s));e((()=>{const t=()=>{const t=u(r(s));p(t)};return window.addEventListener(a,t),()=>{window.removeEventListener(a,t)}}),[p]);const d=t.useRef([]),w=t.useCallback(((t,e)=>{const n="function"==typeof t?t(m()):t?{...m(),...t}:m(),o=h(n,function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(s)),r=`${window.location.pathname}${o.length?"?":""}${o}${window.location.hash}`;if(r===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let a;p(n);const{replace:i,...u}=e||{};d.current.push([i?"replace":"push",r,u]),1===d.current.length&&queueMicrotask((()=>{for(;d.current.length;){const t=d.current.shift();t&&t?.[1]!==a?.[1]&&(a=t)}const[t,e,n]=a||{};a=void 0,t&&c[t](e,n)}))}),[c,h,m]);return{updateState:p,updateUrl:w,state:l,getState:m}}const a="popstate";export{s as useUrlStateBase}; diff --git a/dist/utils.d.ts b/dist/utils.d.ts index 4f83459..f6adc5d 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -26,3 +26,8 @@ export declare const isEqual: (val1: unknown, val2: unknown) => boolean; export declare function filterUnknownParamsClient(shape: T): string; export declare function filterUnknownParams(shape: T, searchParams?: object): T; export declare function assignValue(shape: T, curr: Partial, newVal: Partial): T; +export interface Router { + push: (href: string, opts: object) => void; + replace: (href: string, opts: object) => void; +} +export declare const routerHistory: Router; diff --git a/dist/utils.mjs b/dist/utils.mjs index 94329bd..904f4f8 100644 --- a/dist/utils.mjs +++ b/dist/utils.mjs @@ -1 +1 @@ -const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),c=t instanceof Date;return(e?"null":c&&"date")||r&&"array"||!e&&!c&&!r&&"object"===n&&"object"||n},n=()=>"undefined"==typeof window,e=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",c=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function o(t){const n=new URLSearchParams;return s(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,e])=>n.set(t,e))),n.toString()}function i(t,n){return Object.fromEntries(s(t,Object.entries(n||{})))}function s(t,n){const e=Object.keys(t);return n.filter((([t])=>e.includes(t))).map((([t,n])=>[t.replaceAll("+"," "),n]))}function a(t,n,e){const r=Object.assign({},t,n);return Object.entries(t).forEach((([n])=>{const c=n,o=void 0!==e[c];r[c]=o?e[c]:t[c]})),r}export{a as assignValue,i as filterUnknownParams,o as filterUnknownParamsClient,e as getParams,c as isEqual,n as isSSR,t as typeOf}; +const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),o=t instanceof Date;return(e?"null":o&&"date")||r&&"array"||!e&&!o&&!r&&"object"===n&&"object"||n},n=()=>"undefined"==typeof window,e=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",o=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function i(t){const n=new URLSearchParams;return s(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,e])=>n.set(t,e))),n.toString()}function c(t,n){return Object.fromEntries(s(t,Object.entries(n||{})))}function s(t,n){const e=Object.keys(t);return n.filter((([t])=>e.includes(t))).map((([t,n])=>[t.replaceAll("+"," "),n]))}function a(t,n,e){const r=Object.assign({},t,n);return Object.entries(t).forEach((([n])=>{const o=n,i=void 0!==e[o];r[o]=i?e[o]:t[o]})),r}const l={push:t=>{window&&window.history.pushState(null,"",t)},replace:t=>{window&&window.history.replaceState(null,"",t)}};export{a as assignValue,c as filterUnknownParams,i as filterUnknownParamsClient,e as getParams,o as isEqual,n as isSSR,l as routerHistory,t as typeOf}; diff --git a/package.json b/package.json index 12f5d24..a40f5f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "state-in-url", - "version": "3.1.0", + "version": "4.0.0", "description": "Easily share complex state objects between unrelated React components, preserve types and structure, with TS validation. Deep links and url state synchronization wthout any hasssle or boilerplate.", "homepage": "https://state-in-url.dev", "keywords": [