Skip to content

Instantly share code, notes, and snippets.

@pbatey
Last active April 7, 2022 17:43
Show Gist options
  • Save pbatey/98426ec98cd39e348505ee5bd120bd6c to your computer and use it in GitHub Desktop.
Save pbatey/98426ec98cd39e348505ee5bd120bd6c to your computer and use it in GitHub Desktop.
A react hook to sync useState with useSearchParams

A react hook to sync useState with useSearchParams

Usage

useSearchState is used similarly to useState in that it returns a value and setter. The simplest use case is to tie the value to the search params with a key.

MyComponent.tsx

import useSearchState from './useSearchState'

const MyComponent = () => {
  const [value, setValue] = useSearchState('value')
  return (<>
    <button onClick={()=>setValue('red')} className={value==='red'?'active':''}>Red</button>
    <button onClick={()=>setValue('blue')} className={value==='blue'?'active':''}>Blue</button>
  </>)
}
export default MyComponent

Using numbers or booleans

The default type is string|undefined, but by providing an unset argument, the type can be number or boolean.

const [num, setNum] = useSearchState('num', 0)
const [flag, setFlag] = useSearchState('flag', false)

If setValue is called with the unset value, the key is removed from the search params.

API

useSearchState(name, unset, options)

parameter type description
name string Key in search params
unset any Value to use if the name is unset in the search params. default type is string|undefined
options.format function For encoding the value into the search params. Default is x.toString()
options.parse function For decoding the value from the search params. Default handles number|boolean|string

Returns [value, setValue] like useState()

import { useCallback, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
const parseBoolean = (x, unset) => {
// if x is null return unset
if (x === null) return unset
// when unset (the default) is true, look for false
if (unset) return 'false' !== x.toLowerCase()
// otherwise, unset (the default) is false, so look for true
return 'true' === x.toLocaleLowerCase()
}
const parseNumber = (x, unset) => {
if (x === null) return unset
const n = new Number(x).valueOf()
if (isNaN(n)) return unset
return n
}
/**
* Keeps a key's state synchronized with the search params
* @param name key in search params
* @param unset value to use if the name is unset in the search params
* @param options.format function for encoding the value into the search params (default is x.toString())
* @param options.parse function for decoding the value from the search params (default handles number|boolean|string)
* @returns [value, setValue] like useState()
*/
const useSearchState = (name, unset, {format, parse}) => {
if (!parse) {
switch (typeof unset) {
case 'number':
parse = (x) => (parseNumber(x, unset))
break
case 'boolean':
parse = (x) => (parseBoolean(x, unset))
break
case 'undefined':
default:
parse = (x) => (x === null ? unset : x)
break
}
}
const [searchParams, setSearchParams] = useSearchParams()
const v = searchParams.get(name)
const [value, setValue] = useState(parse(v))
useEffect(()=>{
if (!parse) return
const newValue = parse(v)
if (value !== newValue) setValue(newValue)
}, [v])
const setValueWrapper = useCallback((v?:U) => {
const searchParams = (new URL(window.location.href.replace('/#/','/'))).searchParams
if (v === unset || v === undefined) searchParams.delete(name)
else {
const s = format ? format(v) : (v as any).toString()
searchParams.set(name, s)
}
setSearchParams(searchParams, {replace:true})
setValue(v as unknown as U)
},[]) return [value, setValueWrapper]
}
export default useSearchState
import { useCallback, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
const parseBoolean = (x:string|null, unset:boolean) => {
// if x is null return unset
if (x === null) return unset
// when unset (the default) is true, look for false
if (unset) return 'false' !== x.toLowerCase()
// otherwise, unset (the default) is false, so look for true
return 'true' === x.toLocaleLowerCase()
}
const parseNumber = (x:string|null, unset:number) => {
if (x === null) return unset
const n = new Number(x).valueOf()
if (isNaN(n)) return unset
return n
}
/**
* Keeps a key's state synchronized with the search params
* @param name key in search params
* @param unset value to use if the name is unset in the search params
* @param options.format function for encoding the value into the search params (default is x.toString())
* @param options.parse function for decoding the value from the search params (default handles number|boolean|string)
* @returns [value, setValue] like useState()
*/
const useSearchState = <U=string|undefined>(name:string, unset?:U, {format, parse}:{format?:(x:U)=>string, parse?:(s:string|null)=>U}={}):[U,(x:U)=>void] => {
if (!parse) {
switch (typeof unset) {
case 'number':
parse = (x:string|null) => (parseNumber(x, unset)) as unknown as U
break
case 'boolean':
parse = (x:string|null) => (parseBoolean(x, unset)) as unknown as U
break
case 'undefined':
default:
parse = (x:string|null) => (x === null ? unset : x) as unknown as U
break
}
}
const [searchParams, setSearchParams] = useSearchParams()
const v = searchParams.get(name)
const [value, setValue] = useState<U>(parse(v))
useEffect(()=>{
if (!parse) return
const newValue = parse(v)
if (value !== newValue) setValue(newValue)
}, [v])
const setValueWrapper = useCallback((v) => {
const searchParams = (new URL(window.location.href.replace('/#/','/'))).searchParams
if (v === unset || v === undefined) searchParams.delete(name)
else {
const s = format ? format(v) : v.toString()
searchParams.set(name, s)
}
setSearchParams(searchParams, {replace:true})
setValue(v as unknown as U)
},[])
return [value, setValueWrapper]
}
export default useSearchState
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment