diff --git a/package-lock.json b/package-lock.json
index 35b3c42..553f7a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3148,6 +3148,7 @@
"jest-resolve": "^26.6.2",
"jest-util": "^26.6.2",
"jest-worker": "^26.6.2",
+ "node-notifier": "^8.0.0",
"slash": "^3.0.0",
"source-map": "^0.6.0",
"string-length": "^4.0.1",
@@ -7051,7 +7052,8 @@
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
- "optionator": "^0.8.1"
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
},
"bin": {
"escodegen": "bin/escodegen.js",
@@ -11273,6 +11275,7 @@
"@types/node": "*",
"anymatch": "^3.0.3",
"fb-watchman": "^2.0.0",
+ "fsevents": "^2.1.2",
"graceful-fs": "^4.2.4",
"jest-regex-util": "^26.0.0",
"jest-serializer": "^26.6.2",
@@ -13969,6 +13972,9 @@
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.6"
+ },
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -14767,6 +14773,9 @@
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
"integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==",
"dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
"bin": {
"make-plural": "bin/make-plural"
},
@@ -17945,6 +17954,9 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.18.0.tgz",
"integrity": "sha512-LhuQQp3WpnHo3HlKCRrdMXpB6jdLsGOoXXSfMjbv74s5VdV3WZhkYJT0Z6w/EH3UgPH+g/S9T4GJrKW/5iD8TA==",
"dev": true,
+ "dependencies": {
+ "fsevents": "~2.1.2"
+ },
"bin": {
"rollup": "dist/bin/rollup"
},
diff --git a/src/QueryStateProvider.tsx b/src/QueryStateProvider.tsx
deleted file mode 100644
index 3202564..0000000
--- a/src/QueryStateProvider.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as React from 'react'
-
-import QueryStateContext from './queryState.context'
-
-const QueryStateProvider: React.FunctionComponent = ({ children }) => {
- const [state, setState] = React.useState({})
- return (
-
- {children}
-
- )
-}
-
-export default QueryStateProvider
diff --git a/src/_internal/isClientSide.ts b/src/_internal/isClientSide.ts
new file mode 100644
index 0000000..9dbc8cb
--- /dev/null
+++ b/src/_internal/isClientSide.ts
@@ -0,0 +1 @@
+export const isClientSide: boolean = typeof document === 'object'
diff --git a/src/_internal/parseQueryString.ts b/src/_internal/parseQueryString.ts
new file mode 100644
index 0000000..606417a
--- /dev/null
+++ b/src/_internal/parseQueryString.ts
@@ -0,0 +1,45 @@
+import { isEmpty, mapValues, pickBy, isNil } from 'lodash'
+
+export const parseUrlString = (filter: any): any => {
+ if (filter instanceof Date) {
+ return filter.toISOString()
+ }
+ if (Array.isArray(filter)) {
+ if (filter.length === 0) {
+ return undefined
+ }
+ return filter.map(parseUrlString)
+ }
+ if (typeof filter === 'object') {
+ if (isEmpty(filter)) {
+ return undefined
+ }
+ if (filter['0']) {
+ return Object.values(filter)
+ }
+ return parseQueryString(filter)
+ }
+ if (filter === 'true') {
+ return true
+ }
+ if (filter === 'false') {
+ return false
+ }
+ try {
+ const numberInput = Number(filter.trim())
+ if (!isNaN(numberInput)) {
+ return Math.round(numberInput)
+ }
+ } catch (e) {
+ //
+ }
+ return filter
+}
+
+export const parseQueryString = (filters: any) => {
+ const state = pickBy(mapValues(filters, parseUrlString), (el) => !isNil(el))
+ if (isEmpty(state)) {
+ return undefined
+ }
+ return state
+}
diff --git a/src/cleanQueryState.ts b/src/cleanQueryState.ts
deleted file mode 100644
index 04e1de4..0000000
--- a/src/cleanQueryState.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { isEmpty, mapValues, pickBy, isNil, isFunction } from 'lodash'
-
-type CustomCleanState = (filter: any, key?: string | number) => any
-
-export const cleanState = (customCleanState?: CustomCleanState) => (
- filter: any,
- key?: string | number
-): any => {
- const customCleanedFilters = isFunction(customCleanState)
- ? customCleanState(filter, key)
- : undefined
- if (customCleanedFilters) {
- return customCleanedFilters
- }
- if (filter instanceof Date) {
- return filter.toISOString()
- }
- if (Array.isArray(filter)) {
- if (filter.length === 0) {
- return undefined
- }
- return filter.map(cleanState(customCleanState))
- }
- if (typeof filter === 'object') {
- if (isEmpty(filter)) {
- return undefined
- }
- if (filter['0']) {
- return Object.values(filter)
- }
- return cleanQueryState(filter, customCleanState)
- }
- if (filter === 'true') {
- return true
- }
- if (filter === 'false') {
- return false
- }
- try {
- const numberInput = Number(filter.trim())
- if (!isNaN(numberInput)) {
- return Math.round(numberInput)
- }
- } catch (e) {
- //
- }
- return filter
-}
-
-export const cleanQueryState = (
- filters: any,
- customCleanFilter?: CustomCleanState
-) => {
- const state = pickBy(
- mapValues(filters, cleanState(customCleanFilter)),
- (el) => !isNil(el)
- )
- if (isEmpty(state)) {
- return undefined
- }
- return state
-}
-
-export default cleanQueryState
diff --git a/src/index.ts b/src/index.ts
index 1da9df8..97d0d87 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,2 @@
-export { default as useQueryState } from './useQueryState'
-export { default as cleanQueryState } from './cleanQueryState'
-export { default as QueryStateProvider } from './QueryStateProvider'
-export { UseQueryStateOptions, UrlOptions } from './types'
+export { useQueryState } from './useQueryState'
+export { UseQueryStateOptions } from './types'
diff --git a/src/queryState.context.ts b/src/queryState.context.ts
deleted file mode 100644
index bc1ee17..0000000
--- a/src/queryState.context.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as React from 'react'
-
-type QueryStateContext = { state: any; setState: (value: any) => void }
-
-export default React.createContext({} as QueryStateContext)
diff --git a/src/types.ts b/src/types.ts
index b0f498e..9f18484 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,15 +1,21 @@
-export type UrlOptions = {
- shouldUseGetRoot?: boolean
- persist?: boolean
- persistInitial?: boolean
-}
-
-export type UseQueryStateOptions = {
+export type UseQueryStateOptions = {
+ /**
+ * Default value used in state
+ * @default {}
+ */
defaultValue?: FiltersType
- customClean?: (filter: any, key?: string | number) => any | void
- url?: UrlOptions
- value?: FiltersType
- onChange?: (groupName: string, value: FiltersType) => void
- persistValue?: (groupName: string, value: FiltersType) => void
- groupName?: string
+ /**
+ * Persist state in query url
+ * @default true
+ */
+ persist?: boolean
+ /**
+ * If provided, state is persisted in session storage with the key provided
+ */
+ cacheKey?: string
+ /**
+ * Overwrites default parse url function
+ * @param url
+ */
+ parseSearch?: (search: string) => object | undefined
}
diff --git a/src/usePersistUrlFilters.ts b/src/usePersistUrlFilters.ts
deleted file mode 100644
index 71c6020..0000000
--- a/src/usePersistUrlFilters.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as qs from 'qs'
-import * as React from 'react'
-
-import { UrlOptions } from './types'
-
-const usePersistUrlFilters = (groupName: string, config: UrlOptions = {}) => {
- const { shouldUseGetRoot = false, persist = true } = config
-
- return React.useCallback(
- (filters = {}) => {
- const location = window.location
- const history = window.history
-
- const params = shouldUseGetRoot
- ? filters
- : { ...qs.parse(location.search.slice(1)), [groupName]: filters }
- const url = `${location.origin}${location.pathname}`
- if (params && Object.values(params).length > 0) {
- if (persist) {
- history.replaceState(
- '',
- '',
- `${url}?${qs.stringify(params, { encode: false })}`
- )
- }
- } else {
- history.replaceState('', '', url)
- }
- },
- [groupName, persist, shouldUseGetRoot]
- )
-}
-
-export default usePersistUrlFilters
diff --git a/src/useQueryState.ts b/src/useQueryState.ts
index e95a92d..a23c98f 100644
--- a/src/useQueryState.ts
+++ b/src/useQueryState.ts
@@ -1,88 +1,67 @@
-import { get, isEmpty } from 'lodash'
+import { isEmpty } from 'lodash'
+import * as qs from 'qs'
import * as React from 'react'
-import cleanQueryState from './cleanQueryState'
-import queryStateContext from './queryState.context'
+import { isClientSide } from './_internal/isClientSide'
+import { parseQueryString } from './_internal/parseQueryString'
import { UseQueryStateOptions } from './types'
-import usePersistUrlFilters from './usePersistUrlFilters'
-import useUrlQuery from './useUrlQuery'
-const useQueryState = (
- options: UseQueryStateOptions
+export const useQueryState = (
+ options?: UseQueryStateOptions
) => {
- const initialized = React.useRef(false)
-
- const { state, setState } = React.useContext(queryStateContext)
-
- if (!state && options.onChange === undefined) {
- throw new Error(
- 'Cannot use uncontrolled mode of useQueryState without QueryStateProvider'
- )
- }
+ const [state, setState] = React.useState(() => {
+ if (isClientSide) {
+ const search = window.location.search.slice(1)
+ const parsedQueryString =
+ options?.parseSearch?.(search) ?? parseQueryString(qs.parse(search))
+ if (parsedQueryString) {
+ return parsedQueryString
+ }
- const groupName = options.groupName ?? 'query'
+ if (options?.cacheKey) {
+ try {
+ const sessionStorage = window.sessionStorage.getItem(options.cacheKey)
+ if (!sessionStorage) {
+ return options.defaultValue
+ }
+ return JSON.parse(sessionStorage)
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Unable to parse query string state stored in session storage: ${e}`
+ )
+ }
+ }
- const handleCleanFilters = React.useCallback(
- (dirtyFilters: any) =>
- cleanQueryState(dirtyFilters, options.customClean) as FiltersType,
- [options.customClean]
- )
+ return options?.defaultValue
+ }
+ })
- const handleSetUrl = usePersistUrlFilters(groupName, options.url)
- const urlFilters = useUrlQuery(groupName, options.url)
- const cleanedUrlFilters = handleCleanFilters(urlFilters)
+ const shouldPersistUrl = options?.persist ?? true
- const currentValueRef = React.useRef(
- cleanedUrlFilters ?? options.defaultValue
- )
- const localValue = state?.[groupName] ?? options.value
- if (localValue) {
- currentValueRef.current = localValue
- }
- const currentValue = currentValueRef.current
+ React.useEffect(() => {
+ if (isClientSide) {
+ if (shouldPersistUrl) {
+ const location = window.location
+ const history = window.history
+ const url = `${location.origin}${location.pathname}`
- const handleSetFilter = React.useCallback(
- (
- newFilters: FiltersType = {} as FiltersType,
- filtersOptions?: { save?: boolean }
- ) => {
- if (groupName && get(filtersOptions, 'save', true)) {
- options.persistValue &&
- options.persistValue(groupName, handleCleanFilters(newFilters))
+ if (!isEmpty(state)) {
+ history.replaceState('', '', `${url}?${qs.stringify(state)}`)
+ } else {
+ history.replaceState('', '', url)
+ }
}
- options.onChange && options.onChange(groupName, newFilters)
- setState &&
- setState((oldState: any) => ({ ...oldState, [groupName]: newFilters }))
- },
- [groupName, options, setState, handleCleanFilters]
- )
- React.useEffect(() => {
- if (
- currentValue &&
- initialized.current &&
- currentValue !== options.defaultValue
- ) {
- handleSetUrl(currentValue)
- }
- }, [currentValue, handleSetUrl, options.defaultValue])
-
- React.useEffect(() => {
- if (groupName) {
- if (!isEmpty(urlFilters)) {
- handleSetFilter(cleanedUrlFilters, { save: false })
- } else if (options.url?.persistInitial) {
- handleSetUrl(currentValue)
+ if (options?.cacheKey) {
+ try {
+ window.sessionStorage.setItem(options.cacheKey, JSON.stringify(state))
+ } catch (e) {
+ console.warn(`Unable to stringify query string state: ${e}`) // eslint-disable-line no-console
+ }
}
- initialized.current = true
}
- }, []) // eslint-disable-line
+ }, [options?.cacheKey, shouldPersistUrl, state])
- return [
- currentValue,
- handleSetFilter,
- { isInitialized: initialized.current },
- ] as const
+ return [state, setState] as const
}
-
-export default useQueryState
diff --git a/src/useUrlQuery.ts b/src/useUrlQuery.ts
deleted file mode 100644
index 4df4b51..0000000
--- a/src/useUrlQuery.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as qs from 'qs'
-
-import { UrlOptions } from './types'
-
-const useUrlQuery = (groupName: string, config: UrlOptions = {}) => {
- const { shouldUseGetRoot = false } = config
-
- const search = qs.parse(window.location.search.slice(1))
-
- return shouldUseGetRoot ? search : search[groupName]
-}
-
-export default useUrlQuery
diff --git a/tests/useQueryState.spec.ts b/tests/useQueryState.spec.ts
index 0dc4064..27306a7 100644
--- a/tests/useQueryState.spec.ts
+++ b/tests/useQueryState.spec.ts
@@ -1,42 +1,36 @@
import { renderHook, act } from '@testing-library/react-hooks'
import * as qs from 'qs'
-import { QueryStateProvider, useQueryState } from '../src'
+import { useQueryState } from '../src'
describe('use query state', () => {
- describe('act a normal state', () => {
+ describe('act as normal state', () => {
it('should allow to set state', () => {
- const { result } = renderHook(
- () => useQueryState<{ value: string }>({}),
- { wrapper: QueryStateProvider }
- )
+ const { result } = renderHook(() => useQueryState<{ value: string }>({}))
const newState = { value: 'test' }
act(() => {
const [_state, setState] = result.current
setState(newState)
})
- expect(result.current[0]).toBe(newState)
+ expect(result.current[0]).toStrictEqual(newState)
})
- it('should be initialized after first render', () => {
+ it('should be initialized at first render', () => {
const defaultValue = { value: 'test' }
- const { result, rerender } = renderHook(
- () => useQueryState<{ value: string }>({ defaultValue }),
- { wrapper: QueryStateProvider }
+ const { result } = renderHook(() =>
+ useQueryState<{ value: string }>({ defaultValue })
)
- rerender() // init
- expect(result.current[2]?.isInitialized).toBeTruthy()
+ expect(result.current[0]).toStrictEqual(defaultValue)
})
it('should not change value after a rerender if value has been initialized', () => {
const defaultValue = { value: 'test' }
- const { result, rerender } = renderHook(
- () => useQueryState<{ value: string }>({ defaultValue }),
- { wrapper: QueryStateProvider }
+ const { result, rerender } = renderHook(() =>
+ useQueryState<{ value: string }>({ defaultValue })
)
rerender() // init
const test = result.current
rerender()
- expect(result.current[0]).toBe(test[0])
+ expect(result.current[0]).toStrictEqual(test[0])
})
})
describe('link with url', () => {
@@ -54,25 +48,16 @@ describe('use query state', () => {
})
})
it('should use url value as initial value as first render', () => {
- const { result } = renderHook(
- () =>
- useQueryState<{ value: string }>({
- url: { shouldUseGetRoot: true },
- defaultValue: { value: 'bad value' },
- }),
- { wrapper: QueryStateProvider }
+ const { result } = renderHook(() =>
+ useQueryState<{ value: string }>({
+ defaultValue: { value: 'bad value' },
+ })
)
expect(result.current[0]?.value).toBe(defaultValue.value)
})
it('should change the url according to the state', () => {
window.history.replaceState = jest.fn()
- const { result } = renderHook(
- () =>
- useQueryState<{ value: string }>({
- url: { shouldUseGetRoot: true },
- }),
- { wrapper: QueryStateProvider }
- )
+ const { result } = renderHook(() => useQueryState<{ value: string }>())
const newValue = { value: 'new value' }
act(() => {
const [_state, setState] = result.current
@@ -81,7 +66,7 @@ describe('use query state', () => {
expect(window.history.replaceState).toBeCalledWith(
'',
'',
- `${origin}${pathname}?${qs.stringify(newValue, { encode: false })}`
+ `${origin}${pathname}?${qs.stringify(newValue)}`
)
})
})