obs-portal/frontend/src/query.ts
2021-02-17 23:12:44 +01:00

83 lines
2.6 KiB
TypeScript

import {useMemo} from 'react'
import {useHistory, useLocation} from 'react-router-dom'
type QueryValue = string | number
type QueryParams = {[key: string]: QueryValue}
export function parseValue(value: string): null | QueryValue {
// empty or `-` values should be represented as `null`
if (value === '-' || value === '') {
return null
}
// `isNaN` understands numeric strings as numbers, but also detects empty
// strings and `null` as such. We only want to parse strings that are
// numeric and not empty, therefore this check is a bit more complicated.
if (typeof value === 'string' && value !== '' && !isNaN(Number(value))) {
return parseFloat(value)
}
return value
}
export function parseQuery(search: string): QueryParams {
const result: QueryParams = {}
const params = new URLSearchParams(search)
for (const entry of params.entries()) {
const [key, value_] = entry
const v = parseValue(value_)
if (v != null) {
result[key] = v
}
}
return result
}
export const stringifyParams = (params: Record<string, any>) => {
if (!params) {
return ''
}
const usp = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
usp.append(key, typeof value === 'object' ? JSON.stringify(value) : value)
}
return usp.toString()
}
export function useQueryParam<T extends QueryValue>(
name: string,
defaultValue: T | null = null,
convert: (t: T | null) => T | null = (x) => x
): [T, (newValue: T) => void] {
const history = useHistory()
useLocation() // to trigger a reload when the url changes
const {[name]: value = defaultValue} = (parseQuery(history.location.search) as unknown) as {
[name: string]: T
}
const setter = useMemo(
() => (newValue: T) => {
// We're re-parsing the query here, because it might have been
// changed simulatenously with this call, and the
// history.location.search will already be updated, but react might
// not have rerendered yet. Yes, this is access to some global
// state, but that is okay, since there is just one browser history
// at any time.
const {[name]: _oldValue, ...queryParams} = parseQuery(history.location.search)
const newQueryParams = {
...queryParams,
...(newValue == null || (newValue as any) === defaultValue ? {} : {[name]: newValue}),
}
const queryString = stringifyParams(newQueryParams)
history.replace({...history.location, search: '?' + queryString})
},
[name, history, defaultValue]
)
const result: T = (convert(value) ?? defaultValue) as any
return [result, setter]
}