Skip to content

Commit

Permalink
feat: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed May 11, 2020
1 parent 966674a commit af80b92
Show file tree
Hide file tree
Showing 10 changed files with 4,152 additions and 136 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ jobs:
name: Install dependencies
- run: yarn ci
name: Run integration tests
- uses: coverallsapp/github-action@832e70b
name: Report code coverage
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# - uses: coverallsapp/github-action@832e70b
# name: Report code coverage
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: 8398a7/action-slack@78391c2
name: Notify on Slack
if: always() # Pick up events even if the job fails or is canceled.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
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
Expand Down
93 changes: 85 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,90 @@
# @chiffre/template-library
# useQueryState for Next.js

[![NPM](https://img.shields.io/npm/v/@chiffre/template-library?color=red)](https://www.npmjs.com/package/@chiffre/template-library)
[![MIT License](https://img.shields.io/github/license/chiffre-io/template-library.svg?color=blue)](https://github.com/chiffre-io/template-library/blob/next/LICENSE)
[![Continuous Integration](https://github.com/chiffre-io/template-library/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/template-library/actions)
[![Coverage Status](https://coveralls.io/repos/github/chiffre-io/template-library/badge.svg?branch=next)](https://coveralls.io/github/chiffre-io/template-library?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/template-library)](https://dependabot.com)
[![NPM](https://img.shields.io/npm/v/next-usequerystate?color=red)](https://www.npmjs.com/package/next-usequerystate)
[![MIT License](https://img.shields.io/github/license/47ng/next-usequerystate.svg?color=blue)](https://github.com/47ng/next-usequerystate/blob/next/LICENSE)
[![Continuous Integration](https://github.com/47ng/next-usequerystate/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/47ng/next-usequerystate/actions)
[![Coverage Status](https://coveralls.io/repos/github/47ng/next-usequerystate/badge.svg?branch=next)](https://coveralls.io/github/47ng/next-usequerystate?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=47ng/next-usequerystate)](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).
2 changes: 2 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
42 changes: 30 additions & 12 deletions package.json
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"
Expand All @@ -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",
Expand Down
78 changes: 77 additions & 1 deletion src/index.ts
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]
}
16 changes: 12 additions & 4 deletions tsconfig.json → tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "es2018",
"lib": ["DOM"],
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"jsx": "preserve",

// Strict Type-Checking Options
"strict": true,
Expand All @@ -28,6 +28,14 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts", "./dist", "./node_modules"]
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": [
"./src/**/*.test.ts",
"./src/**/*.test.tsx",
"./dist",
"./node_modules",
"./test",
"./pages",
"./.next"
]
}
8 changes: 8 additions & 0 deletions tsconfig.cjs.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",
"outDir": "./dist/cjs"
}
}
8 changes: 8 additions & 0 deletions tsconfig.esm.json
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"
}
}
Loading

0 comments on commit af80b92

Please sign in to comment.