Skip to content

Commit

Permalink
feat: Add, test & document useQueryStates
Browse files Browse the repository at this point in the history
And refactor useQueryState to cover API edge cases.
  • Loading branch information
franky47 committed Jan 30, 2022
1 parent 83f1146 commit 5cb4441
Show file tree
Hide file tree
Showing 15 changed files with 855 additions and 122 deletions.
15 changes: 9 additions & 6 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
**/*.test.ts
tsconfig.*
.dependabot/
.env
.github/
.husky/
.volumes/
yarn-error.log
.github/**
src/**
**/*.test.ts
coverage/
.dependabot/
next-env.d.ts
src/
tsconfig.*
useQueryState.gif
yarn-error.log
93 changes: 75 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ useQueryState hook for Next.js - Like React.useState, but stored in the URL quer
- 🧘‍♀️ Simple: the URL is the source of truth.
- 🕰 Replace history or append to use the Back button to navigate state updates
- ⚡️ Built-in converters for common object types (number, float, boolean, Date)
- ♊️ Linked querystrings with `useQueryStates`

## Installation

Expand Down Expand Up @@ -62,46 +63,79 @@ Example outputs for our hello world example:
If your state type 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.
We provide helpers for common object types:

Example: simple counter stored in the URL:
```ts
import { queryTypes } from 'next-usequerystate'

useQueryState('tag') // defaults to string
useQueryState('count', queryTypes.integer)
useQueryState('brightness', queryTypes.float)
useQueryState('darkMode', queryTypes.boolean)
useQueryState('after', queryTypes.timestamp) // state is a Date
useQueryState('date', queryTypes.isoDateTime) // state is a Date
```

You may pass a custom set of `parse` and `serialize` functions:

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

export default () => {
const [count, setCount] = useQueryState('count', {
const [hex, setHex] = useQueryState('hex', {
// TypeScript will automatically infer it's a number
// based on what `parse` returns.
parse: parseInt
parse: (query: string) => parseInt(query, 16),
serialize: value => value.toString(16)
})
}
```

Example: simple counter stored in the URL:

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

export default () => {
const [count, setCount] = useQueryState('count', queryTypes.integer)
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(c => c ?? 0 + 1)}>+</button>
<button onClick={() => setCount(c => c ?? 0 - 1)}>-</button>
<button onClick={() => setCount(null)}>Clear</button>
</>
)
}
```

You can also use the built-in serializers/parsers for common object types:
## Default value

```ts
import { queryTypes } from 'next-usequerystate'
When the query string is not present in the URL, the default behaviour is to
return `null` as state.

useQueryState('tag') // defaults to string
useQueryState('count', queryTypes.integer)
useQueryState('brightness', queryTypes.float)
useQueryState('darkMode', queryTypes.boolean)
useQueryState('after', queryTypes.timestamp)
useQueryState('date', queryTypes.isoDateTime)
As you saw in the previous example, it makes state updating and UI rendering
tedious.

You can specify a default value to be returned in this case:

```ts
const [count, setCount] = useQueryState(
'count',
queryTypes.integer.withDefault(0)
)

const increment = () => setCount(c => c + 1) // c will never be null
const decrement = () => setCount(c => c - 1) // c will never be null
const clearCount = () => setCount(null) // Remove query from the URL
```

## Default value
Note: the default value is internal to React, it will **not** be written to the
URL.

Setting the state to `null` will remove the key in the query string and set the
state to the default value.

## History options

Expand Down Expand Up @@ -142,7 +176,30 @@ const MultipleQueriesDemo = () => {
}
```

_Note: support to synchronously update multiple related queries at the same time will come in a future update. See #277._
For query keys that should always move together, you can use `useQueryStates`
with an object containing each key's type:

```ts
import { useQueryStates, queryTypes } from 'next-usequerystate'

const [coordinates, setCoordinates] = useQueryStates(
{
lat: queryTypes.float.withDefault(45.18),
lng: queryTypes.float.withDefault(5.72)
},
{
history: 'push'
}
)

const { lat, lng } = coordinates

// Set all (or a subset of) the keys in one go:
await setCoordinates({
lat: Math.random() * 180 - 90,
lng: Math.random() * 360 - 180
})
```

## Transition Options

Expand Down
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "useQueryState hook for Next.js - Like React.useState, but stored in the URL query string",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"types": "dist/types/index.d.ts",
"license": "MIT",
"author": {
"name": "François Best",
Expand All @@ -27,13 +27,13 @@
"access": "public"
},
"scripts": {
"test": "jest --coverage",
"test:watch": "jest --watch",
"build:clean": "rm -rf ./dist",
"prebuild": "rm -rf ./dist",
"build": "run-s build:*",
"build:cjs": "tsc --project ./tsconfig.cjs.json",
"build:esm": "tsc --project ./tsconfig.esm.json",
"build": "run-s build:clean build:cjs build:esm",
"ci": "run-s build",
"build:dts": "tsc --project ./tsconfig.dts.json",
"test": "tsd",
"ci": "run-s build test",
"prepare": "husky install"
},
"peerDependencies": {
Expand All @@ -56,13 +56,17 @@
"npm-run-all": "^4.1.5",
"ts-jest": "^27.1.3",
"ts-node": "^10.4.0",
"tsd": "^0.19.1",
"typescript": "^4.5.5"
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/js-with-ts",
"testEnvironment": "jsdom"
},
"tsd": {
"directory": "src/tests"
},
"prettier": {
"arrowParens": "avoid",
"semi": false,
Expand Down
4 changes: 0 additions & 4 deletions planning.md

This file was deleted.

14 changes: 12 additions & 2 deletions src/defs.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import type { Router } from 'next/router'

// Next.js does not export the TransitionsOption interface,
// but we can get it from where it's used:
export type TransitionOptions = Parameters<Router['push']>[2]

export type HistoryOptions = 'replace' | 'push'

export type Nullable<T> = {
[K in keyof T]: T[K] | null
}

export type Serializers<T> = {
parse: (value: string) => T | null
serialize: (value: T) => string
serialize?: (value: T) => string
}

export type SerializersWithDefaultFactory<T> = Serializers<T> & {
withDefault: (defaultValue: T) => Serializers<T> & {
readonly defaultValue: T
}
}
}

export type QueryTypeMap = Readonly<{
Expand Down
Loading

0 comments on commit 5cb4441

Please sign in to comment.