NextJS JSON state in URL query param
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { parseUrlState } from './utils.query'; | |
import useQueryString from './useQueryString' | |
type CatInfo = { name: string, age: number }; | |
const defaultCatInfo = { name: 'Robert Paulson', age: 1 }; | |
export const Component = () => { | |
const [cat, setCat] = useQueryString<CatInfo>({ | |
key: 'cat', | |
defaultValue: defaultCatInfo, | |
parser: parseUrlState, | |
toString: catToString, | |
}); | |
return <h1>His name is <strong>{cat.name}</strong></h1> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useMemo, useState } from 'react'; | |
import { useRouter } from 'next/router'; | |
import { getQueryStringValue, updateQueryString } from './utils.query'; | |
interface Props<T> { | |
key: string; | |
defaultValue: T; | |
parser: (val: string | string[]) => T; | |
toString: (val: T) => string; | |
pushUrls?: boolean; | |
} | |
function useQueryString<T>({ | |
key, | |
defaultValue, | |
parser, | |
toString, | |
pushUrls = false, | |
}: Props<T>): [T, React.Dispatch<React.SetStateAction<T>>] { | |
const router = useRouter(); | |
const [loading, setLoading] = useState(false); | |
const [value, setValue] = useState<T>(() => { | |
const queryParamValue = getQueryStringValue(router, key); | |
if (queryParamValue) { | |
return parser(queryParamValue) || defaultValue; | |
} | |
return defaultValue; | |
}); | |
const onSetValue = useCallback( | |
async (newValue: T | ((existing: T) => T)) => { | |
setLoading(true); | |
if (isFunction(newValue)) { | |
setValue((existing) => { | |
const updatedValue = newValue(existing); | |
updateQueryString({ | |
router, | |
key, | |
value: toString(updatedValue), | |
push: pushUrls, | |
}).then(() => setLoading(false)); | |
setLoading(false); | |
return updatedValue; | |
}); | |
} else { | |
setValue(newValue); | |
updateQueryString({ router, key, value: toString(newValue), push: pushUrls }).then(() => | |
setLoading(false), | |
); | |
} | |
}, | |
[router, key, toString, pushUrls], | |
); | |
const queryParamValue = router.query[key]; | |
const queryParamMatches = useMemo(() => { | |
if (!router.isReady) { | |
return false; | |
} | |
const valueIsUndefinedAsQuery = value === undefined || value === defaultValue; | |
const expectedQueryValue = valueIsUndefinedAsQuery ? undefined : toString(value); | |
return queryParamValue === expectedQueryValue; | |
}, [value, toString, queryParamValue, defaultValue, router.isReady]); | |
// Update value on url change | |
useEffect(() => { | |
if (!router.isReady || queryParamMatches || loading) { | |
return; | |
} | |
setValue(parser(queryParamValue) || defaultValue); | |
}, [setValue, queryParamMatches, defaultValue, queryParamValue, parser, router.isReady, loading]); | |
return [value, onSetValue]; | |
} | |
// eslint-disable-next-line @typescript-eslint/ban-types | |
const isFunction = (obj: unknown): obj is Function => { | |
return typeof obj === 'function'; | |
}; | |
export default useQueryString; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const b64Decode = (base64: string): string => { | |
return Buffer.from(base64, 'base64').toString('utf-8'); | |
}; | |
export const b64encode = (data: string): string => { | |
return Buffer.from(data, 'utf-8').toString('base64'); | |
}; | |
export function isValidUrlSafeBase64(value: any): boolean { | |
if (typeof value !== 'string') { | |
return false; | |
} | |
return encodeUrlSafeBase64(decodeUrlSafeBase64(value)) === value; | |
} | |
export function decodeUrlSafeBase64(value: string): string { | |
const base64Value = value.replace(/-/g, '/').replace(/_/g, '+'); | |
return b64Decode(base64Value); | |
} | |
export function encodeUrlSafeBase64(value: string): string { | |
const base64Value = b64encode(value); | |
return base64Value.replace(/\//g, '-').replace(/\+/g, '_'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import qs from 'query-string'; | |
import { NextRouter } from 'next/router'; | |
import { encodeUrlSafeBase64, decodeUrlSafeBase64 } from 'utils.encoding'; | |
export const getQueryStringValue = (router: NextRouter, key: string) => { | |
if (router.isReady) { | |
return router.query[key]; | |
} | |
if (typeof window !== 'undefined') { | |
const values = qs.parse(window.location.search); | |
return values[key]; | |
} | |
// eslint-disable-next-line no-console | |
console.log(`Unable to get query param ${key}`); | |
}; | |
type UpdateQueryStringArgs = { | |
router: NextRouter; | |
key: string; | |
value: string; | |
push?: boolean; | |
}; | |
export const updateQueryString = async ({ | |
router, | |
key, | |
value, | |
push = true, | |
}: UpdateQueryStringArgs) => { | |
if (push) { | |
await router.push({ query: { ...router.query, [key]: value } }, undefined, { | |
shallow: true, | |
}); | |
} else { | |
await router.replace({ query: { ...router.query, [key]: value } }, undefined, { | |
shallow: true, | |
}); | |
} | |
}; | |
export function parseUrlState<T = string>( | |
stateString: string | string[] | undefined, | |
): T | undefined { | |
if (!stateString) { | |
return undefined; | |
} | |
try { | |
return JSON.parse(decodeUrlSafeBase64(String(stateString))) as T; | |
} catch (err) { | |
throw new Error(`Unable to parse url state string ${statestring}`); | |
} | |
} | |
export const dataToUrlString = (state: Record<string, unknown>): string => { | |
try { | |
return encodeUrlSafeBase64(JSON.stringify(state)); | |
} catch (err) { | |
throw new Error(`Unable to stringify ${state}`); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment