UNPKG

qs-state-hook

Version:
1 lines 13.9 kB
{"version":3,"file":"index.mjs","sources":["../src/mini-history.ts","../src/qs-state-hook.ts"],"sourcesContent":["// Create a history object to handle push state history.\n\nexport type QsLocation = { search: string };\n\nconst theLocation =\n typeof window !== 'undefined' ? window.location : { search: '' };\n\nconst push = ({ search }: QsLocation) => {\n const url = new URL(theLocation.toString?.() || '');\n url.search = search;\n history.pushState('', '', url.toString());\n};\n\nexport default {\n location: theLocation,\n push\n};\n","import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport qs from 'qs';\nimport debounce from 'lodash.debounce';\n\nimport history, { QsLocation } from './mini-history';\n\n/**\n * Compares two values using JSON stringification.\n *\n * @param {mixed} a Data to compare\n * @param {mixed} b Data to compare\n */\nexport function isEqualObj(a: any, b: any) {\n // Exist early if they're the same.\n if (a === b) return true;\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n// Each hook handles a single value, however there are times where multiple\n// hooks are called simultaneously triggering several url updates. An example of\n// this would be a map state. We'd have a hook for the center point and another\n// for the zoom level, however these 2 values should be stored in the url at the\n// same time to ensure a proper navigation. When we press the back key we want\n// to go back to the previous center and zoom, and not only one at a time. The\n// solution for this is to store the properties to be changed and only commit\n// the to the url once all the changes were made. This is done using a debounced\n// function that triggers 100ms after the last action.\nexport const COMMIT_DELAY = 100;\nlet commitQueue: { [key: string]: string | null } = {};\n\nconst identityFn = <T>(v: T): T => v;\n\nexport interface QsStateCreatorOptions {\n commit?: (args: QsLocation) => void;\n location?: QsLocation;\n}\n\nexport function useQsStateCreator(options: QsStateCreatorOptions = {}) {\n const { commit: commitToLocation = history.push } = options;\n\n // location is mutable. Use with useRef to prevent linting errors.\n const location = useRef<QsLocation>({ search: '' });\n location.current = options.location || history.location;\n\n const commit = useMemo(\n () =>\n debounce(() => {\n const parsedQS = qs.parse(location.current?.search || '', {\n ignoreQueryPrefix: true\n });\n\n // New object that is going to be stringified to the url.\n // Current properties plus the ones to be changed.\n const qsObject = {\n ...parsedQS,\n ...commitQueue\n };\n\n commitToLocation({\n search: qs.stringify(qsObject, { skipNulls: true })\n });\n // Once the commit happens clear the commit queue.\n commitQueue = {};\n }, COMMIT_DELAY),\n [commitToLocation]\n );\n\n const storeInURL = useCallback(\n (k: string, v: string | null) => {\n // Store the new value in the queue.\n commitQueue = {\n ...commitQueue,\n [k]: v\n };\n // Try to commit.\n commit();\n },\n [commit]\n );\n\n return useMemo(\n () => qsStateFactory({ storeInURL, location: location.current }),\n [storeInURL]\n );\n}\n\nexport interface QsStateFactoryOptions {\n storeInURL: (k: string, v: string | null) => void;\n location: QsLocation;\n}\n\nexport interface QsStateDefinition<T> {\n /** Key name to use on the search string */\n key: string;\n\n /**\n * Default value. Note that when a state value is default it won't go to the\n * search string.\n */\n default: T | null;\n\n /**\n * Any transformation to apply to the value from the search string before\n * using it. Converting to number for example.\n */\n hydrator?: (value?: string) => T | null;\n\n /**\n * Any transformation to apply to the value before adding it to the search.\n * Converting a number to string for example.\n */\n dehydrator?: (value?: T | null) => string;\n\n /**\n * Validator function for the value.\n * If is an array should contain valid options. If it is a function should\n * return true or false.\n */\n validator?: ((value?: T | null) => boolean) | Array<T>;\n}\n\n\nexport type QsStateHookReturn<T> = [T, (value: T | null) => void]\n\nfunction qsStateFactory({ storeInURL, location }: QsStateFactoryOptions) {\n /**\n * Qs State is used to sync a state object with url search string.\n * It will keep the state value in sync with the url.\n * The value will be validated according to the state definition\n *\n * Example:\n * {\n * key: 'field',\n * hydrator: (v) => v,\n * dehydrator: (v) => v,\n * default: 'all',\n * validator: [1, 2, 3]\n * }\n *\n * {string} key - Key name to use on the search string\n * {func} hydrator - Any transformation to apply to the value from the search\n * string before using it. Converting to number for example.\n * {func} dehydrator - Any transformation to apply to the value before adding it\n * to the search. Converting a number to string for example.\n * {string|num} default - Default value. To note that when a state value is\n * default it won't go to the search string.\n * {array|func} validator - Validator function for the value. If is an array\n * should contain valid options. If it is a function\n * should return true or false.\n *\n * @param {object} definition The definition object.\n *\n */\n function useQsState<T>(def: QsStateDefinition<T>): QsStateHookReturn<T | null> {\n const mounted = useRef(false);\n\n // Setup defaults.\n const {\n // Function to convert the value from the string before using it.\n hydrator = identityFn,\n // Function to convert the value to the string before using it.\n dehydrator = identityFn\n } = def;\n\n // Location is mutable.\n // Location search.\n const locSearch = location.search;\n\n // Get the correct validator. Array of items, function or default.\n const validator = useCallback(\n (v: any) => {\n const userValidator = def.validator;\n if (Array.isArray(userValidator)) {\n return userValidator.indexOf(v) !== -1;\n } else if (typeof userValidator === 'function') {\n return userValidator(v);\n } else {\n return !!v;\n }\n },\n [def.validator]\n );\n\n // Parse the value from the url.\n const getValueFromURL = (searchString: string) => {\n const parsedQS = qs.parse(searchString, { ignoreQueryPrefix: true });\n\n // Hydrate the value:\n // Convert from a string to the final type.\n const value = hydrator(parsedQS[def.key] as string);\n\n return validator(value) ? value as T : def.default;\n };\n\n // Store the state relative to this qs key.\n const [valueState, setValueState] = useState(getValueFromURL(locSearch));\n\n // We need a ref to store the state value, otherwise the closure created by\n // the useEffect always shows the original value. We can't pass the value as\n // a dependency of useEffect otherwise it would keep changing itself.\n // Similar issue:\n // https://stackoverflow.com/questions/57847594/react-hooks-accessing-up-to-date-state-from-within-a-callback\n const stateRef = useRef<T | null>();\n stateRef.current = valueState;\n\n const setValue = useCallback(\n (value: T | null) => {\n const v = validator(value) ? value : def.default;\n // Dehydrate the value:\n // Convert to a string usable in the url.\n const dehydratedVal = dehydrator(v);\n\n // Store value in state.\n setValueState(v);\n\n // Because defaults can be objects, we compare on the dehydrated value.\n const dehydratedDefaultVal = dehydrator(def.default);\n storeInURL(\n def.key,\n dehydratedVal !== dehydratedDefaultVal ? dehydratedVal as string : null\n );\n },\n // storeInURL is a necessary dependency despite being outer scope. If the\n // storeInURL value changes then useQsState is going to run again which\n // means that this callback will be recreated.\n [validator, def.default, def.key, dehydrator, storeInURL]\n );\n\n // \"Listen\" to url changes and replace only if different from the currently\n // stored state.\n useEffect(() => {\n // Ensure this check only runs once the component mounted.\n // The initial url setting is done when the state is initialized.\n if (!mounted.current) {\n mounted.current = true;\n return;\n }\n\n const v = getValueFromURL(locSearch);\n\n // The url should only be checked as a source of truth once all the\n // commits have settled.\n const hasPendingCommits = Object.keys(commitQueue).length;\n\n if (!hasPendingCommits && !isEqualObj(v, stateRef.current)) {\n setValueState(v);\n }\n /* eslint-disable-next-line react-hooks/exhaustive-deps */\n }, [locSearch, def]);\n\n return [valueState, setValue];\n }\n\n // This version is just a way to make the use with memo quicker.\n useQsState.memo = function useQsStateMemoized<M>(def: QsStateDefinition<M>, deps: React.DependencyList = []) {\n /* eslint-disable-next-line react-hooks/exhaustive-deps */\n return useQsState<M>(useMemo(() => def, [...deps, storeInURL]));\n };\n\n return useQsState;\n}"],"names":["history"],"mappings":"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"}