diff --git a/package-lock.json b/package-lock.json index 72a27ac0..acc3b9bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "state-in-url", - "version": "4.0.1", + "version": "4.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "state-in-url", - "version": "4.0.1", + "version": "4.0.3", "license": "MIT", "workspaces": [ "packages/urlstate", @@ -33797,21 +33797,6 @@ "name": "state-in-url", "version": "1.0.0", "license": "ISC" - }, - "packages/example-nextjs15/node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz", - "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index e2e3d613..cb88612e 100644 --- a/package.json +++ b/package.json @@ -3,32 +3,6 @@ "version": "4.0.3", "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": [ - "front-end", - "state-management", - "state management", - "deep links", - "deep linking", - "url synchronization", - "useUrlState", - "state in url", - "client components communication", - "query string", - "search params", - "query params", - "typescript", - "workflow", - "javascript", - "hooks", - "react.js", - "reactjs", - "react", - "NextJS", - "Next.js", - "nuqs alternative", - "query params parser", - "query params parsing" - ], "repository": { "type": "git", "url": "git+https://github.com/asmyshlyaev177/state-in-url.git" @@ -404,5 +378,31 @@ "wait-on": "^8.0.1", "wireit": "^0.14.9" }, + "keywords": [ + "front-end", + "state-management", + "state management", + "deep links", + "deep linking", + "url synchronization", + "useUrlState", + "state in url", + "client components communication", + "query string", + "search params", + "query params", + "typescript", + "workflow", + "javascript", + "hooks", + "react.js", + "reactjs", + "react", + "NextJS", + "Next.js", + "nuqs alternative", + "query params parser", + "query params parsing" + ], "packageManager": "npm@10.8.2" } diff --git a/packages/urlstate/next/useUrlState/useUrlState.ts b/packages/urlstate/next/useUrlState/useUrlState.ts index c7077173..6bced38b 100644 --- a/packages/urlstate/next/useUrlState/useUrlState.ts +++ b/packages/urlstate/next/useUrlState/useUrlState.ts @@ -66,10 +66,8 @@ export function useUrlState({ ); const updateUrl = React.useCallback( - (value?: Parameters[0], options?: Options) => { - const _opts = { ...defaultOptions, ...opts, ...options }; - updateUrlBase(value, _opts); - }, + (value?: Parameters[0], options?: Options) => + updateUrlBase(value, { ...defaultOptions, ...opts, ...options }), [updateUrlBase, opts], ); diff --git a/packages/urlstate/react-router/useUrlState/useUrlState.ts b/packages/urlstate/react-router/useUrlState/useUrlState.ts index ca59914e..ff499258 100644 --- a/packages/urlstate/react-router/useUrlState/useUrlState.ts +++ b/packages/urlstate/react-router/useUrlState/useUrlState.ts @@ -63,13 +63,8 @@ export function useUrlState({ ); const updateUrl = React.useCallback( - ( - value?: Parameters[0], - options?: NavigateOptions, - ) => { - const opts = { ...defaultOpts, ...initOpts, ...options }; - updateUrlBase(value, opts); - }, + (value?: Parameters[0], options?: NavigateOptions) => + updateUrlBase(value, { ...defaultOpts, ...initOpts, ...options }), [initOpts], ); diff --git a/packages/urlstate/useSharedState/useSharedState.ts b/packages/urlstate/useSharedState/useSharedState.ts index 1dc10f4b..62bea339 100644 --- a/packages/urlstate/useSharedState/useSharedState.ts +++ b/packages/urlstate/useSharedState/useSharedState.ts @@ -60,9 +60,7 @@ export function useSharedState( const newVal = isFunc ? value(curr) : { ...curr, ...value }; if (isEqual(curr, newVal)) return void 0; stateMap.set(stateShape.current, newVal); - subscribers.get(stateShape.current).forEach((sub) => { - sub(); - }); + subscribers.get(stateShape.current).forEach((sub) => sub()); }, [], ); @@ -79,9 +77,10 @@ export function useSharedState( }, []); // get state without deps - const getState = React.useCallback(() => { - return stateMap.get(stateShape.current) || stateShape.current; - }, []); + const getState = React.useCallback( + () => stateMap.get(stateShape.current) || stateShape.current, + [], + ); return { state, getState, setState }; } diff --git a/packages/urlstate/useUrlEncode/useUrlEncode.ts b/packages/urlstate/useUrlEncode/useUrlEncode.ts index 7567c816..00d4458a 100644 --- a/packages/urlstate/useUrlEncode/useUrlEncode.ts +++ b/packages/urlstate/useUrlEncode/useUrlEncode.ts @@ -25,10 +25,7 @@ import { type JSONCompatible, typeOf } from "../utils"; */ export function useUrlEncode(stateShape: T) { const stringify = React.useCallback( - function ( - state: typeof stateShape, - paramsToKeep?: string | URLSearchParams, - ): string { + (state: typeof stateShape, paramsToKeep?: string | URLSearchParams) => { return typeOf(state) === "object" ? encodeState(state, stateShape, paramsToKeep) : ""; @@ -37,9 +34,8 @@ export function useUrlEncode(stateShape: T) { ); const parse = React.useCallback( - function (strOrSearchParams: string | URLSearchParams) { - return decodeState(strOrSearchParams, stateShape) as typeof stateShape; - }, + (strOrSearchParams: string | URLSearchParams) => + decodeState(strOrSearchParams, stateShape) as typeof stateShape, [stateShape], ); diff --git a/packages/urlstate/useUrlStateBase/useUrlStateBase.test.ts b/packages/urlstate/useUrlStateBase/useUrlStateBase.test.ts index b83aac26..52767577 100644 --- a/packages/urlstate/useUrlStateBase/useUrlStateBase.test.ts +++ b/packages/urlstate/useUrlStateBase/useUrlStateBase.test.ts @@ -225,7 +225,7 @@ describe('useUrlStateBase', () => { result.current.updateUrl({ ...shape, num: 50 }); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(result.current.state).toStrictEqual({ ...shape, num: 50 }); expect(router.push).toHaveBeenCalledTimes(1); @@ -240,7 +240,7 @@ describe('useUrlStateBase', () => { result.current.updateUrl({ num: 50 }); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(result.current.state).toStrictEqual({ ...shape, num: 50 }); expect(router.push).toHaveBeenCalledTimes(1); @@ -258,7 +258,7 @@ describe('useUrlStateBase', () => { result.current.updateUrl(); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(result.current.state).toStrictEqual({ ...shape, num: 50 }); expect(router.push).toHaveBeenCalledTimes(1); @@ -274,7 +274,7 @@ describe('useUrlStateBase', () => { result.current.updateUrl((curr) => ({ ...curr, num: 50 })); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(result.current.state).toStrictEqual({ ...shape, num: 50 }); expect(router.push).toHaveBeenCalledTimes(1); @@ -309,7 +309,7 @@ describe('useUrlStateBase', () => { result.current.updateUrl(newState); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(router.push).toHaveBeenCalledTimes(1); expect(router.push).toHaveBeenNthCalledWith(1, `/?num=55${hash}`, {}); @@ -328,7 +328,7 @@ describe('useUrlStateBase', () => { }); }); - await jest.runAllTimersAsync(); + await new Promise(process.nextTick); expect(result.current.state).toStrictEqual(newState); expect(router.replace).toHaveBeenCalledTimes(1); diff --git a/packages/urlstate/utils.test.ts b/packages/urlstate/utils.test.ts index 65bff282..29048389 100644 --- a/packages/urlstate/utils.test.ts +++ b/packages/urlstate/utils.test.ts @@ -1,4 +1,4 @@ -import { getParams, typeOf, assignValue } from './utils'; +import { getParams, typeOf, assignValue, filterUnknownParamsClient } from './utils'; describe('typeOf', () => { it('string', () => { @@ -128,3 +128,69 @@ describe('assignValue', () => { }) const clone = (obj: object) => JSON.parse(JSON.stringify(obj)) + +describe('filterUnknownParamsClient', () => { + afterAll(() => { + jest.resetAllMocks() + }) + + it('should include only the keys that exist in the shape', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "?foo=bar&baz=qux", + })); + const result = filterUnknownParamsClient({ foo: '', dummy: '' }); + expect(result).toBe("foo=bar"); + }); + + it('should return an empty string if no keys match the shape', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "?foo=bar&baz=qux", + })); + const result = filterUnknownParamsClient({ noKey: '' }); + expect(result).toBe(""); + }); + + it('should handle an empty URL search string returning an empty string', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "", + })); + const result = filterUnknownParamsClient({ someKey: '' }); + expect(result).toBe(""); + }); + + it('should handle an empty shape returning an empty string', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "?foo=bar&baz=qux", + })); + const result = filterUnknownParamsClient({}); + expect(result).toBe(""); + }); + + it('should handle special characters correctly', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "?foo=bar%20baz&baz=qux", + })); + const result = filterUnknownParamsClient({ foo: '' }); + expect(result).toBe("foo=bar+baz"); + }); + + it('should manage repeated keys and take the last one', () => { + const originalLocation = window.location; + jest.spyOn(window, 'location', 'get').mockImplementation(() => ({ + ...originalLocation, + search: "?foo=first&foo=second", + })); + const result = filterUnknownParamsClient({ foo: '' }); + expect(result).toBe("foo=second"); + }); +}); diff --git a/packages/urlstate/utils.ts b/packages/urlstate/utils.ts index ec8fd673..1e7887f2 100644 --- a/packages/urlstate/utils.ts +++ b/packages/urlstate/utils.ts @@ -73,7 +73,6 @@ export type UnknownObj = object | { [key: string]: unknown }; export const isEqual = (val1: unknown, val2: unknown) => JSON.stringify(val1) === JSON.stringify(val2); -// TODO: tests export function filterUnknownParamsClient(shape: T) { const shapeParams = new URLSearchParams();