├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts └── test.ts └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - uses: actions/setup-node@v1 9 | with: 10 | node-version: 10 11 | - run: npm install 12 | - run: npm test 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.js 3 | index.d.ts 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .github 3 | jest.config.js 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Trevor Blades 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useQueryString 2 | 3 | [![Build Status](https://github.com/trevorblades/use-query-string/workflows/Node%20CI/badge.svg)](https://github.com/trevorblades/use-query-string/actions) 4 | 5 | A React hook that serializes state into the URL query string 6 | 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [Configuration](#configuration) 10 | - [`parseOptions`](#parseoptions) 11 | - [`stringifyOptions`](#stringifyoptions) 12 | - [Examples](#examples) 13 | - [Gatsby example](#gatsby-example) 14 | - [Practical example](#practical-example) 15 | - [An example using context](#an-example-using-context) 16 | - [License](#license) 17 | 18 | ## Installation 19 | 20 | ```bash 21 | $ npm install use-query-string 22 | ``` 23 | 24 | ## Usage 25 | 26 | Given a location object and a history updater function, this hook will return an array who's first element is an object representing the current URL query string. The second element in the array is a function that serializes an object into the query string and updates the former `query` object. 27 | 28 | ```js 29 | import useQueryString from 'use-query-string'; 30 | 31 | const [query, setQuery] = useQueryString(location, updateQuery); 32 | ``` 33 | 34 | The first argument passed to the hook is a [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location) object, and the second is a history-updating function with the following signature: 35 | 36 | ```ts 37 | (path: string): void => { 38 | // update the browser history 39 | } 40 | ``` 41 | 42 | ## Configuration 43 | 44 | ### `parseOptions` 45 | 46 | You can supply an optional third argument to the hook that gets passed along as options to the `parse` function. These allow you to do things like automatically convert values to numbers or booleans, when appropriate. See [the `query-string` docs](https://github.com/sindresorhus/query-string#parsestring-options) for all of the accepted options. 47 | 48 | ```js 49 | const [query, setQuery] = useQueryString( 50 | location, 51 | navigate, 52 | { 53 | parseNumbers: true, 54 | parseBooleans: true 55 | } 56 | ); 57 | ``` 58 | 59 | ### `stringifyOptions` 60 | 61 | You can also pass a fourth argument to the hook that gets used as options for the `stringify` function that serializes your state. This is especially useful if you need to serialize/deserialize arrays a way other than the default. See [the `query-string` docs](https://github.com/sindresorhus/query-string#stringifyobject-options) for all of the accepted options. 62 | 63 | ```js 64 | const arrayFormat = 'comma'; 65 | const [query, setQuery] = useQueryString( 66 | location, 67 | navigate, 68 | {arrayFormat}, 69 | { 70 | skipNull: true, 71 | arrayFormat 72 | } 73 | ); 74 | ``` 75 | 76 | ## Examples 77 | 78 | In this example, you'll see a component using the query string to serialize some state about a selected color. The component uses the global [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location) object, and a function that calls [`History.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to update the page URL. 79 | 80 | ```jsx 81 | import React from 'react'; 82 | import useQueryString from 'use-query-string'; 83 | 84 | function updateQuery(path) { 85 | window.history.pushState(null, document.title, path); 86 | } 87 | 88 | function ColorPicker() { 89 | const [{color}, setQuery] = useQueryString( 90 | window.location, 91 | updateQuery 92 | ); 93 | 94 | function handleColorChange(event) { 95 | setQuery({color: event.target.value}); 96 | } 97 | 98 | return ( 99 |
100 |

Color is {color}

101 | 105 |
106 | ); 107 | } 108 | ``` 109 | 110 | ### Gatsby example 111 | 112 | If you're using Gatsby, you could pass `props.location` and the `navigate` helper function from [Gatsby Link](https://www.gatsbyjs.org/docs/gatsby-link/) as arguments to the hook. 113 | 114 | ```js 115 | // pages/index.js 116 | import React from 'react'; 117 | import useQueryString from 'use-query-string'; 118 | import {navigate} from 'gatsby'; 119 | 120 | function Home(props) { 121 | const [query, setQuery] = useQueryString( 122 | props.location, // pages are given a location object via props 123 | navigate 124 | ); 125 | 126 | // ...the rest of your page 127 | } 128 | ``` 129 | 130 | ### Practical example 131 | 132 | The following CodeSandbox contains an example for working with multiple boolean filters that change something in the page and persist between reloads. 133 | 134 | [![Edit zen-stallman-6r908](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/zen-stallman-6r908?fontsize=14&hidenavigation=1&theme=dark) 135 | 136 | ### An example using context 137 | 138 | When building a complex app, you may have multiple components within a page that need to read from and write to the query string. In these cases, using a `useQueryString` hook in each component will cause your query string to fall out of sync, since each invocation of the hook [manages its own internal state](./src/index.ts#L14). 139 | 140 | To avoid this issue, use **context** to pass `query` and `setQuery` to descendant components within a page. 141 | 142 | ```js 143 | // src/pages/billing.js 144 | import React, {createContext, useContext} from 'react'; 145 | import useQueryString from 'use-query-string'; 146 | import {navigate} from 'gatsby'; 147 | 148 | // create context to use in parent and child components 149 | const QueryStringContext = createContext(); 150 | 151 | export default function Billing(props) { 152 | const [query, setQuery] = useQueryString(props.location, navigate); 153 | return ( 154 | 155 |
156 | 157 | 158 | 159 |
160 | {/* render table of filtered data */} 161 |
162 | ); 163 | } 164 | 165 | function FilterInput(props) { 166 | const {query, setQuery} = useContext(QueryStringContext); 167 | 168 | function handleChange(event) { 169 | const {name, value} = event.target; 170 | setQuery({[name]: value}); 171 | } 172 | 173 | return ( 174 | 179 | ); 180 | } 181 | ``` 182 | 183 | [![Edit Nested components example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/nested-components-example-fyed0?fontsize=14&hidenavigation=1&theme=dark) 184 | 185 | ## License 186 | 187 | [MIT](./LICENSE) 188 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | roots: ['/src'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-string", 3 | "version": "2.4.1", 4 | "description": "A React hook that serializes state into the URL query string", 5 | "author": "Trevor Blades ", 6 | "repository": "trevorblades/use-query-string", 7 | "license": "MIT", 8 | "keywords": [ 9 | "query", 10 | "string", 11 | "querystring", 12 | "serialize", 13 | "state", 14 | "react", 15 | "hook" 16 | ], 17 | "main": "index.js", 18 | "eslintConfig": { 19 | "extends": "@trevorblades/eslint-config/typescript" 20 | }, 21 | "scripts": { 22 | "pretest": "eslint src", 23 | "test": "jest", 24 | "prepare": "tsc" 25 | }, 26 | "peerDependencies": { 27 | "react": ">=16.8.0" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/react-hooks": "^7.0.2", 31 | "@trevorblades/eslint-config": "^7.2.8", 32 | "@types/jest": "^27.4.0", 33 | "eslint": "^7.32.0", 34 | "history": "^4.10.1", 35 | "jest": "^27.4.7", 36 | "react": "^17.0.2", 37 | "react-test-renderer": "^17.0.2", 38 | "ts-jest": "^27.1.2", 39 | "typescript": "^4.5.4" 40 | }, 41 | "dependencies": { 42 | "query-string": "^7.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Dispatch, SetStateAction, useEffect, useRef, useState} from 'react'; 2 | import { 3 | ParseOptions, 4 | ParsedQuery, 5 | StringifyOptions, 6 | parse, 7 | stringify 8 | } from 'query-string'; 9 | 10 | export interface QueryStringResult { 11 | [0]: ParsedQuery; 12 | [1]: Dispatch>>; 13 | } 14 | 15 | export default function useQueryString( 16 | location: Location, 17 | navigate: (path: string) => void, 18 | parseOptions?: ParseOptions, 19 | stringifyOptions?: StringifyOptions 20 | ): QueryStringResult { 21 | const isFirst = useRef(true); 22 | const [state, setState] = useState(parse(location.search, parseOptions)); 23 | 24 | useEffect((): void => { 25 | if (isFirst.current) { 26 | isFirst.current = false; 27 | } else { 28 | navigate(location.pathname + '?' + stringify(state, stringifyOptions)); 29 | } 30 | }, [state]); 31 | 32 | const setQuery: typeof setState = (values): void => { 33 | const nextState = typeof values === 'function' ? values(state) : values; 34 | setState( 35 | (state): ParsedQuery => ({ 36 | ...state, 37 | ...nextState 38 | }) 39 | ); 40 | }; 41 | 42 | return [state, setQuery]; 43 | } 44 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import useQueryString, {QueryStringResult} from '.'; 3 | import {act, renderHook} from '@testing-library/react-hooks'; 4 | import {createBrowserHistory} from 'history'; 5 | 6 | const history = createBrowserHistory(); 7 | 8 | test('should update the query string', (): void => { 9 | const {result} = renderHook( 10 | (): QueryStringResult => useQueryString(history.location, history.push) 11 | ); 12 | 13 | act((): void => { 14 | result.current[1]({foo: 'bar'}); 15 | }); 16 | 17 | expect(history.location.search).toBe('?foo=bar'); 18 | expect(result.current[0].foo).toBe('bar'); 19 | }); 20 | 21 | test('does not clobber existing params', (): void => { 22 | const {result} = renderHook( 23 | (): QueryStringResult => useQueryString(history.location, history.push) 24 | ); 25 | 26 | act((): void => { 27 | result.current[1]({baz: 123}); 28 | }); 29 | 30 | expect(history.location.search).toBe('?baz=123&foo=bar'); 31 | expect(result.current[0].baz).toBe(123); 32 | }); 33 | 34 | test('works with an updater function', (): void => { 35 | const {result} = renderHook( 36 | (): QueryStringResult => 37 | useQueryString(history.location, history.push, {parseNumbers: true}) 38 | ); 39 | 40 | act((): void => { 41 | result.current[1](prev => ({baz: prev.baz + 1})); 42 | }); 43 | 44 | expect(history.location.search).toBe('?baz=124&foo=bar'); 45 | expect(result.current[0].baz).toBe(124); 46 | }); 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "es2015" 6 | ], 7 | "declaration": true, 8 | "outDir": "./" 9 | }, 10 | "files": [ 11 | "src/index.ts" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------