Skip to content

Commit

Permalink
feat: Add parsing option for non-string state
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed May 16, 2020
1 parent 547f6a7 commit b4e714b
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 35 deletions.
59 changes: 44 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,69 @@ $ npm install next-usequerystate

## Usage

Example: simple counter stored in the URL:

```tsx
import { useQueryState } from 'next-usequerystate'

export default () => {
const [count, setCount] = useQueryState('count')
const [name, setName] = useQueryState('name')
return (
<>
<pre>count: {count}</pre>
<button onClick={() => setCount('0')}>Reset</button>
<button onClick={() => setCount(c => (parseInt(c) || 0) + 1)}>+</button>
<button onClick={() => setCount(c => (parseInt(c) || 0) - 1)}>-</button>
<button onClick={() => setCount(null)}>Clear</button>
<h1>Hello, {name || 'anonymous visitor'}!</h1>
<input value={name || ''} onChange={e => setName(e.target.value)} />
<button onClick={() => setName(null)}>Clear</button>
</>
)
}
```

![](./useQueryState.gif)

## Documentation

`useQueryState` takes one required argument: the key to use in the query string.

It returns the value present in the query string as a string, or `null` if none
was found.

Example outputs for our counter example:
Example outputs for our hello world example:

| URL | name value | Notes |
| ------------ | ---------- | ----------------------------------------------------------------- |
| `/` | `null` | No `name` key in URL |
| `/?name=` | `''` | Empty string |
| `/?name=foo` | `'foo'` |
| `/?name=2` | `'2'` | Always returns a string by default, see [Parsing](#parsing) below |

| URL | count value | Notes |
| ------------- | ----------- | ----------------------- |
| `/` | `null` | No `count` key in URL |
| `/?count=` | `''` | Empty string |
| `/?count=foo` | `'foo'` |
| `/?count=2` | `'2'` | Always returns a string |
## Parsing

If the type you're expecting as state is not a string, you must pass a parsing
function in the second argument object.

You may pass a `serialize` function
for the opposite direction, by default `toString()` is used.

Example: simple counter stored in the URL:

```tsx
import { useQueryState } from 'next-usequerystate'

export default () => {
const [count, setCount] = useQueryState('count', {
// TypeScript will automatically infer it's a number
// based on what `parse` returns.
parse: parseInt
})
return (
<>
<pre>count: {count}</pre>
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(c => c || 0 + 1)}>+</button>
<button onClick={() => setCount(c => c || 0 - 1)}>-</button>
<button onClick={() => setCount(null)}>Clear</button>
</>
)
}
```

## History options

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
},
"peerDependencies": {
"next": "*",
"react-dom": "*",
"react": "*"
"react": "*",
"react-dom": "*"
},
"dependencies": {},
"devDependencies": {
Expand Down
5 changes: 0 additions & 5 deletions src/index.test.ts

This file was deleted.

39 changes: 26 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react'
import { useRouter } from 'next/router'

export interface UseQueryStateOptions {
export interface UseQueryStateOptions<T> {
/**
* The operation to use on state updates. Defaults to `replace`.
*/
history: 'replace' | 'push'

parse: (value: string) => T | null
serialize: (value: T) => string
}

export type UseQueryStateReturn<T> = [
T,
T | null,
React.Dispatch<React.SetStateAction<T>>
]

Expand All @@ -18,10 +21,14 @@ export type UseQueryStateReturn<T> = [
*
* @param key - The URL query string key to bind to
*/
export function useQueryState(
export function useQueryState<T = string>(
key: string,
{ history = 'replace' }: Partial<UseQueryStateOptions> = {}
): UseQueryStateReturn<string | null> {
{
history = 'replace',
parse = x => (x as unknown) as T,
serialize = x => `${x}`
}: Partial<UseQueryStateOptions<T>> = {}
): UseQueryStateReturn<T | null> {
const router = useRouter()

// Memoizing the update function has the advantage of making it
Expand All @@ -32,13 +39,14 @@ export function useQueryState(
[history]
)

const getValue = React.useCallback((): string | null => {
const getValue = React.useCallback((): T | null => {
if (typeof window === 'undefined') {
// Not available in an SSR context
return null
}
const query = new URLSearchParams(window.location.search)
return query.get(key)
const value = query.get(key)
return value ? parse(value) : null
}, [])

// Update the state value only when the relevant key changes.
Expand All @@ -48,19 +56,24 @@ export function useQueryState(
const value = React.useMemo(getValue, [router.query[key]])

const update = React.useCallback(
(stateUpdater: React.SetStateAction<string | null>) => {
(stateUpdater: React.SetStateAction<T | null>) => {
const isUpdaterFunction = (
input: any
): input is (prevState: T | null) => T | null => {
return typeof input === 'function'
}

// Resolve the new value based on old value & updater
const oldValue = getValue()
const newValue =
typeof stateUpdater === 'function'
? stateUpdater(oldValue)
: stateUpdater
const newValue = isUpdaterFunction(stateUpdater)
? stateUpdater(oldValue)
: stateUpdater
// We can't rely on router.query here to avoid causing
// unnecessary renders when other query parameters change.
// URLSearchParams is already polyfilled by Next.js
const query = new URLSearchParams(window.location.search)
if (newValue) {
query.set(key, newValue)
query.set(key, serialize(newValue))
} else {
// Don't leave value-less keys hanging
query.delete(key)
Expand Down
8 changes: 8 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2019", // Transpile optional chaining operator
"module": "CommonJS",
"noEmit": true
}
}
Binary file added useQueryState.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b4e714b

Please sign in to comment.