diff --git a/src/queryparams.ts b/src/queryparams.ts index 76a1aeb..aae5e3e 100644 --- a/src/queryparams.ts +++ b/src/queryparams.ts @@ -16,14 +16,18 @@ import { parse, ParsedQuery } from 'query-string'; */ export function queryParamsFromLocation< QueryParams extends Record ->( - location: { search?: string }, - defaultQueryParams: QueryParams, - debugName: string -): QueryParams { - const currentQueryParams = location.search - ? queryParamsFromSearch(location.search) - : {}; +>({ + location, + defaultQueryParams, + debugName +}: { + location?: { search?: string }; + defaultQueryParams: QueryParams; + debugName: string; +}): QueryParams { + const currentQueryParams = queryParamsFromSearch( + location ? location.search : window.location.search + ); const mergedQueryParams = { ...defaultQueryParams, ...currentQueryParams }; return convertQueryParamsToConcreteType( mergedQueryParams, @@ -32,9 +36,13 @@ export function queryParamsFromLocation< ); } -function queryParamsFromSearch(search: string): ParsedQuery { +function queryParamsFromSearch(search?: string): ParsedQuery { + if (!search) { + return {}; + } + if (search[0] === '?') { - search = search.substr(1); + search = search.slice(1); } return parse(search); diff --git a/src/useQueryParams.ts b/src/useQueryParams.ts index c963240..95ea54e 100644 --- a/src/useQueryParams.ts +++ b/src/useQueryParams.ts @@ -1,8 +1,8 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { queryParamsFromLocation } from './queryparams'; export type Config = { - location: { search?: string; key?: string }; + location?: { search?: string; key?: string }; defaultQueryParams: QueryParams; debugName?: string; }; @@ -55,20 +55,18 @@ export function useQueryParams>( ): QueryParams { const { location, defaultQueryParams, debugName = '' } = options; const [queryParams, setQueryParams] = useState(() => { - return queryParamsFromLocation(location, defaultQueryParams, debugName); + return queryParamsFromLocation({ location, defaultQueryParams, debugName }); }); - const [search, setSearch] = useState(location.search); - - if (search !== location.search) { - const params = queryParamsFromLocation( - location, - defaultQueryParams, - debugName + useEffect(() => { + setQueryParams( + queryParamsFromLocation({ + location, + defaultQueryParams, + debugName + }) ); - setQueryParams(params); - setSearch(location.search); - } + }, [location ? location.search : window.location.search]); return queryParams; } diff --git a/src/withQueryParams.tsx b/src/withQueryParams.tsx index 3276046..3d38fd0 100644 --- a/src/withQueryParams.tsx +++ b/src/withQueryParams.tsx @@ -50,13 +50,13 @@ export const withQueryParams = < const { location, ...props } = this.props; // @ts-expect-error accept that there might be a displayName; - const debug = WithQueryParams.displayName; + const debugName: string = WithQueryParams.displayName; - const typedQueryParams = queryParamsFromLocation( + const typedQueryParams = queryParamsFromLocation({ location, defaultQueryParams, - debug - ); + debugName + }); return ; } diff --git a/tests/queryparams.test.ts b/tests/queryparams.test.ts index ff9b6a4..7698599 100644 --- a/tests/queryparams.test.ts +++ b/tests/queryparams.test.ts @@ -2,23 +2,42 @@ import { queryParamsFromLocation } from '../src/queryparams'; describe('queryParamsFromLocation', () => { it('should know how to convert strings that start with a question mark', () => { - const location = { search: '?query=hallo' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?query=hallo' } + }); expect( - queryParamsFromLocation(location, { query: 'default' }, 'debug') - ).toEqual({ query: 'hallo' }); + queryParamsFromLocation({ + defaultQueryParams: { query: 'default' }, + debugName: 'debug' + }) + ).toEqual({ + query: 'hallo' + }); }); it('should know how to convert strings that start without a question mark', () => { - const location = { search: 'query=hallo' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: 'query=hallo' } + }); expect( - queryParamsFromLocation(location, { query: 'default' }, 'debug') - ).toEqual({ query: 'hallo' }); + queryParamsFromLocation({ + defaultQueryParams: { query: 'default' }, + debugName: 'debug' + }) + ).toEqual({ + query: 'hallo' + }); }); it('should fallback to default query parameters when params are misssing', () => { - const location = { search: '' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '' } + }); const defaultQueryParams = { number: 42, numbers: [1, 2, 3], @@ -31,11 +50,10 @@ describe('queryParamsFromLocation', () => { booleans: [false, true, false] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ number: 42, numbers: [1, 2, 3], @@ -60,14 +78,16 @@ describe('queryParamsFromLocation', () => { it('should warn when default query parameters does not contain a query param', () => { jest.spyOn(console, 'warn').mockImplementation(() => undefined); - const location = { search: '?color=red' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?color=red' } + }); const defaultQueryParams = {}; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ color: 'red' }); expect(console.warn).toHaveBeenCalledTimes(1); @@ -77,166 +97,187 @@ describe('queryParamsFromLocation', () => { }); it('should leave strings and unhandled types alone', () => { - const location = { search: '?color=red' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?color=red' } + }); const defaultQueryParams = { color: 'blue' }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ color: 'red' }); }); describe('number', () => { it('should know how to transform whole numbers', () => { - const location = { search: '?id=42' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?id=42' } + }); const defaultQueryParams = { id: 1 }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ id: 42 }); }); it('should know how to transform fractured numbers', () => { - const location = { search: '?id=1.12' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?id=1.12' } + }); const defaultQueryParams = { id: 33.3 }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ id: 1.12 }); }); }); describe('boolean', () => { it('should know how to transform string "true" to boolean true', () => { - const location = { search: '?visible=false' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?visible=false' } + }); const defaultQueryParams = { visible: true }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ visible: false }); }); it('should know how to transform string "false" to boolean false', () => { - const location = { search: '?visible=false' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?visible=false' } + }); const defaultQueryParams = { visible: true }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ visible: false }); }); }); describe('array', () => { it('should leave arrays of strings alone', () => { - const location = { search: '?sizes=medium&sizes=large&sizes=small' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?sizes=medium&sizes=large&sizes=small' } + }); const defaultQueryParams = { sizes: ['small'] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ sizes: ['medium', 'large', 'small'] }); }); it('should know how to tranform arrays of numbers', () => { - const location = { search: '?numbers=1&numbers=2&numbers=3' }; + global.window.location = { + search: '?numbers=1&numbers=2&numbers=3' + } as Location; const defaultQueryParams = { numbers: [42] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ numbers: [1, 2, 3] }); }); it('should know how to tranform arrays of booleans', () => { - const location = { + global.window.location = { search: '?visible=true&visible=false&visible=false' - }; + } as Location; const defaultQueryParams = { visible: [true] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ visible: [true, false, false] }); }); it('should when it cannot guess the type, due to empty default, revert to strings', () => { - const location = { search: '?sizes=medium&sizes=large&sizes=small' }; + global.window.location = { + search: '?sizes=medium&sizes=large&sizes=small' + } as Location; const defaultQueryParams = { sizes: [] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ sizes: ['medium', 'large', 'small'] }); }); describe('when encountering a singular value', () => { it('should convert to an array of strings when a singular string is given', () => { - const location = { search: '?sizes=large' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?sizes=large' } + }); const defaultQueryParams = { sizes: ['small'] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ sizes: ['large'] }); }); it('should convert to an array of numbers when a singular number is given', () => { - const location = { search: '?numbers=1' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?numbers=1' } + }); const defaultQueryParams = { numbers: [42] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ numbers: [1] }); }); it('should convert to an array of booleans when a singular boolean is given', () => { - const location = { search: '?visible=true' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?visible=true' } + }); const defaultQueryParams = { visible: [false] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ visible: [true] }); }); it('should convert to an array of the singular value when an empty default is given', () => { - const location = { search: '?sizes=medium' }; + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '?sizes=medium' } + }); const defaultQueryParams = { sizes: [] }; - const queryParams = queryParamsFromLocation( - location, + const queryParams = queryParamsFromLocation({ defaultQueryParams, - 'AppComponent' - ); + debugName: 'AppComponent' + }); expect(queryParams).toEqual({ sizes: ['medium'] }); }); }); diff --git a/tests/setupTests.ts b/tests/setupTests.ts index f905a4a..1399ce3 100644 --- a/tests/setupTests.ts +++ b/tests/setupTests.ts @@ -1,4 +1,8 @@ beforeEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '' } + }); }); diff --git a/tests/useQueryParams.test.tsx b/tests/useQueryParams.test.tsx index 410124d..955a3f5 100644 --- a/tests/useQueryParams.test.tsx +++ b/tests/useQueryParams.test.tsx @@ -75,4 +75,47 @@ describe('useQueryParams', () => { expect(result.current).toEqual({ query: 'default' }); }); + + test('that it uses window location when not providing location', () => { + const mockSearch = jest.fn(); + Object.defineProperty(window, 'location', { + value: { + get search() { + return mockSearch(); + } + } + }); + mockSearch.mockReturnValue('?query=red'); + + const defaultQueryParams = { query: 'default' }; + + const { result, rerender } = renderHook>( + (config: Config) => useQueryParams(config), + { + initialProps: { + defaultQueryParams + } + } + ); + + // Test that the first result will be 'hallo' + const initialResult = result.current; + expect(initialResult).toEqual({ query: 'red' }); + + // Change to something else + mockSearch.mockReturnValue('?query=blue'); + rerender({ + defaultQueryParams + }); + + expect(result.current).toEqual({ query: 'blue' }); + + // Change to empty string so it uses the default + mockSearch.mockReturnValue(''); + rerender({ + defaultQueryParams + }); + + expect(result.current).toEqual({ query: 'default' }); + }); });