generated from chiffre-io/template-library
-
-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
4,152 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020 François Best <[email protected]> | ||
Copyright (c) 2020 François Best <[email protected]> | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,90 @@ | ||
# @chiffre/template-library | ||
# useQueryState for Next.js | ||
|
||
[](https://www.npmjs.com/package/@chiffre/template-library) | ||
[](https://github.com/chiffre-io/template-library/blob/next/LICENSE) | ||
[](https://github.com/chiffre-io/template-library/actions) | ||
[](https://coveralls.io/github/chiffre-io/template-library?branch=next) | ||
[](https://dependabot.com) | ||
[](https://www.npmjs.com/package/next-usequerystate) | ||
[](https://github.com/47ng/next-usequerystate/blob/next/LICENSE) | ||
[](https://github.com/47ng/next-usequerystate/actions) | ||
[](https://coveralls.io/github/47ng/next-usequerystate?branch=next) | ||
[](https://dependabot.com) | ||
|
||
Template for Chiffre libraries | ||
useQueryState hook for Next.js - Like React.useState, but stored in the URL query string | ||
|
||
## Features | ||
|
||
- 🧘♀️ Simple: the URL is the source of truth. | ||
- 🕰 Replace history or append to use the Back button to navigate state updates | ||
|
||
## Installation | ||
|
||
```shell | ||
$ yarn add next-usequerystate | ||
or | ||
$ 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') | ||
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> | ||
</> | ||
) | ||
} | ||
``` | ||
|
||
## 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: | ||
|
||
| URL | count value | Notes | | ||
| ------------- | ----------- | ----------------------- | | ||
| `/` | `null` | No `count` key in URL | | ||
| `/?count=` | `''` | Empty string | | ||
| `/?count=foo` | `'foo'` | | ||
| `/?count=2` | `'2'` | Always returns a string | | ||
|
||
## History options | ||
|
||
By default, operations on the HTML5 history is done by replacing the | ||
current history entry with the updated query when state changes. | ||
|
||
You can see this as a sort of `git squash`, where all state-changing | ||
operations are merged into a single history value. | ||
|
||
You can also opt-in to push a new history item for each state change, | ||
per key, which will let you use the Back button to navigate state | ||
updates: | ||
|
||
```ts | ||
// Default: replace current history with new state | ||
useQueryState('foo', { history: 'replace' }) | ||
|
||
// Append state changes to history: | ||
useQueryState('foo', { history: 'push' }) | ||
``` | ||
|
||
Any other value for the `history` option will fallback to the default. | ||
|
||
## Caveats | ||
|
||
Because the Next.js router is not available in an SSR context, this | ||
hook will always return `null` on SSR/SSG. | ||
|
||
## License | ||
|
||
[MIT](https://github.com/chiffre-io/template-library/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com). | ||
[MIT](https://github.com/47ng/next-usequerystate/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/types/global" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,28 @@ | ||
{ | ||
"name": "@chiffre/template-library", | ||
"name": "next-usequerystate", | ||
"version": "0.0.0-semantically-released", | ||
"description": "Template for Chiffre libraries", | ||
"main": "dist/index.js", | ||
"description": "useQueryState hook for Next.js - Like React.useState, but stored in the URL query string", | ||
"type": "commonjs", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/cjs/index.d.ts", | ||
"license": "MIT", | ||
"author": { | ||
"name": "François Best", | ||
"email": "[email protected]", | ||
"url": "https://chiffre.io" | ||
"email": "[email protected]", | ||
"url": "https://francoisbest.com" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/chiffre-io/template-library" | ||
"url": "https://github.com/47ng/next-usequerystate" | ||
}, | ||
"keywords": [ | ||
"chiffre", | ||
"template" | ||
"nextjs", | ||
"router", | ||
"url", | ||
"query-string", | ||
"react-hook", | ||
"useState" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
|
@@ -24,27 +31,38 @@ | |
"test": "jest --coverage", | ||
"test:watch": "jest --watch", | ||
"build:clean": "rm -rf ./dist", | ||
"build:ts": "tsc", | ||
"build": "run-s build:clean build:ts", | ||
"ci": "run-s build test" | ||
"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" | ||
}, | ||
"peerDependencies": { | ||
"next": "*", | ||
"react-dom": "*", | ||
"react": "*" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@types/jest": "^25.2.1", | ||
"@types/node": "^13.13.5", | ||
"@types/react": "^16.9.35", | ||
"@types/react-dom": "^16.9.8", | ||
"commitlint": "^8.3.5", | ||
"husky": "^4.2.5", | ||
"jest": "^25.5.4", | ||
"next": "^9.3.6", | ||
"npm-run-all": "^4.1.5", | ||
"react": "^16.13.1", | ||
"react-dom": "^16.13.1", | ||
"ts-jest": "^25.5.1", | ||
"ts-node": "^8.10.1", | ||
"typescript": "^3.8.3" | ||
}, | ||
"jest": { | ||
"verbose": true, | ||
"preset": "ts-jest/presets/js-with-ts", | ||
"testEnvironment": "node" | ||
"testEnvironment": "jsdom" | ||
}, | ||
"prettier": { | ||
"arrowParens": "avoid", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,77 @@ | ||
export default (name: string) => `Hello, ${name} !` | ||
import React from 'react' | ||
import { useRouter } from 'next/router' | ||
|
||
export interface UseQueryStateOptions { | ||
/** | ||
* The operation to use on state updates. Defaults to `replace`. | ||
*/ | ||
history: 'replace' | 'push' | ||
} | ||
|
||
export type UseQueryStateReturn<T> = [ | ||
T, | ||
React.Dispatch<React.SetStateAction<T>> | ||
] | ||
|
||
/** | ||
* React state hook synchronized with a URL query string in Next.js | ||
* | ||
* @param key - The URL query string key to bind to | ||
*/ | ||
export function useQueryState( | ||
key: string, | ||
{ history = 'replace' }: Partial<UseQueryStateOptions> = {} | ||
): UseQueryStateReturn<string | null> { | ||
const router = useRouter() | ||
|
||
// Memoizing the update function has the advantage of making it | ||
// immutable as long as `history` stays the same. | ||
// It reduces the amount of reactivity needed to update the state. | ||
const updateUrl = React.useMemo( | ||
() => (history === 'push' ? router.push : router.replace), | ||
[history] | ||
) | ||
|
||
const getValue = React.useCallback((): string | null => { | ||
if (typeof window === 'undefined') { | ||
// Not available in an SSR context | ||
return null | ||
} | ||
const query = new URLSearchParams(window.location.search) | ||
return query.get(key) | ||
}, []) | ||
|
||
// Update the state value only when the relevant key changes. | ||
// Because we're not calling getValue in the function argument | ||
// of React.useMemo, but instead using it as the function to call, | ||
// there is no need to pass it in the dependency array. | ||
const value = React.useMemo(getValue, [router.query[key]]) | ||
|
||
const update = React.useCallback( | ||
(stateUpdater: React.SetStateAction<string | null>) => { | ||
// Resolve the new value based on old value & updater | ||
const oldValue = getValue() | ||
const newValue = | ||
typeof stateUpdater === 'function' | ||
? 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) | ||
} else { | ||
// Don't leave value-less keys hanging | ||
query.delete(key) | ||
} | ||
updateUrl?.call(router, { | ||
pathname: router.pathname, | ||
hash: window.location.hash, | ||
search: query.toString() | ||
}) | ||
}, | ||
[key, updateUrl] | ||
) | ||
return [value, update] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
"outDir": "./dist/cjs" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "./tsconfig.base.json", | ||
"compilerOptions": { | ||
"target": "ESNext", | ||
"module": "ESNext", | ||
"outDir": "./dist/esm" | ||
} | ||
} |
Oops, something went wrong.