├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── docs ├── asset-manifest.json ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ ├── css │ ├── main.14171ca8.css │ └── main.14171ca8.css.map │ └── js │ ├── main.ed681c62.js │ ├── main.ed681c62.js.LICENSE.txt │ └── main.ed681c62.js.map ├── examples ├── react-router-5 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── Issue46.js │ │ ├── ReadmeExample.tsx │ │ ├── ReadmeExample2.tsx │ │ ├── ReadmeExample3.tsx │ │ ├── ReadmeExample3Mapped.tsx │ │ ├── ReadmeExample4.tsx │ │ ├── RenderPropsExample.tsx │ │ ├── UseQueryParamExample.tsx │ │ ├── UseQueryParamsExample.tsx │ │ ├── WithQueryParamsExample.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── react-router-6 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── UseQueryParamExample.tsx │ │ ├── UseQueryParamsExample.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── setupTests.ts │ └── tsconfig.json └── website-example │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.tsx │ ├── UseQueryParamExample.tsx │ ├── UseQueryParamsExample.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── setupTests.ts │ └── tsconfig.json ├── lerna.json ├── package.json ├── packages ├── serialize-query-params │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── decodeQueryParams.test.ts │ │ │ ├── encodeQueryParams.test.ts │ │ │ ├── helpers.ts │ │ │ ├── params.test.ts │ │ │ ├── serialize.test.ts │ │ │ ├── setupTests.ts │ │ │ ├── types.test-d.ts │ │ │ └── updateLocation.test.ts │ │ ├── decodeQueryParams.ts │ │ ├── encodeQueryParams.ts │ │ ├── index.ts │ │ ├── objectToSearchString.ts │ │ ├── params.ts │ │ ├── searchStringToObject.ts │ │ ├── serialize.ts │ │ ├── types.ts │ │ ├── updateLocation.ts │ │ └── withDefault.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts ├── use-query-params-adapter-reach │ ├── LICENSE │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── use-query-params-adapter-react-router-5 │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── declarations.d.ts │ │ │ ├── react-router-5.test.tsx │ │ │ └── setupTests.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts ├── use-query-params-adapter-react-router-6 │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── react-router-6.test.tsx │ │ │ └── setupTests.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts ├── use-query-params-adapter-window │ ├── LICENSE │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── use-query-params │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── scripts │ └── copy-adapters.js │ ├── src │ ├── QueryParamProvider.tsx │ ├── QueryParams.tsx │ ├── __tests__ │ │ ├── QueryParamProvider.test.tsx │ │ ├── QueryParams.test.tsx │ │ ├── helpers.ts │ │ ├── routers │ │ │ ├── README.md │ │ │ ├── mocked.test.tsx │ │ │ └── shared.tsx │ │ ├── setupTests.ts │ │ ├── useQueryParam-SSR.test.tsx │ │ ├── useQueryParam.test.tsx │ │ ├── useQueryParams.test.tsx │ │ └── withQueryParams.test.tsx │ ├── decodedParamCache.ts │ ├── index.ts │ ├── inheritedParams.ts │ ├── latestValues.ts │ ├── memoSearchStringToObject.ts │ ├── options.ts │ ├── removeDefaults.ts │ ├── shallowEqual.ts │ ├── types.ts │ ├── updateSearchString.ts │ ├── urlName.ts │ ├── useQueryParam.ts │ ├── useQueryParams.ts │ └── withQueryParams.tsx │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts ├── tsconfig.json ├── vitest.config.ts └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:react/recommended', 10 | ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly', 14 | }, 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | ecmaVersion: 2018, 21 | sourceType: 'module', 22 | }, 23 | settings: { 24 | react: { 25 | version: '16.8.0', 26 | }, 27 | }, 28 | plugins: ['react', '@typescript-eslint', 'react-hooks'], 29 | rules: { 30 | 'react/prop-types': 'off', 31 | // note you must disable the base rule as it can report incorrect errors 32 | 'no-unused-vars': 'off', 33 | '@typescript-eslint/no-unused-vars': [ 34 | 'error', 35 | { 36 | vars: 'all', 37 | args: 'after-used', 38 | ignoreRestSiblings: false, 39 | }, 40 | ], 41 | 'react-hooks/rules-of-hooks': 'error', 42 | 'react-hooks/exhaustive-deps': 'warn', 43 | 'prefer-const': 'off', 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | 86 | lib/ 87 | esm/ 88 | dist/ 89 | .DS_Store 90 | examples/**/package-lock.json 91 | examples/**/yarn.lock 92 | packages/use-query-params/adapters 93 | .yalc 94 | yalc.lock -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry="https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | cache: npm 5 | 6 | before_script: 7 | # install dependencies for subpackages 8 | - lerna bootstrap --hoist --scope "use-query-params" --scope "serialize-query-params" 9 | # transpile the typescript so we can run tests (uqp depends on sqp) 10 | - npm run build 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": ["/**", "**/node_modules/**"], 11 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 12 | "args": ["run", "--root", "packages/use-query-params-adapter-react-router-6", "${relativeFile}"], 13 | "smartStep": true, 14 | "console": "integratedTerminal", 15 | "runtimeExecutable": "/usr/bin/env node" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

useQueryParams

3 |

A React Hook, HOC, and Render Props solution for managing state in URL query parameters with easy serialization. 4 |

5 |

Works with React Router 5 and 6 out of the box. TypeScript supported.

6 | 7 | 8 |

9 | npm 10 | Travis (.com) 11 | 12 |

13 | 14 |
15 |
16 | 17 | When creating apps with easily shareable URLs, you often want to encode state as query parameters, but all query parameters must be encoded as strings. `useQueryParams` allows you to easily encode and decode data of any type as query parameters with smart memoization to prevent creating unnecessary duplicate objects. It uses [serialize-query-params](/packages/serialize-query-params/). 18 | 19 | ## Docs 20 | 21 | * [use-query-params docs](/packages/use-query-params/#readme) 22 | * [serialize-query-params docs](/packages/serialize-query-params/#readme) 23 | 24 | 25 | ## Packages 26 | 27 | This is a monorepo managed with [Lerna](https://github.com/lerna/lerna). 28 | 29 | | Package | Version | Docs | Description | 30 | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | 31 | | [`use-query-params`](/packages/use-query-params) | [![npm](https://img.shields.io/npm/v/use-query-params.svg?style=flat-square)](https://www.npmjs.com/package/use-query-params) | [![](https://img.shields.io/badge/API%20Docs-readme-orange.svg?style=flat-square)](/packages/use-query-params/#readme) | use-query-params React library | 32 | | [`serialize-query-params`](/packages/serialize-query-params) | [![npm](https://img.shields.io/npm/v/serialize-query-params.svg?style=flat-square)](https://www.npmjs.com/package/serialize-query-params) | [![](https://img.shields.io/badge/API%20Docs-readme-orange.svg?style=flat-square)](/packages/serialize-query-params/#readme) | serialize-query-params js library | 33 | 34 | 35 | 36 | ## Development 37 | 38 | To get running locally: 39 | 40 | ``` 41 | npm install 42 | npx lerna bootstrap --hoist --scope "use-query-params" --scope "serialize-query-params" 43 | npm build 44 | npm test 45 | ``` 46 | 47 | Set up examples: 48 | 49 | ``` 50 | lerna bootstrap --scope "*-example" 51 | lerna link 52 | ``` 53 | 54 | Then run one: 55 | 56 | ``` 57 | lerna run --scope react-router-example start 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/use-query-params/static/css/main.14171ca8.css", 4 | "main.js": "/use-query-params/static/js/main.ed681c62.js", 5 | "index.html": "/use-query-params/index.html", 6 | "main.14171ca8.css.map": "/use-query-params/static/css/main.14171ca8.css.map", 7 | "main.ed681c62.js.map": "/use-query-params/static/js/main.ed681c62.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.14171ca8.css", 11 | "static/js/main.ed681c62.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | use-query-params
-------------------------------------------------------------------------------- /docs/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/docs/logo192.png -------------------------------------------------------------------------------- /docs/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/docs/logo512.png -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/static/css/main.14171ca8.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}a{color:#0f8cff;text-decoration:none}a:hover{text-decoration:underline}p{line-height:1.5;margin-top:2rem}button{background:#eee;border:1px solid #ccc;border-radius:4px;border-top:none;box-shadow:0 2px 0 rgba(0,0,0,.15);font-size:16px;margin:.2rem .5rem;padding:.5em 1em}button:focus{outline:none}button:hover{background-color:#f4f4f4}button:active{background-color:#f8f8f8;border-color:#ddd;box-shadow:0 -2px 0 rgba(0,0,0,.15)}button.active{font-weight:700}table{margin:1rem}td{padding:0 1rem}.text-center{text-align:center}.App{margin:0 auto;max-width:650px}.nav{margin-bottom:3rem;text-align:center}.ext-nav{margin-bottom:1.5em}.example-block{margin-bottom:3rem}.set-btn{font-family:monospace} 2 | /*# sourceMappingURL=main.14171ca8.css.map*/ -------------------------------------------------------------------------------- /docs/static/css/main.14171ca8.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.14171ca8.css","mappings":"AAAA,KACE,sGACF,CAEA,EACE,aAAc,CACd,oBACF,CAEA,QACE,yBACF,CAEA,EACE,eAAgB,CAChB,eACF,CAEA,OAIE,eAAgB,CAGhB,qBAAgB,CAChB,iBAAkB,CADlB,eAAgB,CAFhB,kCAAsC,CAHtC,cAAe,CADf,kBAAqB,CAErB,gBAMF,CAEA,aACE,YACF,CAEA,aACE,wBACF,CAEA,cACE,wBAAyB,CAEzB,iBAAkB,CADlB,mCAEF,CAEA,cACE,eACF,CAEA,MACE,WACF,CAEA,GACE,cACF,CAEA,aAAe,iBAAoB,CAInC,KAEE,aAAc,CADd,eAEF,CAEA,KAEE,kBAAmB,CADnB,iBAEF,CAEA,SACE,mBACF,CAEA,eACE,kBACF,CAEA,SACE,qBACF","sources":["index.css"],"sourcesContent":["body { \n font-family: -apple-system,system-ui,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif;\n}\n\na {\n color: #0f8cff;\n text-decoration: none;\n}\n\na:hover {\n text-decoration: underline;\n}\n\np {\n line-height: 1.5;\n margin-top: 2rem;\n}\n\nbutton {\n margin: 0.2rem 0.5rem;\n font-size: 16px;\n padding: 0.5em 1.0em;\n background: #eee;\n box-shadow: 0 2px 0px rgba(0,0,0,0.15);\n border: 1px solid #ccc;\n border-top: none;\n border-radius: 4px;\n}\n\nbutton:focus {\n outline: none;\n}\n\nbutton:hover {\n background-color: #f4f4f4;\n}\n\nbutton:active {\n background-color: #f8f8f8;\n box-shadow: 0 -2px 0 rgba(0,0,0,0.15);\n border-color: #ddd;\n}\n\nbutton.active {\n font-weight: bold;\n}\n\ntable {\n margin: 1rem;\n}\n\ntd {\n padding: 0 1rem;\n}\n\n.text-center { text-align: center; }\n\n\n\n.App {\n max-width: 650px;\n margin: 0 auto;\n}\n\n.nav {\n text-align: center;\n margin-bottom: 3rem;\n}\n\n.ext-nav {\n margin-bottom: 1.5em;\n}\n\n.example-block {\n margin-bottom: 3rem;\n}\n\n.set-btn {\n font-family: monospace;\n}"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/main.ed681c62.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react-jsx-runtime.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * react.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * scheduler.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | 41 | /** 42 | * React Router v6.3.0 43 | * 44 | * Copyright (c) Remix Software Inc. 45 | * 46 | * This source code is licensed under the MIT license found in the 47 | * LICENSE.md file in the root directory of this source tree. 48 | * 49 | * @license MIT 50 | */ 51 | -------------------------------------------------------------------------------- /examples/react-router-5/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | # remove yarn.lock from examples to stop dependabot 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /examples/react-router-5/README.md: -------------------------------------------------------------------------------- 1 | # Running with local use-query-params 2 | 3 | Due to mismatched versions of react, we can't just link it, instead we use [yalc](https://github.com/wclr/yalc). 4 | 5 | First build use-query-params from the root /: 6 | ``` 7 | yarn build 8 | ``` 9 | 10 | From the /packages/use-query-params directory, publish to yalc. We use no-scripts since it fails to find some hoisted dependencies. Do the same for serialize-query-params. 11 | 12 | ``` 13 | yalc publish --no-scripts 14 | ``` 15 | 16 | Then add to this project 17 | ``` 18 | yalc add use-query-params 19 | yalc add serialize-query-params 20 | ``` 21 | 22 | # Getting Started with Create React App 23 | 24 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 25 | 26 | ## Available Scripts 27 | 28 | In the project directory, you can run: 29 | 30 | ### `npm start` 31 | 32 | Runs the app in the development mode.\ 33 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 34 | 35 | The page will reload if you make edits.\ 36 | You will also see any lint errors in the console. 37 | 38 | ### `npm test` 39 | 40 | Launches the test runner in the interactive watch mode.\ 41 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 42 | 43 | ### `npm run build` 44 | 45 | Builds the app for production to the `build` folder.\ 46 | It correctly bundles React in production mode and optimizes the build for the best performance. 47 | 48 | The build is minified and the filenames include the hashes.\ 49 | Your app is ready to be deployed! 50 | 51 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 52 | 53 | ### `npm run eject` 54 | 55 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 56 | 57 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 58 | 59 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 60 | 61 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 62 | 63 | ## Learn More 64 | 65 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 66 | 67 | To learn React, check out the [React documentation](https://reactjs.org/). 68 | -------------------------------------------------------------------------------- /examples/react-router-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-5", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.11.45", 11 | "@types/react": "^18.0.15", 12 | "@types/react-dom": "^18.0.6", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router": "5", 16 | "react-router-dom": "5", 17 | "react-scripts": "5.0.1", 18 | "serialize-query-params": "file:.yalc/serialize-query-params", 19 | "typescript": "^4.7.4", 20 | "use-query-params": "file:.yalc/use-query-params", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/react-router-5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-5/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-router-5/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/react-router-5/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-5/public/logo192.png -------------------------------------------------------------------------------- /examples/react-router-5/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-5/public/logo512.png -------------------------------------------------------------------------------- /examples/react-router-5/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router-5/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/react-router-5/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import UseQueryParamExample from './UseQueryParamExample'; 4 | import UseQueryParamsExample from './UseQueryParamsExample'; 5 | import WithQueryParamsExample from './WithQueryParamsExample'; 6 | import RenderPropsExample from './RenderPropsExample'; 7 | import ReadmeExample from './ReadmeExample'; 8 | import ReadmeExample2 from './ReadmeExample2'; 9 | import ReadmeExample3 from './ReadmeExample3'; 10 | import ReadmeExample4 from './ReadmeExample4'; 11 | 12 | import ReadmeExample3Mapped from './ReadmeExample3Mapped'; 13 | import Issue46 from './Issue46'; 14 | 15 | const App = (props: any) => { 16 | const [example, setExample] = React.useState(0); 17 | 18 | return ( 19 |
20 | 39 |
40 | {example === 0 && } 41 | {example === 1 && } 42 | {example === 2 && } 43 | {example === 3 && } 44 | {example === 4 && } 45 | {example === 5 && } 46 | {example === 6 && } 47 | {example === 7 && } 48 | {example === 8 && } 49 | {example === 46 && } 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /examples/react-router-5/src/Issue46.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NumberParam, useQueryParam } from 'use-query-params'; 3 | 4 | // https://github.com/pbeshai/use-query-params/pull/46 5 | export default function Issue46() { 6 | const [a = 1, setA] = useQueryParam('a', NumberParam); 7 | const [b, setB] = React.useState(1); 8 | 9 | React.useEffect(() => { 10 | console.log('effect'); 11 | if (b % 2 === 0) { 12 | setA(b); 13 | } 14 | }, [b, setA]); 15 | 16 | return ( 17 |
18 |

a: {a}

19 |

b: {b}

20 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router-5/src/ReadmeExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useQueryParam, NumberParam, StringParam } from 'use-query-params'; 3 | 4 | const UseQueryParamExample = () => { 5 | // something like: ?x=123&foo=bar in the URL 6 | const [num, setNum] = useQueryParam('x', NumberParam); 7 | const [foo, setFoo] = useQueryParam('foo', StringParam); 8 | 9 | return ( 10 |
11 |

num is {num}

12 | 13 |

foo is {foo}

14 | 17 |
18 | ); 19 | }; 20 | 21 | export default UseQueryParamExample; 22 | -------------------------------------------------------------------------------- /examples/react-router-5/src/ReadmeExample2.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | useQueryParams, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | } from 'use-query-params'; 9 | 10 | const UseQueryParamsExample = () => { 11 | const [query, setQuery] = useQueryParams({ 12 | x: NumberParam, 13 | q: StringParam, 14 | filters: withDefault(ArrayParam, []), 15 | }); 16 | const { x: num, q: searchQuery, filters } = query; 17 | console.log('got filters =', filters, num, searchQuery); 18 | return ( 19 |
20 |

num is {num}

21 | 22 |

searchQuery is {searchQuery}

23 |

There are {filters.length} filters active.

24 | 34 |
35 | ); 36 | }; 37 | 38 | export default UseQueryParamsExample; 39 | -------------------------------------------------------------------------------- /examples/react-router-5/src/ReadmeExample3.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | withQueryParams, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | } from 'use-query-params'; 9 | 10 | const UseQueryParamsExample = ({ query, setQuery }: any) => { 11 | const { x: num, q: searchQuery, filters = [] } = query; 12 | console.log('got filters =', filters, query); 13 | return ( 14 |
15 |

num is {num}

16 | 17 |

searchQuery is {searchQuery}

18 |

There are {filters.length} filters active.

19 | 29 |
30 | ); 31 | }; 32 | 33 | export default withQueryParams( 34 | { 35 | x: NumberParam, 36 | q: StringParam, 37 | filters: withDefault(ArrayParam, []), 38 | }, 39 | UseQueryParamsExample 40 | ); 41 | -------------------------------------------------------------------------------- /examples/react-router-5/src/ReadmeExample3Mapped.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | withQueryParamsMapped, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | DecodedValueMap, 9 | SetQuery, 10 | } from 'use-query-params'; 11 | 12 | type BaseProps = { 13 | extra: number; 14 | }; 15 | type UrlProps = { 16 | searchQuery: string; 17 | num: number; 18 | filters: string[]; 19 | onChangeNum: (newNum: number) => void; 20 | setQuery: SetQuery; 21 | }; 22 | type Props = BaseProps & UrlProps; 23 | 24 | const UseQueryParamsExample = ({ 25 | searchQuery, 26 | num, 27 | filters, 28 | onChangeNum, 29 | setQuery, 30 | extra, 31 | }: Props) => { 32 | console.log('got filters =', filters, extra); 33 | return ( 34 |
35 |

num is {num}

36 | 37 |

searchQuery is {searchQuery}

38 |

There are {filters.length} filters active.

39 | 49 |
50 | ); 51 | }; 52 | 53 | const queryConfig = { 54 | x: NumberParam, 55 | q: StringParam, 56 | filters: withDefault(ArrayParam, []), 57 | }; 58 | 59 | export default withQueryParamsMapped( 60 | { 61 | x: NumberParam, 62 | q: StringParam, 63 | filters: withDefault(ArrayParam, []), 64 | }, 65 | 66 | function mapToProps( 67 | query: DecodedValueMap, 68 | setQuery: SetQuery, 69 | ownProps: BaseProps 70 | ) { 71 | return { 72 | searchQuery: query.q, 73 | num: query.x, 74 | filters: query.filters, 75 | onChangeNum: (newNum: number) => setQuery({ x: newNum }), 76 | setQuery, 77 | }; 78 | }, 79 | UseQueryParamsExample 80 | ); 81 | -------------------------------------------------------------------------------- /examples/react-router-5/src/ReadmeExample4.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | QueryParams, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | } from 'use-query-params'; 9 | 10 | const RenderPropsExample = () => { 11 | const queryConfig = { 12 | x: NumberParam, 13 | q: StringParam, 14 | filters: withDefault(ArrayParam, []), 15 | }; 16 | return ( 17 |
18 | 19 | {({ query, setQuery }) => { 20 | const { x: num, q: searchQuery, filters = [] } = query; 21 | return ( 22 | <> 23 |

num is {num}

24 | 27 |

searchQuery is {searchQuery}

28 |

There are {filters.length} filters active.

29 | 43 | 44 | ); 45 | }} 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default RenderPropsExample; 52 | -------------------------------------------------------------------------------- /examples/react-router-5/src/RenderPropsExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { QueryParams, StringParam, NumberParam } from 'use-query-params'; 3 | 4 | const RenderPropsExample: React.FC<{}> = () => { 5 | const [count, setCount] = React.useState(0); 6 | 7 | const queryConfig = { 8 | zzz: NumberParam, 9 | test: StringParam, 10 | anyp: StringParam, 11 | }; 12 | 13 | return ( 14 |
15 | 16 | {({ query, setQuery }) => { 17 | const { zzz, test, anyp } = query; 18 | return ( 19 | <> 20 |

<QueryParams> Render Props Example

21 |
22 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 42 | 43 | 44 | 45 | 46 | 47 | 68 | 69 | 70 | 71 | 72 | 73 | 94 | 95 | 96 |
zzz{zzz}{typeof zzz} 34 | 41 |
test{test}{typeof test} 48 | 57 | 67 |
anyp{anyp}{typeof anyp} 74 | 83 | 93 |
97 |
98 | 99 | ); 100 | }} 101 |
102 |
103 | ); 104 | }; 105 | 106 | export default RenderPropsExample; 107 | -------------------------------------------------------------------------------- /examples/react-router-5/src/UseQueryParamExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | useQueryParam, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | } from 'use-query-params'; 9 | 10 | const MyParam = { 11 | encode: (val: number) => `MY_${val}`, 12 | decode: (input: string | (string | null)[] | null | undefined) => { 13 | const str = input instanceof Array ? input[0] : input; 14 | return str == null ? undefined : +str.split('_')[1]; 15 | }, 16 | }; 17 | 18 | const UseQueryParamExample = () => { 19 | const [count, setCount] = React.useState(0); 20 | const [zzz, setZzz] = useQueryParam('zzz', NumberParam); 21 | const [custom, setCustom] = useQueryParam('custom', MyParam); 22 | const [test, setTest] = useQueryParam('test', StringParam); 23 | const [anyp, setAnyP] = useQueryParam('anyp'); 24 | const [arr, setArr] = useQueryParam( 25 | 'arr', 26 | React.useMemo(() => withDefault(ArrayParam, [] as (string | null)[]), []) 27 | ); 28 | 29 | // verify we aren't creating new arrays each time 30 | const prevArr = React.useRef(arr); 31 | React.useEffect(() => { 32 | if (prevArr.current !== arr) { 33 | console.log('new array. was:', prevArr.current, 'now:', arr); 34 | } else { 35 | console.log('same array'); 36 | } 37 | prevArr.current = arr; 38 | }); 39 | 40 | return ( 41 |
42 |

useQueryParam Example

43 |
44 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | 75 | 76 | 77 | 78 | 79 | 88 | 89 | 90 | 91 | 92 | 93 | 109 | 110 | 111 | 112 | 119 | 120 | 132 | 133 | 134 |
zzz{zzz}{typeof zzz} 56 | 61 |
custom{custom}{typeof custom} 68 | 73 |
test{test}{typeof test} 80 | 87 |
anyp{anyp as any}{typeof anyp} 94 | 101 | 108 |
arr 113 | {arr.map((d: any, i: number) => ( 114 |
115 | arr[{i}] = {d} 116 |
117 | ))} 118 |
{typeof arr} 121 | 131 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default UseQueryParamExample; 141 | -------------------------------------------------------------------------------- /examples/react-router-5/src/UseQueryParamsExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | useQueryParams, 4 | StringParam, 5 | NumberParam, 6 | JsonParam, 7 | } from 'use-query-params'; 8 | 9 | const UseQueryParamsExample = () => { 10 | const [count, setCount] = React.useState(0); 11 | 12 | const [query, setQuery] = useQueryParams({ 13 | zzz: NumberParam, 14 | test: StringParam, 15 | anyp: StringParam, 16 | json: JsonParam, 17 | }); 18 | const { zzz, test, anyp, json } = query; 19 | 20 | return ( 21 |
22 |

useQueryParams Example

23 |
24 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | 68 | 69 | 70 | 71 | 72 | 73 | 92 | 93 | 94 | 95 | 96 | 97 | 128 | 129 | 130 |
zzz{zzz}{typeof zzz} 36 | 43 |
test{test}{typeof test} 50 | 57 | 67 |
anyp{anyp}{typeof anyp} 74 | 81 | 91 |
json{JSON.stringify(json)}{typeof json} 98 | 111 | 127 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default UseQueryParamsExample; 137 | -------------------------------------------------------------------------------- /examples/react-router-5/src/WithQueryParamsExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | withQueryParams, 4 | StringParam, 5 | NumberParam, 6 | DecodedValueMap, 7 | SetQuery, 8 | } from 'use-query-params'; 9 | 10 | const queryConfig = { 11 | zzz: NumberParam, 12 | test: StringParam, 13 | anyp: StringParam, 14 | }; 15 | 16 | interface Props { 17 | query: DecodedValueMap; 18 | setQuery: SetQuery; 19 | } 20 | 21 | const WithQueryParamsExample: React.FC = ({ query, setQuery }) => { 22 | const [count, setCount] = React.useState(0); 23 | 24 | const { zzz, test, anyp } = query; 25 | 26 | return ( 27 |
28 |

withQueryParams Example

29 |
30 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 50 | 51 | 52 | 53 | 54 | 55 | 74 | 75 | 76 | 77 | 78 | 79 | 98 | 99 | 100 |
zzz{zzz}{typeof zzz} 42 | 49 |
test{test}{typeof test} 56 | 63 | 73 |
anyp{anyp}{typeof anyp} 80 | 87 | 97 |
101 |
102 |
103 | ); 104 | }; 105 | 106 | export default withQueryParams(queryConfig, WithQueryParamsExample); 107 | -------------------------------------------------------------------------------- /examples/react-router-5/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | padding: 24px; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | nav { 17 | font-size: 14px; 18 | display: flex; 19 | flex-wrap: wrap; 20 | gap: 8px 0px; 21 | background: #eee; 22 | padding: 16px; 23 | border-bottom: 1px solid #ddd; 24 | margin: -24px -24px 0; 25 | } 26 | 27 | nav button { 28 | font-size: 14px; 29 | padding: 0.25em 0.5em; 30 | } 31 | nav a { 32 | color: blue; 33 | text-decoration: none; 34 | padding: 2px 4px; 35 | display: inline-block; 36 | } 37 | 38 | a.active { 39 | color: rgb(179, 32, 93); 40 | background-color: rgb(242, 228, 234); 41 | } 42 | 43 | nav a:hover { 44 | text-decoration: underline; 45 | } 46 | 47 | button { 48 | margin: 0 0.5rem; 49 | font-size: 16px; 50 | padding: 0.5em 1.0em; 51 | background: #eee; 52 | } 53 | button:active { 54 | background: #e8e8e8; 55 | } 56 | 57 | table { 58 | margin: 1rem; 59 | } 60 | 61 | td { 62 | padding: 0 1rem; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /examples/react-router-5/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { QueryParamProvider } from 'use-query-params'; 5 | import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; 6 | import { parse, stringify } from 'query-string'; 7 | import './index.css'; 8 | import App from './App'; 9 | 10 | const root = ReactDOM.createRoot( 11 | document.getElementById('root') as HTMLElement 12 | ); 13 | root.render( 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /examples/react-router-5/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router-5/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/react-router-5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-router-6/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | # remove yarn.lock from examples to stop dependabot 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /examples/react-router-6/README.md: -------------------------------------------------------------------------------- 1 | # Running with local use-query-params 2 | 3 | Due to mismatched versions of react, we can't just link it, instead we use [yalc](https://github.com/wclr/yalc). 4 | 5 | First build use-query-params from the root /: 6 | ``` 7 | yarn build 8 | ``` 9 | 10 | From the /packages/use-query-params directory, publish to yalc. We use no-scripts since it fails to find some hoisted dependencies. Do the same for serialize-query-params. 11 | 12 | ``` 13 | yalc publish --no-scripts 14 | ``` 15 | 16 | Then add to this project 17 | ``` 18 | yalc add use-query-params 19 | yalc add serialize-query-params 20 | ``` 21 | 22 | 23 | 24 | # Getting Started with Create React App 25 | 26 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 27 | 28 | ## Available Scripts 29 | 30 | In the project directory, you can run: 31 | 32 | ### `npm start` 33 | 34 | Runs the app in the development mode.\ 35 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 36 | 37 | The page will reload if you make edits.\ 38 | You will also see any lint errors in the console. 39 | 40 | ### `npm test` 41 | 42 | Launches the test runner in the interactive watch mode.\ 43 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 44 | 45 | ### `npm run build` 46 | 47 | Builds the app for production to the `build` folder.\ 48 | It correctly bundles React in production mode and optimizes the build for the best performance. 49 | 50 | The build is minified and the filenames include the hashes.\ 51 | Your app is ready to be deployed! 52 | 53 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 54 | 55 | ### `npm run eject` 56 | 57 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 58 | 59 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 60 | 61 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 62 | 63 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 64 | 65 | ## Learn More 66 | 67 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 68 | 69 | To learn React, check out the [React documentation](https://reactjs.org/). 70 | -------------------------------------------------------------------------------- /examples/react-router-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-6", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.11.45", 11 | "@types/react": "^18.0.15", 12 | "@types/react-dom": "^18.0.6", 13 | "query-string": "^7.1.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router": "^6.3.0", 17 | "react-router-dom": "^6.3.0", 18 | "react-scripts": "5.0.1", 19 | "serialize-query-params": "file:.yalc/serialize-query-params", 20 | "typescript": "^4.7.4", 21 | "use-query-params": "file:.yalc/use-query-params", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "resolutions": { 49 | "react": "18.2.0", 50 | "react-dom": "18.2.0", 51 | "react-router-dom": "6.3.0", 52 | "react-router": "6.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/react-router-6/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-6/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-router-6/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/react-router-6/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-6/public/logo192.png -------------------------------------------------------------------------------- /examples/react-router-6/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/react-router-6/public/logo512.png -------------------------------------------------------------------------------- /examples/react-router-6/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router-6/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/react-router-6/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, Outlet } from 'react-router-dom'; 3 | 4 | function App() { 5 | return ( 6 |
7 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /examples/react-router-6/src/UseQueryParamExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | useQueryParam, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | } from 'use-query-params'; 9 | 10 | const MyParam = { 11 | encode: (val: number) => `MY_${val}`, 12 | decode: (input: string | (string | null)[] | null | undefined) => { 13 | const str = input instanceof Array ? input[0] : input; 14 | return str == null ? undefined : +str.split('_')[1]; 15 | }, 16 | }; 17 | 18 | const UseQueryParamExample = () => { 19 | const [count, setCount] = React.useState(0); 20 | const [zzz, setZzz] = useQueryParam('zzz', NumberParam); 21 | const [custom, setCustom] = useQueryParam('custom', MyParam); 22 | const [test, setTest] = useQueryParam('test', StringParam); 23 | const [anyp, setAnyP] = useQueryParam('anyp'); 24 | const [arr, setArr] = useQueryParam( 25 | 'arr', 26 | React.useMemo(() => withDefault(ArrayParam, [] as (string | null)[]), []) 27 | ); 28 | 29 | // verify we aren't creating new arrays each time 30 | const prevArr = React.useRef(arr); 31 | React.useEffect(() => { 32 | if (prevArr.current !== arr) { 33 | console.log('new array. was:', prevArr.current, 'now:', arr); 34 | } else { 35 | console.log('same array'); 36 | } 37 | prevArr.current = arr; 38 | }); 39 | 40 | return ( 41 |
42 |

useQueryParam Example

43 |
44 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | 75 | 76 | 77 | 78 | 79 | 88 | 89 | 90 | 91 | 92 | 93 | 109 | 110 | 111 | 112 | 119 | 120 | 132 | 133 | 134 |
zzz{zzz}{typeof zzz} 56 | 61 |
custom{custom}{typeof custom} 68 | 73 |
test{test}{typeof test} 80 | 87 |
anyp{anyp as any}{typeof anyp} 94 | 101 | 108 |
arr 113 | {arr.map((d: any, i: number) => ( 114 |
115 | arr[{i}] = {d} 116 |
117 | ))} 118 |
{typeof arr} 121 | 131 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default UseQueryParamExample; 141 | -------------------------------------------------------------------------------- /examples/react-router-6/src/UseQueryParamsExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | useQueryParams, 4 | StringParam, 5 | NumberParam, 6 | JsonParam, 7 | } from 'use-query-params'; 8 | 9 | const UseQueryParamsExample = () => { 10 | const [count, setCount] = React.useState(0); 11 | 12 | const [query, setQuery] = useQueryParams({ 13 | zzz: NumberParam, 14 | test: StringParam, 15 | anyp: StringParam, 16 | json: JsonParam, 17 | }); 18 | const { zzz, test, anyp, json } = query; 19 | 20 | return ( 21 |
22 |

useQueryParams Example

23 |
24 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | 68 | 69 | 70 | 71 | 72 | 73 | 92 | 93 | 94 | 95 | 96 | 97 | 128 | 129 | 130 |
zzz{zzz}{typeof zzz} 36 | 43 |
test{test}{typeof test} 50 | 57 | 67 |
anyp{anyp}{typeof anyp} 74 | 81 | 91 |
json{JSON.stringify(json)}{typeof json} 98 | 111 | 127 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default UseQueryParamsExample; 137 | -------------------------------------------------------------------------------- /examples/react-router-6/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | padding: 24px; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | nav { 17 | font-size: 14px; 18 | display: flex; 19 | flex-wrap: wrap; 20 | gap: 12px; 21 | background: #eee; 22 | padding: 0 16px; 23 | border-bottom: 1px solid #ddd; 24 | margin: -24px -24px 0; 25 | } 26 | 27 | nav a { 28 | color: black; 29 | font-weight: 600; 30 | text-decoration: none; 31 | padding: 12px 12px; 32 | display: inline-block; 33 | } 34 | 35 | a.active { 36 | color: rgb(179, 32, 93); 37 | background-color: white; 38 | } 39 | 40 | nav a:hover { 41 | text-decoration: underline; 42 | } 43 | 44 | button { 45 | margin: 0 0.5rem; 46 | font-size: 16px; 47 | padding: 0.5em 1.0em; 48 | background: #eee; 49 | } 50 | 51 | table { 52 | margin: 1rem; 53 | } 54 | 55 | td { 56 | padding: 0 1rem; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/react-router-6/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { QueryParamProvider } from 'use-query-params'; 6 | import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; 7 | import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; 8 | import UseQueryParamExample from './UseQueryParamExample'; 9 | import UseQueryParamsExample from './UseQueryParamsExample'; 10 | // optionally use the query-string parse / stringify functions to 11 | // handle more advanced cases than URLSearchParams supports. 12 | import { parse, stringify } from 'query-string'; 13 | 14 | const root = ReactDOM.createRoot( 15 | document.getElementById('root') as HTMLElement 16 | ); 17 | root.render( 18 | 19 | 20 | 27 | 28 | }> 29 | } /> 30 | } /> 31 | } /> 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /examples/react-router-6/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router-6/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/react-router-6/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/website-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | # remove yarn.lock from examples to stop dependabot 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /examples/website-example/README.md: -------------------------------------------------------------------------------- 1 | # Running with local use-query-params 2 | 3 | Due to mismatched versions of react, we can't just link it, instead we use [yalc](https://github.com/wclr/yalc). 4 | 5 | First build use-query-params from the root /: 6 | ``` 7 | yarn build 8 | ``` 9 | 10 | From the /packages/use-query-params directory, publish to yalc. We use no-scripts since it fails to find some hoisted dependencies. Do the same for serialize-query-params. 11 | 12 | ``` 13 | yalc publish --no-scripts 14 | ``` 15 | 16 | Then add to this project 17 | ``` 18 | yalc add use-query-params 19 | yalc add serialize-query-params 20 | ``` 21 | 22 | 23 | 24 | # Getting Started with Create React App 25 | 26 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 27 | 28 | ## Available Scripts 29 | 30 | In the project directory, you can run: 31 | 32 | ### `npm start` 33 | 34 | Runs the app in the development mode.\ 35 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 36 | 37 | The page will reload if you make edits.\ 38 | You will also see any lint errors in the console. 39 | 40 | ### `npm test` 41 | 42 | Launches the test runner in the interactive watch mode.\ 43 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 44 | 45 | ### `npm run build` 46 | 47 | Builds the app for production to the `build` folder.\ 48 | It correctly bundles React in production mode and optimizes the build for the best performance. 49 | 50 | The build is minified and the filenames include the hashes.\ 51 | Your app is ready to be deployed! 52 | 53 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 54 | 55 | ### `npm run eject` 56 | 57 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 58 | 59 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 60 | 61 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 62 | 63 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 64 | 65 | ## Learn More 66 | 67 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 68 | 69 | To learn React, check out the [React documentation](https://reactjs.org/). 70 | -------------------------------------------------------------------------------- /examples/website-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website-example", 3 | "version": "0.1.0", 4 | "homepage": "https://pbeshai.github.io/use-query-params", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.11.45", 12 | "@types/react": "^18.0.15", 13 | "@types/react-dom": "^18.0.6", 14 | "@types/react-syntax-highlighter": "^13.5.0", 15 | "@types/nanoid": "^2.1.0", 16 | "react-syntax-highlighter": "^15.4.3", 17 | "nanoid": "^3.1.22", 18 | "query-string": "^7.1.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router": "^6.3.0", 22 | "react-router-dom": "^6.3.0", 23 | "react-scripts": "5.0.1", 24 | "serialize-query-params": "file:.yalc/serialize-query-params", 25 | "typescript": "^4.7.4", 26 | "use-query-params": "file:.yalc/use-query-params", 27 | "web-vitals": "^2.1.4" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject", 34 | "copy-build": "rsync -vaHE --delete ./build/ ../../docs/" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "resolutions": { 55 | "react": "18.2.0", 56 | "react-dom": "18.2.0", 57 | "react-router-dom": "6.3.0", 58 | "react-router": "6.3.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/website-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/website-example/public/favicon.ico -------------------------------------------------------------------------------- /examples/website-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | use-query-params 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/website-example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/website-example/public/logo192.png -------------------------------------------------------------------------------- /examples/website-example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbeshai/use-query-params/b14c97ec2e7b1dfaca51d4d17439f9c306b34dba/examples/website-example/public/logo512.png -------------------------------------------------------------------------------- /examples/website-example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/website-example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/website-example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UseQueryParamExample from './UseQueryParamExample'; 3 | import UseQueryParamsExample from './UseQueryParamsExample'; 4 | 5 | const App = (props: any) => { 6 | const [example, setExample] = React.useState(0); 7 | 8 | return ( 9 |
10 |
11 |

useQueryParams

12 |
13 | GitHub 14 |
15 |
16 | 22 | 28 |
29 |
30 |
31 | {example === 0 && } 32 | {example === 1 && } 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /examples/website-example/src/UseQueryParamExample.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | import * as React from 'react'; 3 | import SyntaxHighlighter from 'react-syntax-highlighter'; 4 | import { atomOneLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 5 | import { 6 | NumericArrayParam, 7 | StringParam, 8 | useQueryParam, 9 | } from 'use-query-params'; 10 | 11 | const UseQueryParamExample = () => { 12 | const [foo, setFoo] = useQueryParam('foo', StringParam); 13 | const [arr, setArr] = useQueryParam('arr', NumericArrayParam); 14 | 15 | // verify we aren't creating new arrays each time 16 | const prevArr = React.useRef(arr); 17 | React.useEffect(() => { 18 | if (prevArr.current !== arr) { 19 | console.log('new array. was:', prevArr.current, 'now:', arr); 20 | } else { 21 | console.log('same array'); 22 | } 23 | prevArr.current = arr; 24 | }); 25 | 26 | const nextFoo = nanoid(4); 27 | const nextArr = [ 28 | Math.round(Math.random() * 100), 29 | Math.round(Math.random() * 100), 30 | Math.round(Math.random() * 100), 31 | ]; 32 | 33 | return ( 34 |
35 |

useQueryParam Example

36 |
37 | 38 | const [foo, setFoo] = useQueryParam('foo', StringParam) 39 | 40 |
41 | The value of foo is{' '} 42 | {foo === undefined ? 'undefined' : JSON.stringify(foo)} 43 | 46 |
47 |
48 |
49 | 50 | const [arr, setArr] = useQueryParam('arr', NumericArrayParam) 51 | 52 |
53 | The value of arr is{' '} 54 | {arr === undefined ? 'undefined' : JSON.stringify(arr)} 55 | 58 |

59 | Since we specify the update type as push, the back 60 | button will work. If we used pushIn, the value of{' '} 61 | foo would be retained. 62 |

63 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default UseQueryParamExample; 73 | -------------------------------------------------------------------------------- /examples/website-example/src/UseQueryParamsExample.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | import * as React from 'react'; 3 | import SyntaxHighlighter from 'react-syntax-highlighter'; 4 | import { atomOneLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 5 | import { 6 | NumericArrayParam, 7 | StringParam, 8 | useQueryParams, 9 | } from 'use-query-params'; 10 | 11 | const UseQueryParamsExample = () => { 12 | const [query, setQuery] = useQueryParams({ 13 | foo: StringParam, 14 | arr: NumericArrayParam, 15 | }); 16 | const { foo, arr } = query; 17 | 18 | // verify we aren't creating new arrays each time 19 | const prevArr = React.useRef(arr); 20 | React.useEffect(() => { 21 | if (prevArr.current !== arr) { 22 | console.log('new array. was:', prevArr.current, 'now:', arr); 23 | } else { 24 | console.log('same array'); 25 | } 26 | prevArr.current = arr; 27 | }); 28 | 29 | const nextFoo = nanoid(4); 30 | const nextArr = [ 31 | Math.round(Math.random() * 100), 32 | Math.round(Math.random() * 100), 33 | Math.round(Math.random() * 100), 34 | ]; 35 | 36 | return ( 37 |
38 |

useQueryParams Example

39 |
40 | 41 | {`const [query, setQuery] = useQueryParams({ 42 | foo: StringParam, 43 | arr: NumericArrayParam, 44 | }); 45 | const { foo, arr } = query;`} 46 | 47 |
48 | The value of foo is{' '} 49 | {foo === undefined ? 'undefined' : JSON.stringify(foo)} 50 | 56 |
57 |
58 |
59 |
60 | The value of arr is{' '} 61 | {arr === undefined ? 'undefined' : JSON.stringify(arr)} 62 | 68 |

69 | Since we specify the update type as push, the back 70 | button will work. If we used pushIn, the value of{' '} 71 | foo would be retained. 72 |

73 | 79 |

80 | With setQuery, we can update multiple parameters at 81 | once. 82 |

83 | 89 |
90 |
91 |
92 | ); 93 | }; 94 | 95 | export default UseQueryParamsExample; 96 | -------------------------------------------------------------------------------- /examples/website-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; 3 | } 4 | 5 | a { 6 | color: #0f8cff; 7 | text-decoration: none; 8 | } 9 | 10 | a:hover { 11 | text-decoration: underline; 12 | } 13 | 14 | p { 15 | line-height: 1.5; 16 | margin-top: 2rem; 17 | } 18 | 19 | button { 20 | margin: 0.2rem 0.5rem; 21 | font-size: 16px; 22 | padding: 0.5em 1.0em; 23 | background: #eee; 24 | box-shadow: 0 2px 0px rgba(0,0,0,0.15); 25 | border: 1px solid #ccc; 26 | border-top: none; 27 | border-radius: 4px; 28 | } 29 | 30 | button:focus { 31 | outline: none; 32 | } 33 | 34 | button:hover { 35 | background-color: #f4f4f4; 36 | } 37 | 38 | button:active { 39 | background-color: #f8f8f8; 40 | box-shadow: 0 -2px 0 rgba(0,0,0,0.15); 41 | border-color: #ddd; 42 | } 43 | 44 | button.active { 45 | font-weight: bold; 46 | } 47 | 48 | table { 49 | margin: 1rem; 50 | } 51 | 52 | td { 53 | padding: 0 1rem; 54 | } 55 | 56 | .text-center { text-align: center; } 57 | 58 | 59 | 60 | .App { 61 | max-width: 650px; 62 | margin: 0 auto; 63 | } 64 | 65 | .nav { 66 | text-align: center; 67 | margin-bottom: 3rem; 68 | } 69 | 70 | .ext-nav { 71 | margin-bottom: 1.5em; 72 | } 73 | 74 | .example-block { 75 | margin-bottom: 3rem; 76 | } 77 | 78 | .set-btn { 79 | font-family: monospace; 80 | } -------------------------------------------------------------------------------- /examples/website-example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { QueryParamProvider } from 'use-query-params'; 6 | import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; 7 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 8 | // optionally use the query-string parse / stringify functions to 9 | // handle more advanced cases than URLSearchParams supports. 10 | import { parse, stringify } from 'query-string'; 11 | 12 | const root = ReactDOM.createRoot( 13 | document.getElementById('root') as HTMLElement 14 | ); 15 | root.render( 16 | 17 | 18 | 25 | 26 | } /> 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /examples/website-example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/website-example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/website-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "latest", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "version": "independent", 9 | "registry": "https://registry.npmjs.org/" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "@babel/core": "^7.18.6", 6 | "@babel/preset-env": "^7.18.6", 7 | "@babel/preset-react": "^7.13.13", 8 | "@babel/preset-typescript": "^7.18.6", 9 | "@better-builds/package-bundler": "^1.3.1", 10 | "@testing-library/jest-dom": "^5.16.4", 11 | "@testing-library/react": "^12.1.5", 12 | "@testing-library/react-hooks": "^8.0.1", 13 | "@types/react": "^17.0.0", 14 | "@types/react-router": "^5.1.13", 15 | "@types/react-router-dom": "^5.3.3", 16 | "@typescript-eslint/eslint-plugin": "^4.22.0", 17 | "@typescript-eslint/parser": "^4.22.0", 18 | "eslint": "^7.25.0", 19 | "eslint-plugin-import": "^2.22.1", 20 | "eslint-plugin-react": "^7.23.2", 21 | "eslint-plugin-react-hooks": "^4.2.0", 22 | "jsdom": "^20.0.0", 23 | "lerna": "^5.1.6", 24 | "lerna-audit": "^1.3.3", 25 | "npm-run-all": "^4.1.5", 26 | "prettier": "^2.2.1", 27 | "query-string": "^6.12.1", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "rimraf": "^3.0.2", 31 | "tsd": "^0.22.0", 32 | "typescript": "^4.2.4", 33 | "vitest": "^0.18.1" 34 | }, 35 | "workspaces": { 36 | "packages": [ 37 | "packages/*" 38 | ], 39 | "nohoist": [ 40 | "**/react-router", 41 | "**/react-router-dom", 42 | "**/history", 43 | "**/react-router-dom-6", 44 | "**/react-router-dom-5", 45 | "**/history-5", 46 | "**/history-4" 47 | ] 48 | }, 49 | "scripts": { 50 | "audit": "lerna-audit", 51 | "clean": "lerna run clean", 52 | "build": "lerna run build --scope \"*-query-params\"", 53 | "build:uqp": "lerna run build --scope \"use-query-params\"", 54 | "build:adapter": "lerna run build --scope \"use-query-params-adapter-react-router-6\"", 55 | "build:sqp": "lerna run build --scope \"serialize-query-params\"", 56 | "clean:uqp": "lerna run clean --scope \"use-query-params\"", 57 | "test": "vitest", 58 | "test:uqp": "vitest --root ./packages/use-query-params/", 59 | "test:sqp-all": "yarn workspace serialize-query-params test-all", 60 | "test:sqp": "vitest --root ./packages/serialize-query-params/", 61 | "test:lerna": "lerna run test --scope \"*-query-params\" --stream", 62 | "lint": "lerna run lint --scope \"*-query-params\"", 63 | "prettier": "lerna run prettier --scope \"*-query-params\"" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/serialize-query-params/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## serialize-query-params v2.0.2 4 | 5 | **Fixes** 6 | - `updateLocation` returns an empty string for `href` when the original `location.href` is not defined. This allows `updateLocation` to be recursively recalled without crashing when creating a `new URL()` (#244) 7 | 8 | ## serialize-query-params v2.0.1 9 | 10 | **Fixes** 11 | - type for urlName is now string (#227) 12 | 13 | ## serialize-query-params v2.0.0 14 | 15 | **Breaking** 16 | 17 | - Drop dependency for [query-string](https://github.com/sindresorhus/query-string). You now must pass your stringify/parse functions directly to `updateLocation` and `updateInLocation`. 18 | 19 | **New Features** 20 | 21 | - `objectToSearchString` - small wrapper around URLSearchParams that handles undefined, null, and array values. A lesser version of query-string’s “stringify” 22 | - `searchStringToObject` - small wrapper around URLSearchParams that handles array values. A lesser version of query-string’s “parse” 23 | - Adds new `createEnumArrayParam` and `createEnumDelimitedArrayParam` helpers 24 | - Adds `default` and `urlName` as optional attributes of a Parameter, mostly for other tooling to introspect on. Note that `withDefault` now populates the `default` attribute. 25 | -------------------------------------------------------------------------------- /packages/serialize-query-params/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/serialize-query-params/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serialize-query-params", 3 | "version": "2.0.2", 4 | "description": "A library for simplifying encoding and decoding URL query parameters.", 5 | "main": "./dist/index.cjs.js", 6 | "types": "./dist/index.d.ts", 7 | "typings": "./dist/index.d.ts", 8 | "module": "./dist/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "scripts": { 14 | "build": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json", 15 | "clean": "rimraf dist", 16 | "dev": "NODE_ENV=development tsc -w", 17 | "prepublishOnly": "npm-run-all test-all clean build", 18 | "test": "vitest run", 19 | "test-watch": "vitest watch", 20 | "test-coverage": "vitest run --coverage", 21 | "test-types": "tsd", 22 | "test-all": "npm-run-all --parallel test test-types" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/pbeshai/use-query-params.git" 27 | }, 28 | "keywords": [ 29 | "serialization", 30 | "serialize", 31 | "deserialize", 32 | "encode", 33 | "decode", 34 | "url", 35 | "query", 36 | "parameters", 37 | "query param" 38 | ], 39 | "author": "Peter Beshai ", 40 | "license": "ISC", 41 | "devDependencies": { 42 | "@types/node": "15.0.1" 43 | }, 44 | "tsd": { 45 | "directory": "src" 46 | }, 47 | "gitHead": "4589090c353d8131dc11a8ea7d55ae4d859ba624" 48 | } 49 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/decodeQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest'; 2 | import { decodeQueryParams } from '../index'; 3 | 4 | import { 5 | NumberParam, 6 | ArrayParam, 7 | StringParam, 8 | DelimitedArrayParam, 9 | } from '../params'; 10 | import withDefault from '../withDefault'; 11 | 12 | describe('decodeQueryParams', () => { 13 | it('works', () => { 14 | const decodedQuery = decodeQueryParams( 15 | { 16 | foo: StringParam, 17 | bar: NumberParam, 18 | baz: ArrayParam, 19 | box: DelimitedArrayParam, 20 | not: withDefault(NumberParam, 94), 21 | }, 22 | { 23 | foo: '123', 24 | bar: '555', 25 | baz: ['a', 'b', 'c'], 26 | box: 'a_b_c', 27 | } 28 | ); 29 | expect(decodedQuery).toEqual({ 30 | foo: '123', 31 | bar: 555, 32 | baz: ['a', 'b', 'c'], 33 | box: ['a', 'b', 'c'], 34 | not: 94, 35 | }); 36 | }); 37 | 38 | it('ignores unconfigured parameters', () => { 39 | const decodedQuery = decodeQueryParams( 40 | { 41 | foo: StringParam, 42 | }, 43 | { 44 | foo: '123', 45 | bar: '555', 46 | baz: ['a', 'b', 'c'], 47 | box: 'a,b,c', 48 | } as any 49 | ); 50 | 51 | expect(decodedQuery).toEqual({ 52 | foo: '123', 53 | bar: '555', 54 | baz: ['a', 'b', 'c'], 55 | box: 'a,b,c', 56 | } as any); 57 | }); 58 | 59 | it('ignores configured parameters with no value', () => { 60 | const decodedQuery = decodeQueryParams( 61 | { 62 | foo: StringParam, 63 | bar: NumberParam, 64 | }, 65 | { bar: '555' } 66 | ); 67 | expect(decodedQuery).toEqual({ 68 | bar: 555, 69 | }); 70 | }); 71 | 72 | it('handles nully and empty values', () => { 73 | const encodedQuery = decodeQueryParams( 74 | { 75 | und: StringParam, 76 | emp: StringParam, 77 | nul: NumberParam, 78 | }, 79 | { nul: null, emp: '', und: undefined } 80 | ); 81 | expect(encodedQuery).toEqual({ 82 | nul: null, 83 | emp: '', 84 | und: undefined, 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/encodeQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest'; 2 | import { encodeQueryParams } from '../index'; 3 | import { 4 | NumberParam, 5 | ArrayParam, 6 | StringParam, 7 | DelimitedArrayParam, 8 | } from '../params'; 9 | 10 | describe('encodeQueryParams', () => { 11 | it('works', () => { 12 | const encodedQuery = encodeQueryParams( 13 | { 14 | foo: StringParam, 15 | bar: NumberParam, 16 | baz: ArrayParam, 17 | box: DelimitedArrayParam, 18 | }, 19 | { foo: '123', bar: 555, baz: ['a', 'b', 'c'], box: ['a', 'b', 'c'] } 20 | ); 21 | expect(encodedQuery).toEqual({ 22 | foo: '123', 23 | bar: '555', 24 | baz: ['a', 'b', 'c'], 25 | box: 'a_b_c', 26 | }); 27 | }); 28 | 29 | it('ignores unconfigured parameters', () => { 30 | const encodedQuery = encodeQueryParams( 31 | { 32 | foo: StringParam, 33 | }, 34 | { 35 | foo: '123', 36 | bar: 555, 37 | baz: ['a', 'b', 'c'], 38 | box: ['a', 'b', 'c'], 39 | } as any 40 | ); 41 | expect(encodedQuery).toEqual({ 42 | foo: '123', 43 | bar: '555', // unconfigured defaults to string 44 | baz: 'a,b,c', // unconfigured defaults to string 45 | box: 'a,b,c', // unconfigured defaults to string 46 | }); 47 | }); 48 | 49 | it('ignores configured parameters with no value', () => { 50 | const encodedQuery = encodeQueryParams( 51 | { 52 | foo: StringParam, 53 | emp: StringParam, 54 | nul: StringParam, 55 | bar: NumberParam, 56 | }, 57 | { bar: 555, emp: '', nul: null } 58 | ); 59 | expect(encodedQuery).toEqual({ 60 | bar: '555', 61 | emp: '', 62 | nul: null, 63 | }); 64 | }); 65 | 66 | it('handles nully and empty values', () => { 67 | const encodedQuery = encodeQueryParams( 68 | { 69 | emp: StringParam, 70 | nul: StringParam, 71 | bar: NumberParam, 72 | }, 73 | { bar: undefined, emp: '', nul: null } 74 | ); 75 | expect(encodedQuery).toEqual({ 76 | bar: undefined, 77 | emp: '', 78 | nul: null, 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'query-string'; 2 | import { EncodedQuery } from '..'; 3 | 4 | export function makeMockLocation(query: EncodedQuery, includeHref: boolean = true): Location { 5 | const queryStr = stringify(query); 6 | const search = queryStr.length ? `?${queryStr}` : ''; 7 | return { 8 | protocol: 'http:', 9 | host: 'localhost:3000', 10 | pathname: '/', 11 | search, 12 | href: includeHref ? 'http://localhost:3000/' + search : undefined, 13 | key: `mock-${Date.now()}`, 14 | } as Location & { key: string }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/setupTests.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/types.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import { 3 | DecodedValueMap, 4 | StringParam, 5 | NumberParam, 6 | ArrayParam, 7 | withDefault, 8 | createEnumParam, 9 | createEnumArrayParam, 10 | } from '..'; 11 | 12 | // test basic decoded values 13 | const queryParamConfig = { str: StringParam, num: NumberParam }; 14 | const decodedValueMap: DecodedValueMap = { 15 | str: queryParamConfig.str.decode('foo'), 16 | num: queryParamConfig.num.decode('9'), 17 | }; 18 | expectType<{ str: string | null | undefined; num: number | null | undefined }>( 19 | decodedValueMap 20 | ); 21 | 22 | // test decoded values with defaults including null and not 23 | const queryParamConfig2 = { 24 | str: withDefault(StringParam, 'x', false), 25 | num: withDefault(NumberParam, 0), 26 | }; 27 | const decodedValueMap2: DecodedValueMap = { 28 | str: queryParamConfig2.str.decode('foo'), 29 | num: queryParamConfig2.num.decode('9'), 30 | }; 31 | expectType<{ str: string | null; num: number }>(decodedValueMap2); 32 | 33 | // test enum param with default 34 | const MyEnumParam = createEnumParam(['foo', 'bar']); 35 | const queryParamConfig3 = { 36 | enum: withDefault(MyEnumParam, 'foo' as const), 37 | }; 38 | 39 | const decodedValueMap3: DecodedValueMap = { 40 | enum: queryParamConfig3.enum.decode('foo'), 41 | }; 42 | expectType<{ enum: 'foo' | 'bar' }>(decodedValueMap3); 43 | 44 | // test enum param with default array type 45 | const queryParamConfig4 = { 46 | arr: withDefault(ArrayParam, ['a', 'b']), 47 | }; 48 | 49 | const decodedValueMap4: DecodedValueMap = { 50 | arr: queryParamConfig4.arr.decode(['x', 'b']), 51 | }; 52 | expectType<{ arr: string[] | (string | null)[] }>(decodedValueMap4); 53 | 54 | // test enum param with default array type 55 | const queryParamConfig5 = { 56 | arr: withDefault(createEnumArrayParam(['foo', 'bar']), ['foo'] as ( 57 | | 'foo' 58 | | 'bar' 59 | )[]), 60 | }; 61 | 62 | const decodedValueMap5: DecodedValueMap = { 63 | arr: queryParamConfig5.arr.decode(['x', 'b']), 64 | }; 65 | expectType<{ arr: ('foo' | 'bar')[] }>(decodedValueMap5); 66 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/__tests__/updateLocation.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest'; 2 | import { 3 | updateLocation, 4 | updateInLocation, 5 | transformSearchStringJsonSafe, 6 | } from '../index'; 7 | import { makeMockLocation } from './helpers'; 8 | import { parse, stringify } from 'query-string'; 9 | 10 | describe('updateLocation', () => { 11 | it('creates the correct search string', () => { 12 | const location = makeMockLocation({ 13 | foo: 'abc', 14 | bar: '555', 15 | baz: '222', 16 | }); 17 | const newLocation = updateLocation( 18 | { 19 | foo: 'xxx', 20 | pgb: null, 21 | und: undefined, 22 | emp: '', 23 | }, 24 | location, 25 | stringify 26 | ); 27 | expect(parse(newLocation.search)).toEqual({ 28 | foo: 'xxx', 29 | pgb: null, 30 | emp: '', 31 | }); 32 | expect((newLocation as any).key).toBeDefined(); 33 | expect((newLocation as any).key).not.toBe((location as any).key); 34 | // include updated search string 35 | expect(newLocation.href).toBe( 36 | 'http://localhost:3000/' + newLocation.search 37 | ); 38 | 39 | // check multiple params 40 | expect( 41 | parse(updateLocation({ foo: 'a', baz: 'b' }, location, stringify).search) 42 | ).toEqual({ foo: 'a', baz: 'b' }); 43 | }); 44 | 45 | it('works with no query params', () => { 46 | // check updating to no params 47 | const location = makeMockLocation({ foo: 'abc', bar: '555' }); 48 | const newLocation = updateLocation({}, location); 49 | expect(newLocation.search).toBe(''); 50 | expect(newLocation.href).toBe('http://localhost:3000/'); 51 | 52 | // check updating from no params 53 | expect(updateLocation({ foo: 'xxx' }, makeMockLocation({})).search).toBe( 54 | '?foo=xxx' 55 | ); 56 | }); 57 | 58 | it('handles custom stringify options', () => { 59 | const location = makeMockLocation({ 60 | foo: 'one two', 61 | bar: '[({1,2:3:4,5})]', 62 | }); 63 | const newLocation = updateLocation( 64 | { foo: 'o t h', bar: '[({1,2:3:6,5})]' }, 65 | location, 66 | (params) => stringify(params, { encode: false }) 67 | ); 68 | expect(newLocation.search).toBe('?bar=[({1,2:3:6,5})]&foo=o t h'); 69 | 70 | const newLocation2 = updateLocation( 71 | { foo: 'o t h', bar: '[({1,2:6:4,5})]' }, 72 | location, 73 | (params) => { 74 | const str = stringify(params, { encode: false }); 75 | return transformSearchStringJsonSafe(str).replace(/ /g, '%20'); 76 | } 77 | ); 78 | expect(newLocation2.search).toBe('?bar=[({1,2:6:4,5})]&foo=o%20t%20h'); 79 | }); 80 | it('call it twice, should not throw exception', () => { 81 | const location = makeMockLocation({ 82 | foo: 'one two', 83 | bar: '[({1,2:3:4,5})]', 84 | }, false); 85 | const newLocationAsReturned = updateLocation( 86 | { foo: 'o t h', bar: '[({1,2:3:6,5})]' }, 87 | location, 88 | (params) => stringify(params, { encode: false }) 89 | ); 90 | const newLocation2 = updateLocation( 91 | { foo: 'o t h', bar: '[({1,2:6:4,5})]' }, 92 | newLocationAsReturned, 93 | (params) => { 94 | const str = stringify(params, { encode: false }); 95 | return transformSearchStringJsonSafe(str).replace(/ /g, '%20'); 96 | } 97 | ); 98 | expect(newLocation2.search).toBe('?bar=[({1,2:6:4,5})]&foo=o%20t%20h'); 99 | }); 100 | }); 101 | 102 | describe('updateInLocation', () => { 103 | it('creates the correct search string', () => { 104 | const location = makeMockLocation({ foo: 'abc', bar: '555', baz: '222' }); 105 | const newLocation = updateInLocation( 106 | { foo: 'xxx', pgb: null, baz: undefined, bar: '' }, 107 | location, 108 | stringify, 109 | parse 110 | ); 111 | expect(parse(newLocation.search)).toEqual({ 112 | foo: 'xxx', 113 | bar: '', 114 | pgb: null, 115 | }); 116 | expect((newLocation as any).key).toBeDefined(); 117 | expect((newLocation as any).key).not.toBe((location as any).key); 118 | 119 | // check multiple params 120 | expect( 121 | parse(updateInLocation({ foo: 'a', baz: 'b' }, location).search) 122 | ).toEqual({ foo: 'a', bar: '555', baz: 'b' }); 123 | }); 124 | 125 | it('works with no query params', () => { 126 | // check updating to no params 127 | const location = makeMockLocation({ foo: 'abc', bar: '555' }); 128 | const newLocation = updateInLocation({}, location); 129 | expect(parse(newLocation.search)).toEqual({ foo: 'abc', bar: '555' }); 130 | 131 | // check updating from no params 132 | expect(updateInLocation({ foo: 'xxx' }, makeMockLocation({})).search).toBe( 133 | '?foo=xxx' 134 | ); 135 | }); 136 | 137 | it('handles stringify options', () => { 138 | const location = makeMockLocation({ 139 | foo: 'one two', 140 | bar: '[({1,2:3:4,5})]', 141 | }); 142 | const newLocation = updateInLocation({ foo: 'o t h' }, location, (params) => 143 | stringify(params, { 144 | encode: false, 145 | }) 146 | ); 147 | expect(newLocation.search).toBe('?bar=[({1,2:3:4,5})]&foo=o t h'); 148 | 149 | const newLocation2 = updateInLocation( 150 | { foo: 'o t h' }, 151 | location, 152 | (params) => { 153 | const str = stringify(params, { encode: false }); 154 | return str.replace(/ /g, '%20'); 155 | } 156 | ); 157 | expect(newLocation2.search).toBe('?bar=[({1,2:3:4,5})]&foo=o%20t%20h'); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/decodeQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { DecodedValueMap, QueryParamConfigMap, EncodedValueMap } from './types'; 2 | 3 | /** 4 | * Convert the values in query to strings via the encode functions configured 5 | * in paramConfigMap 6 | * 7 | * @param paramConfigMap Map from query name to { encode, decode } config 8 | * @param query Query updates mapping param name to decoded value 9 | */ 10 | export function decodeQueryParams( 11 | paramConfigMap: QPCMap, 12 | encodedQuery: Partial> 13 | ): Partial> { 14 | const decodedQuery: Partial> = {}; 15 | 16 | // iterate over all keys in the config (#30) 17 | const paramNames = Object.keys(paramConfigMap); 18 | 19 | // ensure any non configured keys that are in the URL are also included 20 | for (const encodedKey of Object.keys(encodedQuery)) { 21 | if (paramConfigMap[encodedKey] == null) { 22 | paramNames.push(encodedKey); 23 | } 24 | } 25 | 26 | for (const paramName of paramNames) { 27 | const encodedValue = encodedQuery[paramName]; 28 | 29 | if (!paramConfigMap[paramName]) { 30 | if (process.env.NODE_ENV === 'development') { 31 | console.warn( 32 | `Passing through parameter ${paramName} during decoding since it was not configured.` 33 | ); 34 | } 35 | 36 | // NOTE: we could just not include it, but it is probably convenient to have 37 | // it default to be a string type. 38 | (decodedQuery as any)[paramName] = encodedValue; 39 | } else { 40 | decodedQuery[paramName as keyof QPCMap] = paramConfigMap[ 41 | paramName 42 | ].decode(encodedValue as string | (string | null)[] | null); 43 | } 44 | } 45 | 46 | return decodedQuery; 47 | } 48 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/encodeQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { DecodedValueMap, QueryParamConfigMap, EncodedValueMap } from './types'; 2 | 3 | /** 4 | * Convert the values in query to strings via the encode functions configured 5 | * in paramConfigMap 6 | * 7 | * @param paramConfigMap Map from query name to { encode, decode } config 8 | * @param query Query updates mapping param name to decoded value 9 | */ 10 | export function encodeQueryParams( 11 | paramConfigMap: QPCMap, 12 | query: Partial> 13 | ): Partial> { 14 | const encodedQuery: Partial> = {}; 15 | 16 | const paramNames = Object.keys(query); 17 | for (const paramName of paramNames) { 18 | const decodedValue = query[paramName]; 19 | 20 | if (!paramConfigMap[paramName]) { 21 | // NOTE: we could just not encode it, but it is probably convenient to have 22 | // it be included by default as a string type. 23 | (encodedQuery as any)[paramName] = 24 | decodedValue == null ? decodedValue : String(decodedValue); 25 | } else { 26 | encodedQuery[paramName as keyof QPCMap] = paramConfigMap[ 27 | paramName 28 | ].encode(query[paramName]); 29 | } 30 | } 31 | 32 | return encodedQuery; 33 | } 34 | export default encodeQueryParams; 35 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/index.ts: -------------------------------------------------------------------------------- 1 | export { withDefault } from './withDefault'; 2 | 3 | export { 4 | encodeDate, 5 | decodeDate, 6 | encodeBoolean, 7 | decodeBoolean, 8 | encodeNumber, 9 | decodeNumber, 10 | encodeString, 11 | decodeString, 12 | decodeEnum, 13 | decodeArrayEnum, 14 | decodeDelimitedArrayEnum, 15 | encodeJson, 16 | decodeJson, 17 | encodeArray, 18 | decodeArray, 19 | encodeNumericArray, 20 | decodeNumericArray, 21 | encodeDelimitedArray, 22 | decodeDelimitedArray, 23 | encodeDelimitedNumericArray, 24 | decodeDelimitedNumericArray, 25 | encodeObject, 26 | decodeObject, 27 | encodeNumericObject, 28 | decodeNumericObject, 29 | } from './serialize'; 30 | 31 | export { 32 | StringParam, 33 | NumberParam, 34 | ObjectParam, 35 | ArrayParam, 36 | NumericArrayParam, 37 | JsonParam, 38 | DateParam, 39 | DateTimeParam, 40 | BooleanParam, 41 | NumericObjectParam, 42 | DelimitedArrayParam, 43 | DelimitedNumericArrayParam, 44 | createEnumParam, 45 | createEnumArrayParam, 46 | createEnumDelimitedArrayParam, 47 | } from './params'; 48 | 49 | export type { 50 | EncodedQuery, 51 | QueryParamConfig, 52 | QueryParamConfigMap, 53 | DecodedValueMap, 54 | EncodedValueMap, 55 | } from './types'; 56 | 57 | export { 58 | updateLocation, 59 | updateInLocation, 60 | transformSearchStringJsonSafe, 61 | } from './updateLocation'; 62 | 63 | export { encodeQueryParams } from './encodeQueryParams'; 64 | export { decodeQueryParams } from './decodeQueryParams'; 65 | 66 | export { searchStringToObject } from './searchStringToObject'; 67 | export { objectToSearchString } from './objectToSearchString'; 68 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/objectToSearchString.ts: -------------------------------------------------------------------------------- 1 | import { EncodedQuery } from './types'; 2 | 3 | /** 4 | * Default implementation of objectToSearchString powered by URLSearchParams. 5 | * Does not support null values. Does not prefix with "?" 6 | * This converts an object { foo: '123', bar: 'x' } to a search string `?foo=123&bar=x` 7 | * This is only a very basic version, you may prefer the advanced versions offered 8 | * by third party libraries like query-string ("stringify") or qs. 9 | */ 10 | export function objectToSearchString(encodedParams: EncodedQuery): string { 11 | const params = new URLSearchParams(); 12 | const entries = Object.entries(encodedParams); 13 | 14 | for (const [key, value] of entries) { 15 | if (value === undefined) continue; 16 | if (value === null) continue; 17 | 18 | if (Array.isArray(value)) { 19 | for (const item of value) { 20 | params.append(key, item ?? ''); 21 | } 22 | } else { 23 | params.append(key, value); 24 | } 25 | } 26 | 27 | return params.toString(); 28 | } 29 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/params.ts: -------------------------------------------------------------------------------- 1 | import * as Serialize from './serialize'; 2 | import { QueryParamConfig } from './types'; 3 | 4 | /** 5 | * String values 6 | */ 7 | export const StringParam: QueryParamConfig< 8 | string | null | undefined, 9 | string | null | undefined 10 | > = { 11 | encode: Serialize.encodeString, 12 | decode: Serialize.decodeString, 13 | }; 14 | 15 | /** 16 | * String enum 17 | */ 18 | export const createEnumParam = ( 19 | enumValues: T[] 20 | ): QueryParamConfig => ({ 21 | encode: Serialize.encodeString, 22 | decode: (input) => Serialize.decodeEnum(input, enumValues), 23 | }); 24 | 25 | /** 26 | * Array enum 27 | */ 28 | export const createEnumArrayParam = ( 29 | enumValues: T[] 30 | ): QueryParamConfig => ({ 31 | encode: (text) => 32 | Serialize.encodeArray(text == null || Array.isArray(text) ? text : [text]), 33 | decode: (input) => Serialize.decodeArrayEnum(input, enumValues), 34 | }); 35 | 36 | /** 37 | * Array delimited enum 38 | */ 39 | export const createEnumDelimitedArrayParam = ( 40 | enumValues: T[], 41 | entrySeparator = '_' 42 | ): QueryParamConfig => ({ 43 | encode: (text) => 44 | Serialize.encodeDelimitedArray( 45 | text == null || Array.isArray(text) ? text : [text], 46 | entrySeparator 47 | ), 48 | decode: (input) => 49 | Serialize.decodeDelimitedArrayEnum(input, enumValues, entrySeparator), 50 | }); 51 | 52 | /** 53 | * Numbers (integers or floats) 54 | */ 55 | export const NumberParam: QueryParamConfig< 56 | number | null | undefined, 57 | number | null | undefined 58 | > = { 59 | encode: Serialize.encodeNumber, 60 | decode: Serialize.decodeNumber, 61 | }; 62 | 63 | /** 64 | * For flat objects where values are strings 65 | */ 66 | export const ObjectParam: QueryParamConfig< 67 | { [key: string]: string | undefined } | null | undefined, 68 | { [key: string]: string | undefined } | null | undefined 69 | > = { 70 | encode: Serialize.encodeObject, 71 | decode: Serialize.decodeObject, 72 | }; 73 | 74 | /** 75 | * For flat arrays of strings, filters out undefined values during decode 76 | */ 77 | export const ArrayParam: QueryParamConfig< 78 | (string | null)[] | null | undefined, 79 | (string | null)[] | null | undefined 80 | > = { 81 | encode: Serialize.encodeArray, 82 | decode: Serialize.decodeArray, 83 | }; 84 | 85 | /** 86 | * For flat arrays of strings, filters out undefined values during decode 87 | */ 88 | export const NumericArrayParam: QueryParamConfig< 89 | (number | null)[] | null | undefined, 90 | (number | null)[] | null | undefined 91 | > = { 92 | encode: Serialize.encodeNumericArray, 93 | decode: Serialize.decodeNumericArray, 94 | }; 95 | 96 | /** 97 | * For any type of data, encoded via JSON.stringify 98 | */ 99 | export const JsonParam: QueryParamConfig = { 100 | encode: Serialize.encodeJson, 101 | decode: Serialize.decodeJson, 102 | }; 103 | 104 | /** 105 | * For simple dates (YYYY-MM-DD) 106 | */ 107 | export const DateParam: QueryParamConfig< 108 | Date | null | undefined, 109 | Date | null | undefined 110 | > = { 111 | encode: Serialize.encodeDate, 112 | decode: Serialize.decodeDate, 113 | equals: ( 114 | valueA: Date | null | undefined, 115 | valueB: Date | null | undefined 116 | ) => { 117 | if (valueA === valueB) return true; 118 | if (valueA == null || valueB == null) return valueA === valueB; 119 | 120 | // ignore time of day 121 | return ( 122 | valueA.getFullYear() === valueB.getFullYear() && 123 | valueA.getMonth() === valueB.getMonth() && 124 | valueA.getDate() === valueB.getDate() 125 | ); 126 | }, 127 | }; 128 | 129 | /** 130 | * For dates in simplified extended ISO format (YYYY-MM-DDTHH:mm:ss.sssZ or ±YYYYYY-MM-DDTHH:mm:ss.sssZ) 131 | */ 132 | export const DateTimeParam: QueryParamConfig< 133 | Date | null | undefined, 134 | Date | null | undefined 135 | > = { 136 | encode: Serialize.encodeDateTime, 137 | decode: Serialize.decodeDateTime, 138 | equals: ( 139 | valueA: Date | null | undefined, 140 | valueB: Date | null | undefined 141 | ) => { 142 | if (valueA === valueB) return true; 143 | if (valueA == null || valueB == null) return valueA === valueB; 144 | 145 | return valueA.valueOf() === valueB.valueOf(); 146 | }, 147 | }; 148 | 149 | /** 150 | * For boolean values: 1 = true, 0 = false 151 | */ 152 | export const BooleanParam: QueryParamConfig< 153 | boolean | null | undefined, 154 | boolean | null | undefined 155 | > = { 156 | encode: Serialize.encodeBoolean, 157 | decode: Serialize.decodeBoolean, 158 | }; 159 | 160 | /** 161 | * For flat objects where the values are numbers 162 | */ 163 | export const NumericObjectParam: QueryParamConfig< 164 | { [key: string]: number | null | undefined } | null | undefined, 165 | { [key: string]: number | null | undefined } | null | undefined 166 | > = { 167 | encode: Serialize.encodeNumericObject, 168 | decode: Serialize.decodeNumericObject, 169 | }; 170 | 171 | /** 172 | * For flat arrays of strings, filters out undefined values during decode 173 | */ 174 | export const DelimitedArrayParam: QueryParamConfig< 175 | (string | null)[] | null | undefined, 176 | (string | null)[] | null | undefined 177 | > = { 178 | encode: Serialize.encodeDelimitedArray, 179 | decode: Serialize.decodeDelimitedArray, 180 | }; 181 | 182 | /** 183 | * For flat arrays where the values are numbers, filters out undefined values during decode 184 | */ 185 | export const DelimitedNumericArrayParam: QueryParamConfig< 186 | (number | null)[] | null | undefined, 187 | (number | null)[] | null | undefined 188 | > = { 189 | encode: Serialize.encodeDelimitedNumericArray, 190 | decode: Serialize.decodeDelimitedNumericArray, 191 | }; 192 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/searchStringToObject.ts: -------------------------------------------------------------------------------- 1 | import { EncodedQuery } from './types'; 2 | 3 | /** 4 | * Default implementation of searchStringToObject powered by URLSearchParams 5 | * This converts a search string like `?foo=123&bar=x` to { foo: '123', bar: 'x' } 6 | * This is only a very basic version, you may prefer the advanced versions offered 7 | * by third party libraries like query-string ("parse") or qs. 8 | */ 9 | export function searchStringToObject(searchString: string): EncodedQuery { 10 | const params = new URLSearchParams(searchString); 11 | const parsed: EncodedQuery = {}; 12 | for (let [key, value] of params) { 13 | if (Object.prototype.hasOwnProperty.call(parsed, key)) { 14 | if (Array.isArray(parsed[key])) { 15 | (parsed[key] as string[]).push(value); 16 | } else { 17 | parsed[key] = [parsed[key] as string, value]; 18 | } 19 | } else { 20 | parsed[key] = value; 21 | } 22 | } 23 | 24 | return parsed; 25 | } 26 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encoded query parameters, possibly including null or undefined values 3 | */ 4 | export interface EncodedQuery { 5 | [key: string]: string | (string | null)[] | null | undefined; 6 | } 7 | 8 | /** 9 | * Configuration for a query param specifying how to encode it 10 | * (convert it to a string) and decode it (convert it from a string 11 | * back to its native type) 12 | * 13 | * D = type to be encoded 14 | * D2 = type from decode (typically = D) 15 | */ 16 | export interface QueryParamConfig { 17 | /** Convert the query param value to a string */ 18 | encode: (value: D) => string | (string | null)[] | null | undefined; 19 | 20 | /** Convert the query param string value to its native type */ 21 | decode: (value: string | (string | null)[] | null | undefined) => D2; 22 | 23 | /** Checks if two values are equal (otherwise typically shallowEqual will be used) */ 24 | equals?: (valueA: D | D2, valueB: D | D2) => boolean; 25 | 26 | /** 27 | * optionally provide a default value for other tooling 28 | 29 | * @note not typically used by serialize-query-params, but use-query-params 30 | * does and it would be annoying for there to be two slightly different 31 | * types. 32 | */ 33 | default?: D2; 34 | 35 | /** 36 | * optionally provide a different name when in the URL for other tooling 37 | 38 | * @note not typically used by serialize-query-params, but use-query-params 39 | * does and it would be annoying for there to be two slightly different 40 | * types. 41 | */ 42 | urlName?: string; 43 | } 44 | 45 | /** 46 | * Mapping from a query parameter name to a { encode, decode } config 47 | */ 48 | export interface QueryParamConfigMap { 49 | [paramName: string]: QueryParamConfig; 50 | } 51 | 52 | /** 53 | * Mapping from a query parameter name to it's decoded value type 54 | */ 55 | export type DecodedValueMap = { 56 | [P in keyof QPCMap]: ReturnType; 57 | }; 58 | 59 | /** 60 | * Mapping from a query parameter name to it's encoded value type 61 | */ 62 | export type EncodedValueMap = { 63 | [P in keyof QPCMap]: string | (string | null)[] | null | undefined; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/updateLocation.ts: -------------------------------------------------------------------------------- 1 | import { EncodedQuery } from './types'; 2 | import { objectToSearchString } from './objectToSearchString'; 3 | import { searchStringToObject } from '.'; 4 | 5 | /** 6 | * An example of a transformSearchString function that undoes encoding of 7 | * common JSON characters that are technically allowed in URLs. 8 | */ 9 | const JSON_SAFE_CHARS = `{}[],":` 10 | .split('') 11 | .map((d) => [d, encodeURIComponent(d)]); 12 | 13 | function getHrefFromLocation(location: Location, search: string): string { 14 | // https://developer.mozilla.org/en-US/docs/Web/API/URL/URL 15 | let href: string = search; 16 | 17 | if (location.href) { 18 | // TODO - implement base option if location.href is relative 19 | // see https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#syntax 20 | try { 21 | const url = new URL(location.href); 22 | href = `${url.origin}${url.pathname}${search}`; 23 | } catch (e) { 24 | href = ''; 25 | } 26 | } 27 | 28 | return href; 29 | } 30 | 31 | export function transformSearchStringJsonSafe(searchString: string): string { 32 | let str = searchString; 33 | for (let [char, code] of JSON_SAFE_CHARS) { 34 | str = str.replace(new RegExp('\\' + code, 'g'), char); 35 | } 36 | return str; 37 | } 38 | 39 | /** 40 | * Update a location, wiping out parameters not included in encodedQuery 41 | * If a param is set to undefined it will be removed from the URL. 42 | */ 43 | export function updateLocation( 44 | encodedQuery: EncodedQuery, 45 | location: Location, 46 | objectToSearchStringFn = objectToSearchString 47 | ): Location { 48 | let encodedSearchString = objectToSearchStringFn(encodedQuery); 49 | 50 | const search = encodedSearchString.length ? `?${encodedSearchString}` : ''; 51 | 52 | const newLocation: Location & { 53 | key: string; 54 | query: EncodedQuery; 55 | } = { 56 | ...location, 57 | key: `${Date.now()}`, // needed for some routers (e.g. react-router) 58 | href: getHrefFromLocation(location, search), 59 | search, 60 | query: encodedQuery, // needed for some routers (e.g. found) 61 | }; 62 | 63 | return newLocation; 64 | } 65 | 66 | /** 67 | * Update a location while retaining existing parameters. 68 | * If a param is set to undefined it will be removed from the URL. 69 | */ 70 | export function updateInLocation( 71 | encodedQueryReplacements: EncodedQuery, 72 | location: Location, 73 | objectToSearchStringFn = objectToSearchString, 74 | searchStringToObjectFn = searchStringToObject 75 | ): Location { 76 | // explicitly avoid parsing numbers to ensure the 77 | // return type has the same shape as EncodeQuery 78 | const currQuery = searchStringToObjectFn(location.search); 79 | 80 | const newQuery = { 81 | ...currQuery, 82 | ...encodedQueryReplacements, 83 | }; 84 | 85 | return updateLocation(newQuery, location, objectToSearchStringFn); 86 | } 87 | -------------------------------------------------------------------------------- /packages/serialize-query-params/src/withDefault.ts: -------------------------------------------------------------------------------- 1 | import { QueryParamConfig } from './types'; 2 | 3 | /** 4 | * Wrap a given parameter with a default value when undefined or null (optionally, default includes null) 5 | * @param param QueryParamConfig - { encode, decode} to serialize a parameter 6 | * @param defaultValue A default value 7 | * @param includeNull 8 | */ 9 | export function withDefault( 10 | param: QueryParamConfig, 11 | defaultValue: DefaultType, 12 | includeNull?: false | undefined 13 | ): QueryParamConfig | DefaultType>; 14 | export function withDefault( 15 | param: QueryParamConfig, 16 | defaultValue: DefaultType, 17 | includeNull?: true 18 | ): QueryParamConfig | DefaultType>; 19 | export function withDefault( 20 | param: QueryParamConfig, 21 | defaultValue: DefaultType, 22 | includeNull: boolean = true 23 | ): QueryParamConfig { 24 | const decodeWithDefault = ( 25 | ...args: Parameters 26 | ): Exclude | Exclude | DefaultType => { 27 | const decodedValue = param.decode(...args); 28 | 29 | if (decodedValue === undefined) { 30 | return defaultValue; 31 | } 32 | if (includeNull) { 33 | if (decodedValue === null) { 34 | return defaultValue; 35 | } else { 36 | return decodedValue as Exclude; 37 | } 38 | } 39 | 40 | return decodedValue as Exclude; 41 | }; 42 | 43 | // note we add `default` into the param for other tools to introspect 44 | return { ...param, default: defaultValue, decode: decodeWithDefault }; 45 | } 46 | export default withDefault; 47 | -------------------------------------------------------------------------------- /packages/serialize-query-params/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/serialize-query-params/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["src/*", "node_modules/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/serialize-query-params/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './src/__tests__/setupTests.ts', 8 | testTimeout: 5000, 9 | include: ['**/*[.-]{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-reach/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-reach/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-params-adapter-reach", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Reach router adapter for use-query-params.", 6 | "main": "./dist/index.cjs.js", 7 | "typings": "./dist/index.d.ts", 8 | "module": "./dist/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "scripts": { 14 | "build": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json --sourcemap false", 15 | "clean": "rimraf dist", 16 | "prepublishOnly": "npm run test && npm run clean && npm run build", 17 | "lint": "eslint --ext js,ts,tsx src", 18 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", 19 | "test": "vitest", 20 | "test-watch": "vitest watch", 21 | "test-coverage": "vitest run --coverage" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/pbeshai/use-query-params.git" 26 | }, 27 | "author": "Peter Beshai ", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@reach/router": "^1.2.1", 31 | "lint-staged": "^10.5.4", 32 | "prettier": "^2.2.1", 33 | "react": "^17.0.2", 34 | "react-dom": "^17.0.2" 35 | }, 36 | "peerDependencies": { 37 | "react": ">=16.8.0", 38 | "react-dom": ">=16.8.0", 39 | "@reach/router": "^1.2.1" 40 | }, 41 | "dependencies": { 42 | "use-query-params": "file:../use-query-params" 43 | }, 44 | "husky": { 45 | "hooks": { 46 | "pre-commit": "lint-staged" 47 | } 48 | }, 49 | "lint-staged": { 50 | "src/*.{js,jsx,ts,tsx,md}": [ 51 | "prettier --write", 52 | "git add" 53 | ], 54 | "src/*.{js,ts,tsx}": [ 55 | "eslint --fix", 56 | "git add" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-reach/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { globalHistory } from '@reach/router'; 3 | import { useState } from 'react'; 4 | import { PartialLocation, QueryParamAdapterComponent } from 'use-query-params'; 5 | 6 | function makeAdapter() { 7 | const adapter = { 8 | replace(location: PartialLocation) { 9 | globalHistory.navigate(location.search || '?', { 10 | replace: true, 11 | state: location.state, 12 | }); 13 | }, 14 | push(location: PartialLocation) { 15 | globalHistory.navigate(location.search || '?', { 16 | replace: false, 17 | state: location.state, 18 | }); 19 | }, 20 | 21 | get location() { 22 | return globalHistory.location; 23 | }, 24 | }; 25 | 26 | return adapter; 27 | } 28 | 29 | /** 30 | * Adapts @reach/router history to work with our 31 | * { replace, push } interface. 32 | */ 33 | export const ReachAdapter: QueryParamAdapterComponent = ({ children }) => { 34 | // we use a lazy caching solution to prevent #46 from happening 35 | const [adapter] = useState(makeAdapter); 36 | 37 | return children(adapter); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-reach/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/use-query-params-adapter-reach/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["src/*", "node_modules/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-params-adapter-react-router-5", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "React-router 5 adapter for use-query-params.", 6 | "main": "./dist/index.cjs.js", 7 | "typings": "./dist/index.d.ts", 8 | "module": "./dist/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "scripts": { 14 | "build": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json --sourcemap false", 15 | "clean": "rimraf dist", 16 | "prepublishOnly": "npm run test && npm run clean && npm run build", 17 | "lint": "eslint --ext js,ts,tsx src", 18 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", 19 | "test": "vitest", 20 | "test-watch": "vitest watch", 21 | "test-coverage": "vitest run --coverage" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/pbeshai/use-query-params.git" 26 | }, 27 | "author": "Peter Beshai ", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "react-router": "^5.3.3", 31 | "react-router-dom": "^5.3.3", 32 | "history": "^4.9.0", 33 | "lint-staged": "^10.5.4", 34 | "prettier": "^2.2.1", 35 | "react": "^17.0.2", 36 | "react-dom": "^17.0.2" 37 | }, 38 | "peerDependencies": { 39 | "react": ">=16.8.0", 40 | "react-dom": ">=16.8.0", 41 | "react-router": ">=5" 42 | }, 43 | "dependencies": { 44 | "use-query-params": "file:../use-query-params" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "lint-staged": { 52 | "src/*.{js,jsx,ts,tsx,md}": [ 53 | "prettier --write", 54 | "git add" 55 | ], 56 | "src/*.{js,ts,tsx}": [ 57 | "eslint --fix", 58 | "git add" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/src/__tests__/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'history'; 2 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/src/__tests__/react-router-5.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import { createMemoryHistory } from 'history'; 3 | import * as React from 'react'; 4 | import { Router, Route, Switch, Link } from 'react-router-dom'; 5 | import { NumberParam, withDefault } from 'serialize-query-params'; 6 | import { describe, test } from 'vitest'; 7 | import { 8 | QueryParamProvider, 9 | QueryParamOptions, 10 | useQueryParam, 11 | } from 'use-query-params/src'; 12 | import { testSpec } from 'use-query-params/src/__tests__/routers/shared'; 13 | import { ReactRouter5Adapter } from '..'; 14 | 15 | function renderWithRouter( 16 | ui: React.ReactNode, 17 | initialRoute: string, 18 | options?: QueryParamOptions 19 | ) { 20 | const history = createMemoryHistory({ initialEntries: [initialRoute] }); 21 | const results = render( 22 | 23 | 24 | {ui} 25 | 26 | 27 | ); 28 | const rerender = (ui: React.ReactNode, newOptions = options) => 29 | results.rerender( 30 | 31 | 32 | {ui} 33 | 34 | 35 | ); 36 | return { 37 | ...results, 38 | rerender, 39 | history, 40 | }; 41 | } 42 | 43 | afterEach(cleanup); 44 | describe('routers/react-router-5', () => { 45 | testSpec(renderWithRouter); 46 | 47 | // #95 48 | test('useEffect clobber example', async () => { 49 | const App = () => { 50 | return ( 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | const FirstPage = () => { 65 | return ( 66 |
67 |
click-dummy
68 |

This is the first page.

69 | link-to-second 70 |
71 | ); 72 | }; 73 | 74 | const SecondPage = () => { 75 | const [paramFromUrl, setParam] = useQueryParam( 76 | 'param', 77 | withDefault(NumberParam, undefined) 78 | ); 79 | 80 | React.useEffect(() => { 81 | // set the param as the default on mount. 82 | if (paramFromUrl === undefined) { 83 | setParam(10); 84 | } 85 | }, [paramFromUrl, setParam]); 86 | 87 | const param = paramFromUrl === undefined ? 10 : paramFromUrl; 88 | 89 | return ( 90 |
91 |
click-dummy
92 |

This is the second page.

93 |

Param is {param}

94 | link-to-first 95 |

96 | 97 |

98 |
99 | ); 100 | }; 101 | 102 | const { getByText } = renderWithRouter(, ''); 103 | getByText(/link-to-second/).click(); // head to second page 104 | getByText(/link-to-first/); // verify we are on second page 105 | 106 | // wait for next tick (dont know how to make it do this otherwise...) 107 | // (await screen.findByText('click-dummy')).click(); 108 | await new Promise((resolve) => setTimeout(() => resolve(true), 0)); 109 | // verify we are still on second page 110 | getByText(/link-to-first/); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/src/__tests__/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { useHistory, useLocation } from 'react-router-dom'; 3 | import { 4 | QueryParamAdapter, 5 | QueryParamAdapterComponent, 6 | } from 'use-query-params'; 7 | 8 | /** 9 | * Query Param Adapter for react-router v5 10 | */ 11 | export const ReactRouter5Adapter: QueryParamAdapterComponent = ({ 12 | children, 13 | }) => { 14 | // note we need to useLocation() to get re-renders when location changes 15 | // but we prefer to read location directly from history to fix #233 16 | // @ts-ignore-line 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | const location = useLocation(); 19 | 20 | const history = useHistory(); 21 | 22 | const adapter: QueryParamAdapter = { 23 | replace(location) { 24 | history.replace(location.search || '?', location.state); 25 | }, 26 | push(location) { 27 | history.push(location.search || '?', location.state); 28 | }, 29 | get location() { 30 | return history.location; 31 | }, 32 | }; 33 | 34 | return children(adapter); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["src/*", "node_modules/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-5/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './src/__tests__/setupTests.ts', 8 | testTimeout: 5000, 9 | include: ['**/*[.-]{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-params-adapter-react-router-6", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "React-router 6 adapter for use-query-params.", 6 | "main": "./dist/index.cjs.js", 7 | "typings": "./dist/index.d.ts", 8 | "module": "./dist/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "scripts": { 14 | "build": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json --sourcemap false", 15 | "clean": "rimraf dist", 16 | "prepublishOnly": "npm run test && npm run clean && npm run build", 17 | "lint": "eslint --ext js,ts,tsx src", 18 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", 19 | "test": "vitest", 20 | "test-watch": "vitest watch", 21 | "test-coverage": "vitest run --coverage" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/pbeshai/use-query-params.git" 26 | }, 27 | "author": "Peter Beshai ", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@remix-run/router": "^1.3.2", 31 | "@remix-run/web-fetch": "^4.3.2", 32 | "history": "^5.2.0", 33 | "lint-staged": "^10.5.4", 34 | "prettier": "^2.2.1", 35 | "react": "^17.0.2", 36 | "react-dom": "^17.0.2", 37 | "react-router": "^6.8.1", 38 | "react-router-dom": "^6.8.1" 39 | }, 40 | "peerDependencies": { 41 | "react": ">=16.8.0", 42 | "react-dom": ">=16.8.0", 43 | "react-router": "^6.8.1" 44 | }, 45 | "dependencies": { 46 | "use-query-params": "file:../use-query-params" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "src/*.{js,jsx,ts,tsx,md}": [ 55 | "prettier --write", 56 | "git add" 57 | ], 58 | "src/*.{js,ts,tsx}": [ 59 | "eslint --fix", 60 | "git add" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/src/__tests__/react-router-6.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import * as React from 'react'; 3 | import { createMemoryRouter, RouterProvider } from 'react-router-dom'; 4 | import { QueryParamOptions, QueryParamProvider } from 'use-query-params/src'; 5 | import { testSpec } from 'use-query-params/src/__tests__/routers/shared'; 6 | import { afterEach, describe, vi } from 'vitest'; 7 | import { ReactRouter6Adapter } from '..'; 8 | 9 | function renderWithRouter( 10 | ui: React.ReactNode, 11 | initialRoute: string, 12 | options?: QueryParamOptions 13 | ) { 14 | // note this set up is a bit weird due to historical reasons where 15 | // we originally relied on a history object for a router to 16 | // determine push/replace. this is less the focus of newer react 17 | // router versions, but this adapter approach works. 18 | let entries: any[] = [initialRoute]; 19 | const router = createMemoryRouter( 20 | [ 21 | { 22 | path: '/:page?', 23 | element: ( 24 | 25 | {ui} 26 | 27 | ), 28 | }, 29 | ], 30 | { initialEntries: entries } 31 | ); 32 | let history = { 33 | router, 34 | get location() { 35 | return this.router.state.location; 36 | }, 37 | replace: vi.fn(), 38 | push: vi.fn(), 39 | }; 40 | 41 | // keep track of updates to help tests inspect push/replace/rerender 42 | router.subscribe(function (state) { 43 | entries.push(state.location); 44 | if (state.historyAction === 'REPLACE') { 45 | history.replace(state.location); 46 | } else if (state.historyAction === 'PUSH') { 47 | history.push(state.location); 48 | } 49 | }); 50 | 51 | const results = render(); 52 | const rerender = (ui: React.ReactNode, newOptions = options) => { 53 | const newRouter = createMemoryRouter( 54 | [ 55 | { 56 | path: '/:page?', 57 | element: ( 58 | 62 | {ui} 63 | 64 | ), 65 | }, 66 | ], 67 | { initialEntries: entries } 68 | ); 69 | history.router = newRouter; 70 | 71 | // keep track of updates to help tests inspect push/replace/rerender 72 | newRouter.subscribe(function (state) { 73 | entries.push(state.location); 74 | if (state.historyAction === 'REPLACE') { 75 | history.replace(state.location); 76 | } else if (state.historyAction === 'PUSH') { 77 | history.push(state.location); 78 | } 79 | }); 80 | 81 | return results.rerender(); 82 | }; 83 | return { 84 | ...results, 85 | rerender, 86 | history, 87 | }; 88 | } 89 | 90 | afterEach(cleanup); 91 | describe('routers/react-router-6', () => { 92 | testSpec(renderWithRouter); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/src/__tests__/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import { fetch, Request, Response } from '@remix-run/web-fetch'; 7 | 8 | // https://stackoverflow.com/a/74501698/14056107 9 | // prevent Request not being defined in Vitest 10 | if (!globalThis.fetch) { 11 | // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web 12 | // fetch API allows a URL so @remix-run/web-fetch defines 13 | // `fetch(string | URL | Request, ...)` 14 | // @ts-expect-error 15 | globalThis.fetch = fetch; 16 | } 17 | if (!globalThis.Request) { 18 | // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor 19 | // @ts-expect-error 20 | globalThis.Request = Request; 21 | } 22 | if (!globalThis.Response) { 23 | // web-std/fetch Response does not currently implement Response.error() 24 | // @ts-expect-error 25 | globalThis.Response = Response; 26 | } 27 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { 3 | UNSAFE_NavigationContext, 4 | useNavigate, 5 | useLocation, 6 | UNSAFE_DataRouterContext, 7 | } from 'react-router-dom'; 8 | import { 9 | QueryParamAdapter, 10 | QueryParamAdapterComponent, 11 | } from 'use-query-params'; 12 | 13 | /** 14 | * Query Param Adapter for react-router v6 15 | */ 16 | export const ReactRouter6Adapter: QueryParamAdapterComponent = ({ 17 | children, 18 | }) => { 19 | // we need the navigator directly so we can access the current version 20 | // of location in case of multiple updates within a render (e.g. #233) 21 | // but we will limit our usage of it and have a backup to just use 22 | // useLocation() output in case of some kind of breaking change we miss. 23 | // see: https://github.com/remix-run/react-router/blob/f3d87dcc91fbd6fd646064b88b4be52c15114603/packages/react-router-dom/index.tsx#L113-L131 24 | const { navigator } = useContext(UNSAFE_NavigationContext); 25 | const navigate = useNavigate(); 26 | const router = useContext(UNSAFE_DataRouterContext)?.router; 27 | const location = useLocation(); 28 | 29 | const adapter: QueryParamAdapter = { 30 | replace(location) { 31 | navigate(location.search || '?', { 32 | replace: true, 33 | state: location.state, 34 | }); 35 | }, 36 | push(location) { 37 | navigate(location.search || '?', { 38 | replace: false, 39 | state: location.state, 40 | }); 41 | }, 42 | get location() { 43 | // be a bit defensive here in case of an unexpected breaking change in React Router 44 | return ( 45 | router?.state?.location ?? (navigator as any)?.location ?? location 46 | ); 47 | }, 48 | }; 49 | 50 | return children(adapter); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["src/*", "node_modules/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-react-router-6/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './src/__tests__/setupTests.ts', 8 | testTimeout: 5000, 9 | include: ['**/*[.-]{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-window/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-window/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-params-adapter-window", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Window adapter for use-query-params.", 6 | "main": "./dist/index.cjs.js", 7 | "typings": "./dist/index.d.ts", 8 | "module": "./dist/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "scripts": { 14 | "build": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json --sourcemap false", 15 | "clean": "rimraf dist", 16 | "prepublishOnly": "npm run test && npm run clean && npm run build", 17 | "lint": "eslint --ext js,ts,tsx src", 18 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", 19 | "test": "vitest", 20 | "test-watch": "vitest watch", 21 | "test-coverage": "vitest run --coverage" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/pbeshai/use-query-params.git" 26 | }, 27 | "author": "Peter Beshai ", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "lint-staged": "^10.5.4", 31 | "prettier": "^2.2.1", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2" 34 | }, 35 | "peerDependencies": { 36 | "react": ">=16.8.0", 37 | "react-dom": ">=16.8.0" 38 | }, 39 | "dependencies": { 40 | "use-query-params": "file:../use-query-params" 41 | }, 42 | "husky": { 43 | "hooks": { 44 | "pre-commit": "lint-staged" 45 | } 46 | }, 47 | "lint-staged": { 48 | "src/*.{js,jsx,ts,tsx,md}": [ 49 | "prettier --write", 50 | "git add" 51 | ], 52 | "src/*.{js,ts,tsx}": [ 53 | "eslint --fix", 54 | "git add" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-window/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { PartialLocation, QueryParamAdapterComponent } from 'use-query-params'; 3 | 4 | function makeAdapter() { 5 | const adapter = { 6 | replace(location: PartialLocation) { 7 | window.history.replaceState(location.state, '', location.search || '?'); 8 | }, 9 | push(location: PartialLocation) { 10 | window.history.pushState(location.state, '', location.search || '?'); 11 | }, 12 | get location() { 13 | return window.location; 14 | }, 15 | }; 16 | 17 | return adapter; 18 | } 19 | 20 | /** 21 | * Adapts standard DOM window history to work with our 22 | * { replace, push } interface. 23 | */ 24 | export const WindowHistoryAdapter: QueryParamAdapterComponent = ({ 25 | children, 26 | }) => { 27 | // we use a lazy caching solution to prevent #46 from happening 28 | const [adapter] = useState(makeAdapter); 29 | 30 | return children(adapter); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/use-query-params-adapter-window/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/use-query-params-adapter-window/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["src/*", "node_modules/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/use-query-params/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2019-present Peter Beshai 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/use-query-params/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-query-params", 3 | "version": "2.2.1", 4 | "description": "React Hook for managing state in URL query parameters with easy serialization.", 5 | "main": "./dist/index.cjs.js", 6 | "typings": "./dist/index.d.ts", 7 | "module": "./dist/index.js", 8 | "files": [ 9 | "dist", 10 | "src", 11 | "adapters" 12 | ], 13 | "scripts": { 14 | "build": "npm-run-all build:code build:adapters copy:adapters", 15 | "build:code": "package-bundler --copyPackageJson --rewritePackageJson --tsconfigPath ./tsconfig.build.json", 16 | "build:adapters": "npx lerna run build --scope \"*-adapter-*\"", 17 | "copy:adapters": "scripts/copy-adapters.js", 18 | "build-website": "npm run test && npm run build && cd examples/website-example && npm run build && npm run copy-build", 19 | "clean": "rimraf dist adapters", 20 | "dev": "cross-env NODE_ENV=development tsc -m esNext --outDir esm -w", 21 | "prepublishOnly": "npm-run-all test:run clean build", 22 | "lint": "eslint --ext js,ts,tsx src", 23 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", 24 | "test": "vitest", 25 | "test:run": "vitest --run", 26 | "test-watch": "vitest watch", 27 | "test-coverage": "vitest run --coverage" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/pbeshai/use-query-params.git" 32 | }, 33 | "keywords": [ 34 | "react", 35 | "url", 36 | "query", 37 | "parameters", 38 | "hook", 39 | "hooks", 40 | "query param", 41 | "react use" 42 | ], 43 | "author": "Peter Beshai ", 44 | "license": "ISC", 45 | "devDependencies": { 46 | "@babel/preset-react": "^7.13.13", 47 | "@types/history": "^4.7.8", 48 | "@types/react": "^17.0.0", 49 | "@types/react-router": "^5.1.13", 50 | "@types/react-router-dom": "^5.3.3", 51 | "@typescript-eslint/eslint-plugin": "^4.22.0", 52 | "@typescript-eslint/parser": "^4.22.0", 53 | "cross-env": "^7.0.3", 54 | "eslint": "^7.25.0", 55 | "eslint-plugin-import": "^2.22.1", 56 | "eslint-plugin-react": "^7.23.2", 57 | "eslint-plugin-react-hooks": "^4.2.0", 58 | "history-4": "npm:history@^4.9.0", 59 | "history-5": "npm:history@^5.2.0", 60 | "husky": "^6.0.0", 61 | "lint-staged": "^10.5.4", 62 | "prettier": "^2.2.1", 63 | "react": "^17.0.2", 64 | "react-dom": "^17.0.2", 65 | "react-router-5": "npm:react-router@^5.3.3", 66 | "react-router-6": "npm:react-router@^6.8.1", 67 | "react-router-dom-5": "npm:react-router-dom@^5.3.3", 68 | "react-router-dom-6": "npm:react-router-dom@^6.8.1" 69 | }, 70 | "peerDependencies": { 71 | "@reach/router": "^1.2.1", 72 | "react": ">=16.8.0", 73 | "react-dom": ">=16.8.0", 74 | "react-router-dom": ">=5" 75 | }, 76 | "peerDependenciesMeta": { 77 | "@reach/router": { 78 | "optional": true 79 | }, 80 | "react-router-dom": { 81 | "optional": true 82 | } 83 | }, 84 | "dependencies": { 85 | "serialize-query-params": "^2.0.2" 86 | }, 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "lint-staged" 90 | } 91 | }, 92 | "lint-staged": { 93 | "src/*.{js,jsx,ts,tsx,md}": [ 94 | "prettier --write", 95 | "git add" 96 | ], 97 | "src/*.{js,ts,tsx}": [ 98 | "eslint --fix", 99 | "git add" 100 | ] 101 | }, 102 | "gitHead": "4589090c353d8131dc11a8ea7d55ae4d859ba624" 103 | } 104 | -------------------------------------------------------------------------------- /packages/use-query-params/scripts/copy-adapters.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const packagesDir = path.resolve(__dirname, '../../'); 7 | const outputDir = path.resolve(__dirname, '../adapters'); 8 | console.log(packagesDir); 9 | 10 | if (!fs.existsSync(outputDir)) { 11 | fs.mkdirSync(outputDir, { recursive: true }); 12 | } 13 | 14 | const adapterDirs = fs 15 | .readdirSync(packagesDir, { withFileTypes: true }) 16 | .filter((dirent) => dirent.isDirectory() && dirent.name.includes('-adapter-')) 17 | .map((dirent) => dirent.name); 18 | 19 | for (const adapterDir of adapterDirs) { 20 | const adapterName = adapterDir.split('-adapter-')[1]; 21 | const adapterBuildDir = path.resolve(packagesDir, adapterDir, 'dist'); 22 | console.log( 23 | 'copying adapter files for', 24 | adapterName, 25 | 'from', 26 | adapterBuildDir 27 | ); 28 | 29 | const adapterOutDir = path.resolve(outputDir, adapterName); 30 | if (!fs.existsSync(adapterOutDir)) { 31 | fs.mkdirSync(adapterOutDir); 32 | } 33 | 34 | const buildFiles = fs.readdirSync(adapterBuildDir); 35 | for (const buildFile of buildFiles) { 36 | // don't include source map since we dont include the src 37 | if (buildFile !== 'package.json') { 38 | fs.copyFileSync( 39 | path.resolve(adapterBuildDir, buildFile), 40 | path.resolve(adapterOutDir, buildFile) 41 | ); 42 | } 43 | } 44 | 45 | // create a package.json for import 46 | // see #224 https://github.com/pbeshai/use-query-params/issues/224 47 | const packageJson = { 48 | main: 'index.cjs.js', 49 | module: 'index.js', 50 | name: `use-query-params/adapters/${adapterName}`, 51 | types: 'index.d.ts', 52 | }; 53 | fs.writeFileSync( 54 | path.resolve(adapterOutDir, 'package.json'), 55 | JSON.stringify(packageJson, null, 2) 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/use-query-params/src/QueryParamProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | mergeOptions, 4 | defaultOptions, 5 | QueryParamOptions, 6 | QueryParamOptionsWithRequired, 7 | } from './options'; 8 | import { QueryParamAdapter, QueryParamAdapterComponent } from './types'; 9 | 10 | /** 11 | * Shape of the QueryParamContext, which the hooks consume to read and 12 | * update the URL state. 13 | */ 14 | type QueryParamContextValue = { 15 | adapter: QueryParamAdapter; 16 | options: QueryParamOptionsWithRequired; 17 | }; 18 | 19 | const providerlessContextValue: QueryParamContextValue = { 20 | adapter: {} as QueryParamAdapter, 21 | options: defaultOptions, 22 | }; 23 | 24 | export const QueryParamContext = React.createContext( 25 | providerlessContextValue 26 | ); 27 | 28 | export function useQueryParamContext() { 29 | const value = React.useContext(QueryParamContext); 30 | if ( 31 | process.env.NODE_ENV !== 'production' && 32 | (value === undefined || value === providerlessContextValue) 33 | ) { 34 | throw new Error('useQueryParams must be used within a QueryParamProvider'); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | /** 41 | * Props for the Provider component, used to hook the active routing 42 | * system into our controls. Note only the root provider requires 43 | * `adapter`. We try to encourage that via intellisense by writing 44 | * the types this way (you must provide at least one of adapter or options, 45 | * default intellisense suggests adapter required.) 46 | */ 47 | type QueryParamProviderProps = { 48 | /** Main app goes here */ 49 | children: React.ReactNode; 50 | } & ( 51 | | { 52 | adapter?: never; 53 | options: QueryParamOptions; 54 | } 55 | | { 56 | /** required for the root provider but not for nested ones */ 57 | adapter: QueryParamAdapterComponent; 58 | options?: QueryParamOptions; 59 | } 60 | ); 61 | 62 | function QueryParamProviderInner({ 63 | children, 64 | adapter, 65 | options, 66 | }: { 67 | children: React.ReactNode; 68 | adapter?: QueryParamAdapter | undefined; 69 | options?: QueryParamOptions; 70 | }) { 71 | // allow merging in parent options 72 | const { adapter: parentAdapter, options: parentOptions } = 73 | React.useContext(QueryParamContext); 74 | 75 | const value = React.useMemo(() => { 76 | return { 77 | adapter: adapter ?? parentAdapter, 78 | options: mergeOptions( 79 | parentOptions, 80 | options 81 | ) as QueryParamOptionsWithRequired, 82 | }; 83 | }, [adapter, options, parentAdapter, parentOptions]); 84 | 85 | return ( 86 | 87 | {children} 88 | 89 | ); 90 | } 91 | 92 | /** 93 | * Context provider for query params to have access to the 94 | * active routing system, enabling updates to the URL. 95 | */ 96 | export function QueryParamProvider({ 97 | children, 98 | adapter, 99 | options, 100 | }: QueryParamProviderProps) { 101 | const Adapter = adapter; 102 | return Adapter ? ( 103 | 104 | {(adapter) => ( 105 | 106 | {children} 107 | 108 | )} 109 | 110 | ) : ( 111 | 112 | {children} 113 | 114 | ); 115 | } 116 | 117 | export default QueryParamProvider; 118 | -------------------------------------------------------------------------------- /packages/use-query-params/src/QueryParams.tsx: -------------------------------------------------------------------------------- 1 | import { QueryParamConfigMap, DecodedValueMap } from 'serialize-query-params'; 2 | import useQueryParams from './useQueryParams'; 3 | import { SetQuery } from './types'; 4 | 5 | export interface QueryRenderProps { 6 | query: DecodedValueMap; 7 | setQuery: SetQuery; 8 | } 9 | 10 | export interface QueryParamsProps { 11 | config: QPCMap; 12 | children: (renderProps: QueryRenderProps) => JSX.Element; 13 | } 14 | 15 | export const QueryParams = ({ 16 | config, 17 | children, 18 | }: QueryParamsProps) => { 19 | const [query, setQuery] = useQueryParams(config); 20 | return children({ query, setQuery }); 21 | }; 22 | 23 | export default QueryParams; 24 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/QueryParamProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cleanup, render } from '@testing-library/react'; 3 | import { describe, it } from 'vitest'; 4 | import { 5 | QueryParamProvider, 6 | useQueryParamContext, 7 | } from '../QueryParamProvider'; 8 | import { QueryParamAdapter } from '../types'; 9 | import { makeMockAdapter } from './helpers'; 10 | 11 | describe('QueryParamProvider', () => { 12 | afterEach(cleanup); 13 | 14 | it('adapter comes through', () => { 15 | const Adapter = makeMockAdapter({ search: '?foo=99' }); 16 | const adapter = (Adapter as any).adapter as QueryParamAdapter; 17 | 18 | let innerAdapter: any; 19 | const TestComponent = () => { 20 | const { adapter } = useQueryParamContext(); 21 | innerAdapter = adapter; 22 | return
consumed
; 23 | }; 24 | 25 | render( 26 | 27 | 28 | 29 | ); 30 | 31 | expect(innerAdapter).toBe(adapter); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/QueryParams.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { cleanup, render } from '@testing-library/react'; 3 | import * as React from 'react'; 4 | import { describe, it } from 'vitest'; 5 | 6 | import { 7 | DecodedValueMap, 8 | EncodedQuery, 9 | NumberParam, 10 | StringParam, 11 | objectToSearchString, 12 | } from 'serialize-query-params'; 13 | import { 14 | QueryParamAdapter, 15 | QueryParamProvider, 16 | QueryParams, 17 | SetQuery, 18 | } from '../index'; 19 | import { calledPushQuery, makeMockAdapter } from './helpers'; 20 | 21 | // helper to setup tests 22 | function setupWrapper(query: EncodedQuery) { 23 | const Adapter = makeMockAdapter({ search: objectToSearchString(query) }); 24 | const adapter = (Adapter as any).adapter as QueryParamAdapter; 25 | const wrapper = ({ children }: any) => ( 26 | {children} 27 | ); 28 | 29 | return { wrapper, adapter }; 30 | } 31 | 32 | const queryConfig = { foo: NumberParam, bar: StringParam }; 33 | 34 | interface Props { 35 | query: DecodedValueMap; 36 | setQuery: SetQuery; 37 | other: string; 38 | } 39 | 40 | const MockComponent: React.FC = ({ query, setQuery, other }) => { 41 | return ( 42 |
43 |
other = {other}
44 |
foo = {query.foo}
45 |
bar = {query.bar}
46 | 47 |
48 | ); 49 | }; 50 | 51 | describe('QueryParams', () => { 52 | afterEach(cleanup); 53 | 54 | it('works', () => { 55 | const { wrapper, adapter } = setupWrapper({ 56 | foo: '123', 57 | bar: 'xxx', 58 | }); 59 | const { getByText } = render( 60 | 61 | {({ query, setQuery }) => ( 62 | 63 | )} 64 | , 65 | { 66 | wrapper, 67 | } 68 | ); 69 | 70 | // @ts-ignore 71 | expect(getByText(/other = zing/)).toBeInTheDocument(); 72 | // @ts-ignore 73 | expect(getByText(/foo = 123/)).toBeInTheDocument(); 74 | // @ts-ignore 75 | expect(getByText(/bar = xxx/)).toBeInTheDocument(); 76 | getByText(/change foo/).click(); 77 | expect(calledPushQuery(adapter, 0)).toEqual({ foo: '99', bar: 'xxx' }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseQueryString, stringify } from 'query-string'; 2 | import { EncodedQuery } from 'serialize-query-params'; 3 | import { vi } from 'vitest'; 4 | import { 5 | PartialLocation, 6 | QueryParamAdapter, 7 | QueryParamAdapterComponent, 8 | } from '../types'; 9 | 10 | export function makeMockAdapter( 11 | currentLocation: PartialLocation 12 | ): QueryParamAdapterComponent { 13 | const adapter: QueryParamAdapter = { 14 | replace: vi 15 | .fn() 16 | .mockImplementation((newLocation) => 17 | Object.assign(currentLocation, newLocation) 18 | ), 19 | push: vi 20 | .fn() 21 | .mockImplementation((newLocation) => 22 | Object.assign(currentLocation, newLocation) 23 | ), 24 | get location() { 25 | return currentLocation; 26 | }, 27 | }; 28 | 29 | const Adapter = ({ children }: any) => children(adapter); 30 | Adapter.adapter = adapter; 31 | 32 | return Adapter; 33 | } 34 | 35 | // helper to get the query params from the updated location 36 | export function calledPushQuery(adapter: QueryParamAdapter, index: number = 0) { 37 | return parseQueryString((adapter.push as any).mock.calls[index][0].search); 38 | } 39 | 40 | export function makeMockLocation( 41 | query: EncodedQuery, 42 | includeHost: boolean = false 43 | ): Location { 44 | const queryStr = stringify(query); 45 | let newLocation = { 46 | pathname: '/', 47 | search: queryStr.length ? `?${queryStr}` : '', 48 | } as Location; 49 | 50 | if (includeHost) { 51 | newLocation.protocol = 'http:'; 52 | newLocation.host = 'localhost'; 53 | } 54 | 55 | return newLocation; 56 | } 57 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/routers/README.md: -------------------------------------------------------------------------------- 1 | The tests in the shared.tsx are re-used in the adapter packages (e.g. for React Router 5 and 6). This was the only way I could think of to get it working given mixed dependency versions and annoyances with packaging/importing different versions of react and history. -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/routers/mocked.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import * as React from 'react'; 3 | import { describe } from 'vitest'; 4 | import { 5 | PartialLocation, 6 | QueryParamAdapter, 7 | QueryParamAdapterComponent, 8 | QueryParamProvider, 9 | } from '../../index'; 10 | import { QueryParamOptions } from '../../options'; 11 | import { testSpec } from './shared'; 12 | 13 | const createTestHistory = (initialRoute: string = '') => { 14 | const url = new URL( 15 | `http://localhost/${ 16 | initialRoute.startsWith('/') ? initialRoute.slice(1) : initialRoute 17 | }` 18 | ); 19 | const initialLocation = { 20 | search: url.search, 21 | state: {}, 22 | pathname: url.pathname ?? (url as any).path[0], 23 | }; 24 | 25 | let location: PartialLocation = initialLocation; 26 | const history = { 27 | replace(newLocation: PartialLocation) { 28 | location = { ...location, ...newLocation }; 29 | this.onChange(location); 30 | }, 31 | push(newLocation: PartialLocation) { 32 | location = { ...location, ...newLocation }; 33 | this.onChange(location); 34 | }, 35 | get location() { 36 | return location; 37 | }, 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | onChange: (newLocation: PartialLocation) => {}, 41 | }; 42 | 43 | return history; 44 | }; 45 | 46 | type TestHistory = ReturnType; 47 | 48 | const TestRouterContext = React.createContext(createTestHistory()); 49 | 50 | const TestRouter = ({ 51 | history, 52 | children, 53 | }: { 54 | history: TestHistory; 55 | children: React.ReactNode; 56 | }) => { 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | /** 65 | * Query Param Adapter for mocked router 66 | */ 67 | const TestAdapter: QueryParamAdapterComponent = ({ children }) => { 68 | const history = React.useContext(TestRouterContext); 69 | // need a use state here to force a re-render 70 | const [, setLocation] = React.useState(history.location); 71 | React.useLayoutEffect(() => { 72 | history.onChange = setLocation; 73 | }, [history]); 74 | 75 | const adapter: QueryParamAdapter = { 76 | replace(newLocation) { 77 | history.replace(newLocation); 78 | }, 79 | push(newLocation) { 80 | history.push(newLocation); 81 | }, 82 | 83 | // note this always reads the latest in history to fix #233 84 | get location() { 85 | return history.location; 86 | }, 87 | }; 88 | return children(adapter); 89 | }; 90 | 91 | function renderTest( 92 | ui: React.ReactNode, 93 | initialRoute: string, 94 | options?: QueryParamOptions 95 | ): any & { history: any } { 96 | const history = createTestHistory(initialRoute); 97 | const results = render( 98 | 99 | 100 | {ui} 101 | 102 | 103 | ); 104 | const rerender = (ui: React.ReactNode, newOptions = options) => 105 | results.rerender( 106 | 107 | 108 | {ui} 109 | 110 | 111 | ); 112 | return { 113 | ...results, 114 | rerender, 115 | history, 116 | } as any; 117 | } 118 | 119 | afterEach(cleanup); 120 | describe('routers/mocked', () => { 121 | testSpec(renderTest); 122 | }); 123 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import { fetch, Request, Response } from '@remix-run/web-fetch'; 7 | 8 | // https://stackoverflow.com/a/74501698/14056107 9 | // prevent Request not being defined in Vitest 10 | if (!globalThis.fetch) { 11 | // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web 12 | // fetch API allows a URL so @remix-run/web-fetch defines 13 | // `fetch(string | URL | Request, ...)` 14 | // @ts-expect-error 15 | globalThis.fetch = fetch; 16 | } 17 | if (!globalThis.Request) { 18 | // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor 19 | // @ts-expect-error 20 | globalThis.Request = Request; 21 | } 22 | if (!globalThis.Response) { 23 | // web-std/fetch Response does not currently implement Response.error() 24 | // @ts-expect-error 25 | globalThis.Response = Response; 26 | } 27 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/useQueryParam-SSR.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import * as React from 'react'; 5 | import { renderToString } from 'react-dom/server'; 6 | import { StringParam, objectToSearchString } from 'serialize-query-params'; 7 | import { test } from 'vitest'; 8 | import QueryParamProvider from '../QueryParamProvider'; 9 | import { useQueryParam } from '../useQueryParam'; 10 | import { makeMockAdapter } from './helpers'; 11 | 12 | test('SSR initial query param', () => { 13 | const Component = () => { 14 | const [foo] = useQueryParam('foo', StringParam); 15 | 16 | return
{foo}
; 17 | }; 18 | 19 | const query = { foo: 'bar' }; 20 | const Adapter = makeMockAdapter({ search: objectToSearchString(query) }); 21 | 22 | const result = renderToString( 23 | 24 | 25 | 26 | ); 27 | 28 | expect(result).toMatchInlineSnapshot(`"
bar
"`); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/use-query-params/src/__tests__/withQueryParams.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { cleanup, render } from '@testing-library/react'; 3 | import * as React from 'react'; 4 | import { describe, it } from 'vitest'; 5 | 6 | import { 7 | DecodedValueMap, 8 | EncodedQuery, 9 | NumberParam, 10 | StringParam, 11 | objectToSearchString, 12 | } from 'serialize-query-params'; 13 | import { 14 | QueryParamAdapter, 15 | QueryParamProvider, 16 | SetQuery, 17 | withQueryParams, 18 | } from '../index'; 19 | import { calledPushQuery, makeMockAdapter } from './helpers'; 20 | 21 | // helper to setup tests 22 | function setupWrapper(query: EncodedQuery) { 23 | const Adapter = makeMockAdapter({ search: objectToSearchString(query) }); 24 | const adapter = (Adapter as any).adapter as QueryParamAdapter; 25 | const wrapper = ({ children }: any) => ( 26 | {children} 27 | ); 28 | 29 | return { wrapper, adapter }; 30 | } 31 | const queryConfig = { foo: NumberParam, bar: StringParam }; 32 | 33 | interface Props { 34 | query: DecodedValueMap; 35 | setQuery: SetQuery; 36 | other: string; 37 | } 38 | 39 | const MockComponent: React.FC = ({ query, setQuery, other }) => { 40 | return ( 41 |
42 |
other = {other}
43 |
foo = {query.foo}
44 |
bar = {query.bar}
45 | 46 |
47 | ); 48 | }; 49 | 50 | const MockWithHoc = withQueryParams(queryConfig, MockComponent); 51 | 52 | describe('withQueryParams', () => { 53 | afterEach(cleanup); 54 | 55 | it('works', () => { 56 | const { wrapper, adapter } = setupWrapper({ 57 | foo: '123', 58 | bar: 'xxx', 59 | }); 60 | const { getByText } = render(, { 61 | wrapper, 62 | }); 63 | 64 | // @ts-ignore 65 | expect(getByText(/other = zing/)).toBeInTheDocument(); 66 | // @ts-ignore 67 | expect(getByText(/foo = 123/)).toBeInTheDocument(); 68 | // @ts-ignore 69 | expect(getByText(/bar = xxx/)).toBeInTheDocument(); 70 | getByText(/change foo/).click(); 71 | expect(calledPushQuery(adapter, 0)).toEqual({ foo: '99', bar: 'xxx' }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/use-query-params/src/decodedParamCache.ts: -------------------------------------------------------------------------------- 1 | type EncodedValue = string | (string | null)[] | null | undefined; 2 | 3 | type CachedParam = { 4 | stringified: EncodedValue; 5 | decoded: any; 6 | decode: Function; 7 | }; 8 | 9 | /** 10 | * simple cache that keeps values around so long as something 11 | * has registered interest in it (typically via calling useQueryParams). 12 | * Caches based on the stringified value as the key and the 13 | * last passed in decode function. 14 | */ 15 | export class DecodedParamCache { 16 | private paramsMap: Map; 17 | private registeredParams: Map; 18 | 19 | constructor() { 20 | this.paramsMap = new Map(); 21 | this.registeredParams = new Map(); 22 | } 23 | 24 | set( 25 | param: string, 26 | stringifiedValue: EncodedValue, 27 | decodedValue: any, 28 | decode: Function 29 | ) { 30 | this.paramsMap.set(param, { 31 | stringified: stringifiedValue, 32 | decoded: decodedValue, 33 | decode, 34 | }); 35 | } 36 | 37 | /** 38 | * A param has been cached if the stringified value and decode function matches 39 | */ 40 | has(param: string, stringifiedValue: EncodedValue, decode?: Function) { 41 | if (!this.paramsMap.has(param)) return false; 42 | const cachedParam = this.paramsMap.get(param); 43 | if (!cachedParam) return false; 44 | 45 | return ( 46 | cachedParam.stringified === stringifiedValue && 47 | (decode == null || cachedParam.decode === decode) 48 | ); 49 | } 50 | 51 | get(param: string) { 52 | if (this.paramsMap.has(param)) return this.paramsMap.get(param)?.decoded; 53 | return undefined; 54 | } 55 | 56 | /** 57 | * Register interest in a set of param names. When these go to 0 they are cleaned out. 58 | */ 59 | registerParams(paramNames: string[]) { 60 | for (const param of paramNames) { 61 | const currValue = this.registeredParams.get(param) || 0; 62 | this.registeredParams.set(param, currValue + 1); 63 | } 64 | } 65 | 66 | /** 67 | * Unregister interest in a set of param names. If there is no remaining interest, 68 | * remove the decoded value from the cache to prevent memory leaks. 69 | */ 70 | unregisterParams(paramNames: string[]) { 71 | for (const param of paramNames) { 72 | const value = (this.registeredParams.get(param) || 0) - 1; 73 | if (value <= 0) { 74 | this.registeredParams.delete(param); 75 | if (this.paramsMap.has(param)) { 76 | this.paramsMap.delete(param); 77 | } 78 | } else { 79 | this.registeredParams.set(param, value); 80 | } 81 | } 82 | } 83 | 84 | clear() { 85 | this.paramsMap.clear(); 86 | this.registeredParams.clear(); 87 | } 88 | } 89 | 90 | export const decodedParamCache = new DecodedParamCache(); 91 | -------------------------------------------------------------------------------- /packages/use-query-params/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'serialize-query-params'; 2 | export * from './types'; 3 | 4 | export { useQueryParam } from './useQueryParam'; 5 | export { useQueryParams } from './useQueryParams'; 6 | export { withQueryParams, withQueryParamsMapped } from './withQueryParams'; 7 | export type { InjectedQueryProps } from './withQueryParams'; 8 | 9 | export { QueryParams } from './QueryParams'; 10 | export type { QueryParamsProps, QueryRenderProps } from './QueryParams'; 11 | export { QueryParamProvider } from './QueryParamProvider'; 12 | export type { 13 | QueryParamOptions, 14 | QueryParamOptionsWithRequired, 15 | } from './options'; 16 | -------------------------------------------------------------------------------- /packages/use-query-params/src/inheritedParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryParamConfig, 3 | QueryParamConfigMap, 4 | StringParam, 5 | } from 'serialize-query-params'; 6 | import { QueryParamOptions } from './options'; 7 | import { QueryParamConfigMapWithInherit } from './types'; 8 | 9 | /** 10 | * Convert inherit strings from a query param config to actual 11 | * parameters based on predefined ('inherited') mappings. 12 | * Defaults to StringParam. 13 | */ 14 | export function convertInheritedParamStringsToParams( 15 | paramConfigMapWithInherit: QueryParamConfigMapWithInherit, 16 | options: QueryParamOptions 17 | ): QueryParamConfigMap { 18 | const paramConfigMap: QueryParamConfigMap = {}; 19 | let hasInherit = false; 20 | 21 | const hookKeys = Object.keys(paramConfigMapWithInherit); 22 | let paramKeys = hookKeys; 23 | 24 | // include known params if asked for explicitly, or no params were configured and we didn't 25 | // explicitly say not to 26 | const includeKnownParams = 27 | options.includeKnownParams || 28 | (options.includeKnownParams !== false && hookKeys.length === 0); 29 | 30 | if (includeKnownParams) { 31 | const knownKeys = Object.keys(options.params ?? {}); 32 | paramKeys.push(...knownKeys); 33 | } 34 | 35 | for (const key of paramKeys) { 36 | const param = paramConfigMapWithInherit[key]; 37 | // does it have an existing parameter definition? use it 38 | if (param != null && typeof param === 'object') { 39 | paramConfigMap[key] = param; 40 | continue; 41 | } 42 | 43 | // otherwise, we have to inherit or use the default 44 | hasInherit = true; 45 | 46 | // default is StringParam 47 | paramConfigMap[key] = options.params?.[key] ?? StringParam; 48 | } 49 | 50 | // if we didn't inherit anything, just return the input 51 | if (!hasInherit) return paramConfigMapWithInherit as QueryParamConfigMap; 52 | 53 | return paramConfigMap; 54 | } 55 | 56 | /** 57 | * Extends a config to include params for all specified keys, 58 | * defaulting to StringParam if not found in the inheritedParams 59 | * map. 60 | */ 61 | export function extendParamConfigForKeys( 62 | baseParamConfigMap: QueryParamConfigMap, 63 | paramKeys: string[], 64 | inheritedParams?: QueryParamOptions['params'] | undefined, 65 | defaultParam?: QueryParamConfig | undefined 66 | ) { 67 | // if we aren't inheriting anything or there are no params, return the input 68 | if (!inheritedParams || !paramKeys.length) return baseParamConfigMap; 69 | 70 | let paramConfigMap = { ...baseParamConfigMap }; 71 | let hasInherit = false; 72 | for (const paramKey of paramKeys) { 73 | // if it is missing a parameter, fill it in 74 | if (!Object.prototype.hasOwnProperty.call(paramConfigMap, paramKey)) { 75 | paramConfigMap[paramKey] = inheritedParams[paramKey] ?? defaultParam; 76 | hasInherit = true; 77 | } 78 | } 79 | 80 | if (!hasInherit) return baseParamConfigMap; 81 | return paramConfigMap; 82 | } 83 | -------------------------------------------------------------------------------- /packages/use-query-params/src/latestValues.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DecodedValueMap, 3 | EncodedQuery, 4 | QueryParamConfigMap, 5 | } from 'serialize-query-params'; 6 | import { DecodedParamCache } from './decodedParamCache'; 7 | import shallowEqual from './shallowEqual'; 8 | 9 | /** 10 | * Helper to get the latest decoded values with smart caching. 11 | * Abstracted into its own function to allow re-use in a functional setter (#26) 12 | */ 13 | export function getLatestDecodedValues( 14 | parsedParams: EncodedQuery, 15 | paramConfigMap: QPCMap, 16 | decodedParamCache: DecodedParamCache 17 | ) { 18 | const decodedValues: Partial> = {}; 19 | 20 | // we have new encoded values, so let's get new decoded values. 21 | // recompute new values but only for those that changed 22 | const paramNames = Object.keys(paramConfigMap); 23 | for (const paramName of paramNames) { 24 | // do we have a new encoded value? 25 | const paramConfig = paramConfigMap[paramName]; 26 | const encodedValue = parsedParams[paramName]; 27 | 28 | // if we have a new encoded value, re-decode. otherwise reuse cache 29 | let decodedValue; 30 | if (decodedParamCache.has(paramName, encodedValue, paramConfig.decode)) { 31 | decodedValue = decodedParamCache.get(paramName); 32 | } else { 33 | decodedValue = paramConfig.decode(encodedValue); 34 | 35 | // check if we had a cached value for this encoded value but a different encoder 36 | // (sometimes people inline decode functions, e.g. withDefault...) 37 | // AND we had a different equals check than === 38 | if ( 39 | paramConfig.equals && 40 | decodedParamCache.has(paramName, encodedValue) 41 | ) { 42 | const oldDecodedValue = decodedParamCache.get(paramName); 43 | if (paramConfig.equals(decodedValue, oldDecodedValue)) { 44 | decodedValue = oldDecodedValue; 45 | } 46 | } 47 | 48 | // do not cache undefined values 49 | if (decodedValue !== undefined) { 50 | decodedParamCache.set( 51 | paramName, 52 | encodedValue, 53 | decodedValue, 54 | paramConfig.decode 55 | ); 56 | } 57 | } 58 | 59 | // in case the decode function didn't interpret `default` for some reason, 60 | // we can interpret it here as a backup 61 | if (decodedValue === undefined && paramConfig.default !== undefined) { 62 | decodedValue = paramConfig.default; 63 | } 64 | 65 | decodedValues[paramName as keyof QPCMap] = decodedValue; 66 | } 67 | 68 | return decodedValues as DecodedValueMap; 69 | } 70 | 71 | /** 72 | * Wrap get latest so we use the same exact object if the current 73 | * values are shallow equal to the previous. 74 | */ 75 | export function makeStableGetLatestDecodedValues() { 76 | let prevDecodedValues: DecodedValueMap | undefined; 77 | 78 | function stableGetLatest( 79 | parsedParams: EncodedQuery, 80 | paramConfigMap: QPCMap, 81 | decodedParamCache: DecodedParamCache 82 | ) { 83 | const decodedValues = getLatestDecodedValues( 84 | parsedParams, 85 | paramConfigMap, 86 | decodedParamCache 87 | ); 88 | if ( 89 | prevDecodedValues != null && 90 | shallowEqual(prevDecodedValues, decodedValues) 91 | ) { 92 | return prevDecodedValues; 93 | } 94 | prevDecodedValues = decodedValues; 95 | return decodedValues; 96 | } 97 | 98 | return stableGetLatest; 99 | } 100 | -------------------------------------------------------------------------------- /packages/use-query-params/src/memoSearchStringToObject.ts: -------------------------------------------------------------------------------- 1 | import { EncodedQuery } from 'serialize-query-params'; 2 | import shallowEqual from './shallowEqual'; 3 | import { deserializeUrlNameMap } from './urlName'; 4 | 5 | let cachedSearchString: string | undefined; 6 | let cachedUrlNameMapString: string | undefined; 7 | let cachedSearchStringToObjectFn: 8 | | ((searchString: string) => EncodedQuery) 9 | | undefined; 10 | let cachedParsedQuery: EncodedQuery = {}; 11 | 12 | /** 13 | * cached conversion of ?foo=1&bar=2 to { foo: '1', bar: '2' } 14 | */ 15 | export const memoSearchStringToObject = ( 16 | searchStringToObject: (searchString: string) => EncodedQuery, 17 | searchString?: string | undefined, 18 | /** optionally provide a mapping string to handle renames via `urlName` 19 | * mapping are separated by \n and mappings are urlName\0paramName 20 | */ 21 | urlNameMapStr?: string | undefined 22 | ) => { 23 | // if we have a cached version, just return it 24 | if ( 25 | cachedSearchString === searchString && 26 | cachedSearchStringToObjectFn === searchStringToObject && 27 | cachedUrlNameMapString === urlNameMapStr 28 | ) { 29 | return cachedParsedQuery; 30 | } 31 | 32 | cachedSearchString = searchString; 33 | cachedSearchStringToObjectFn = searchStringToObject; 34 | const newParsedQuery = searchStringToObject(searchString ?? ''); 35 | cachedUrlNameMapString = urlNameMapStr; 36 | 37 | const urlNameMap = deserializeUrlNameMap(urlNameMapStr); 38 | 39 | // keep old values for keys if they are the same 40 | for (let [key, value] of Object.entries(newParsedQuery)) { 41 | // handle url name mapping 42 | if (urlNameMap?.[key]) { 43 | delete newParsedQuery[key]; 44 | key = urlNameMap[key]; 45 | newParsedQuery[key] = value; 46 | } 47 | 48 | const oldValue = cachedParsedQuery[key]; 49 | if (shallowEqual(value, oldValue)) { 50 | newParsedQuery[key] = oldValue; 51 | } 52 | } 53 | 54 | cachedParsedQuery = newParsedQuery; 55 | return newParsedQuery; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/use-query-params/src/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EncodedQuery, 3 | QueryParamConfigMap, 4 | searchStringToObject, 5 | objectToSearchString, 6 | } from 'serialize-query-params'; 7 | import { UrlUpdateType } from './types'; 8 | 9 | export const defaultOptions: QueryParamOptionsWithRequired = { 10 | searchStringToObject: searchStringToObject, 11 | objectToSearchString: objectToSearchString, 12 | updateType: 'pushIn', 13 | includeKnownParams: undefined, 14 | includeAllParams: false, 15 | removeDefaultsFromUrl: false, 16 | enableBatching: false, 17 | skipUpdateWhenNoChange: true, 18 | }; 19 | 20 | export interface QueryParamOptions { 21 | searchStringToObject?: (searchString: string) => EncodedQuery; 22 | objectToSearchString?: (encodedParams: EncodedQuery) => string; 23 | updateType?: UrlUpdateType; 24 | includeKnownParams?: boolean; 25 | includeAllParams?: boolean; 26 | /** whether sets that result in no change to the location search string should be ignored (default: true) */ 27 | skipUpdateWhenNoChange?: boolean; 28 | params?: QueryParamConfigMap; 29 | 30 | /** when a value equals its default, do not encode it in the URL when updating */ 31 | removeDefaultsFromUrl?: boolean; 32 | 33 | /** 34 | * @experimental this is an experimental option to combine multiple `set` calls 35 | * into a single URL update. 36 | */ 37 | enableBatching?: boolean; 38 | } 39 | 40 | type RequiredOptions = 'searchStringToObject' | 'objectToSearchString'; 41 | export type QueryParamOptionsWithRequired = Required< 42 | Pick 43 | > & 44 | Omit; 45 | 46 | export function mergeOptions( 47 | parentOptions: QueryParamOptionsWithRequired, 48 | currOptions: QueryParamOptions | null | undefined 49 | ): QueryParamOptionsWithRequired { 50 | if (currOptions == null) { 51 | currOptions = {}; 52 | } 53 | 54 | const merged = { ...parentOptions, ...currOptions }; 55 | 56 | // deep merge param objects 57 | if (currOptions.params && parentOptions.params) { 58 | merged.params = { ...parentOptions.params, ...currOptions.params }; 59 | } 60 | 61 | return merged; 62 | } 63 | -------------------------------------------------------------------------------- /packages/use-query-params/src/removeDefaults.ts: -------------------------------------------------------------------------------- 1 | import { EncodedValueMap, QueryParamConfigMap } from 'serialize-query-params'; 2 | 3 | /** 4 | * Note: This function is destructive - it mutates encodedValues. 5 | * Remove values that match the encoded defaults from the encodedValues object 6 | */ 7 | export function removeDefaults( 8 | encodedValues: Partial>, 9 | paramConfigMap: QueryParamConfigMap 10 | ) { 11 | for (const paramName in encodedValues) { 12 | // does it have a configured default and does it have a non-undefined value? 13 | if ( 14 | paramConfigMap[paramName]?.default !== undefined && 15 | encodedValues[paramName] !== undefined 16 | ) { 17 | // does its current value match the encoded default 18 | const encodedDefault = paramConfigMap[paramName].encode( 19 | paramConfigMap[paramName].default 20 | ); 21 | if (encodedDefault === encodedValues[paramName]) { 22 | encodedValues[paramName] = undefined; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/use-query-params/src/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license at 5 | * https://github.com/facebook/fbjs/blob/master/LICENSE 6 | */ 7 | 8 | /*eslint-disable no-self-compare */ 9 | 10 | const hasOwnProperty = Object.prototype.hasOwnProperty; 11 | 12 | /** 13 | * inlined Object.is polyfill to avoid requiring consumers ship their own 14 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 15 | */ 16 | function is(x: any, y: any): boolean { 17 | // SameValue algorithm 18 | if (x === y) { 19 | // Steps 1-5, 7-10 20 | // Steps 6.b-6.e: +0 != -0 21 | // Added the nonzero y check to make Flow happy, but it is redundant 22 | return x !== 0 || y !== 0 || 1 / x === 1 / y; 23 | } else { 24 | // Step 6.a: NaN == NaN 25 | return x !== x && y !== y; 26 | } 27 | } 28 | 29 | /** 30 | * Performs equality by iterating through keys on an object and returning false 31 | * when any key has values which are not strictly equal between the arguments. 32 | * Returns true when the values of all keys are strictly equal. 33 | 34 | * @pbeshai modification of shallowEqual to take into consideration a map providing 35 | * equals functions 36 | */ 37 | export default function shallowEqual( 38 | objA: any, 39 | objB: any, 40 | equalMap?: any 41 | ): boolean { 42 | if (is(objA, objB)) { 43 | return true; 44 | } 45 | 46 | if ( 47 | typeof objA !== 'object' || 48 | objA === null || 49 | typeof objB !== 'object' || 50 | objB === null 51 | ) { 52 | return false; 53 | } 54 | 55 | const keysA = Object.keys(objA); 56 | const keysB = Object.keys(objB); 57 | 58 | if (keysA.length !== keysB.length) { 59 | return false; 60 | } 61 | 62 | // Test for A's keys different from B. 63 | for (let i = 0; i < keysA.length; i++) { 64 | const isEqual = equalMap?.[keysA[i]]?.equals ?? is; 65 | if ( 66 | !hasOwnProperty.call(objB, keysA[i]) || 67 | !isEqual(objA[keysA[i]], objB[keysA[i]]) 68 | ) { 69 | return false; 70 | } 71 | } 72 | 73 | return true; 74 | } 75 | -------------------------------------------------------------------------------- /packages/use-query-params/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryParamConfigMap, 3 | DecodedValueMap, 4 | QueryParamConfig, 5 | } from 'serialize-query-params'; 6 | 7 | /** 8 | * Different methods for updating the URL: 9 | * 10 | * - replaceIn: Replace just a single parameter, leaving the rest as is 11 | * - replace: Replace all parameters with just those specified 12 | * - pushIn: Push just a single parameter, leaving the rest as is (back button works) 13 | * - push: Push all parameters with just those specified (back button works) 14 | */ 15 | export type UrlUpdateType = 'replace' | 'replaceIn' | 'push' | 'pushIn'; 16 | 17 | /** 18 | * The setter function signature mapping 19 | */ 20 | export type SetQuery = ( 21 | changes: 22 | | Partial> 23 | | (( 24 | latestValues: DecodedValueMap 25 | ) => Partial>), 26 | updateType?: UrlUpdateType 27 | ) => void; 28 | 29 | export interface PartialLocation { 30 | readonly search: string; 31 | readonly state?: any; 32 | } 33 | 34 | export interface QueryParamAdapter { 35 | location: PartialLocation; 36 | replace: (location: PartialLocation) => void; 37 | push: (location: PartialLocation) => void; 38 | } 39 | // for backwards compat 40 | export type PushReplaceHistory = QueryParamAdapter; 41 | 42 | export type QueryParamAdapterComponent = ({ 43 | children, 44 | }: { 45 | children: (adapter: QueryParamAdapter) => React.ReactElement | null; 46 | }) => React.ReactElement | null; 47 | 48 | export type QueryParamConfigMapWithInherit = Record< 49 | string, 50 | QueryParamConfig | string 51 | >; 52 | -------------------------------------------------------------------------------- /packages/use-query-params/src/urlName.ts: -------------------------------------------------------------------------------- 1 | import { EncodedValueMap, QueryParamConfigMap } from 'serialize-query-params'; 2 | 3 | /** 4 | * Create an alias mapping using the optional `urlName` property on params 5 | */ 6 | export function serializeUrlNameMap( 7 | paramConfigMap: QueryParamConfigMap 8 | ): string | undefined { 9 | let urlNameMapParts: string[] | undefined; 10 | for (const paramName in paramConfigMap) { 11 | if (paramConfigMap[paramName].urlName) { 12 | const urlName = paramConfigMap[paramName].urlName; 13 | const part = `${urlName}\0${paramName}`; 14 | if (!urlNameMapParts) urlNameMapParts = [part]; 15 | else urlNameMapParts.push(part); 16 | } 17 | } 18 | 19 | return urlNameMapParts ? urlNameMapParts.join('\n') : undefined; 20 | } 21 | 22 | /** 23 | * Converts the stringified alias/urlName map back into an object 24 | */ 25 | export function deserializeUrlNameMap( 26 | urlNameMapStr: string | undefined 27 | ): Record | undefined { 28 | if (!urlNameMapStr) return undefined; 29 | 30 | return Object.fromEntries( 31 | urlNameMapStr.split('\n').map((part) => part.split('\0')) 32 | ); 33 | } 34 | 35 | /** 36 | * converts { searchString: 'foo'} to { q: 'foo'} if the searchString 37 | * is configured to have "q" as its urlName. 38 | */ 39 | export function applyUrlNames( 40 | encodedValues: Partial>, 41 | paramConfigMap: QueryParamConfigMap 42 | ) { 43 | let newEncodedValues: Partial> = {}; 44 | for (const paramName in encodedValues) { 45 | if (paramConfigMap[paramName]?.urlName != null) { 46 | newEncodedValues[paramConfigMap[paramName].urlName!] = 47 | encodedValues[paramName]; 48 | } else { 49 | newEncodedValues[paramName] = encodedValues[paramName]; 50 | } 51 | } 52 | 53 | return newEncodedValues; 54 | } 55 | -------------------------------------------------------------------------------- /packages/use-query-params/src/useQueryParam.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import { QueryParamConfig } from 'serialize-query-params'; 3 | import { QueryParamOptions } from './options'; 4 | import { UrlUpdateType } from './types'; 5 | import useQueryParams from './useQueryParams'; 6 | 7 | type NewValueType = D | ((latestValue: D) => D); 8 | 9 | /** 10 | * Given a query param name and query parameter configuration ({ encode, decode }) 11 | * return the decoded value and a setter for updating it. 12 | * 13 | * The setter takes two arguments (newValue, updateType) where updateType 14 | * is one of 'replace' | 'replaceIn' | 'push' | 'pushIn', defaulting to 15 | * 'pushIn'. 16 | */ 17 | export const useQueryParam = ( 18 | name: string, 19 | paramConfig?: QueryParamConfig, 20 | options?: QueryParamOptions 21 | ): [ 22 | TypeFromDecode, 23 | (newValue: NewValueType, updateType?: UrlUpdateType) => void 24 | ] => { 25 | const paramConfigMap = useMemo( 26 | () => ({ [name]: paramConfig ?? 'inherit' }), 27 | [name, paramConfig] 28 | ); 29 | const [query, setQuery] = useQueryParams(paramConfigMap, options); 30 | const decodedValue = query[name]; 31 | const setValue = useCallback( 32 | (newValue: NewValueType, updateType?: UrlUpdateType) => { 33 | if (typeof newValue === 'function') { 34 | return setQuery((latestValues) => { 35 | const newValueFromLatest = (newValue as Function)(latestValues[name]); 36 | return { [name]: newValueFromLatest }; 37 | }, updateType); 38 | } 39 | return setQuery({ [name]: newValue } as any, updateType); 40 | }, 41 | [name, setQuery] 42 | ); 43 | 44 | return [decodedValue, setValue]; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/use-query-params/src/withQueryParams.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { QueryParamConfigMap, DecodedValueMap } from 'serialize-query-params'; 3 | import useQueryParams from './useQueryParams'; 4 | import { SetQuery } from './types'; 5 | 6 | type Omit = Pick>; 7 | type Diff = Omit; 8 | 9 | export interface InjectedQueryProps { 10 | query: DecodedValueMap; 11 | setQuery: SetQuery; 12 | } 13 | 14 | /** 15 | * HOC to provide query parameters via props `query` and `setQuery` 16 | * NOTE: I couldn't get type to automatically infer generic when 17 | * using the format withQueryParams(config)(component), so I switched 18 | * to withQueryParams(config, component). 19 | * See: https://github.com/microsoft/TypeScript/issues/30134 20 | */ 21 | export function withQueryParams< 22 | QPCMap extends QueryParamConfigMap, 23 | P extends InjectedQueryProps 24 | >(paramConfigMap: QPCMap, WrappedComponent: React.ComponentType

) { 25 | // return a FC that takes props excluding query and setQuery 26 | const Component: React.FC>> = (props) => { 27 | const [query, setQuery] = useQueryParams(paramConfigMap); 28 | 29 | // see https://github.com/microsoft/TypeScript/issues/28938#issuecomment-450636046 for why `...props as P` 30 | return ( 31 | 32 | ); 33 | }; 34 | Component.displayName = `withQueryParams(${ 35 | WrappedComponent.displayName || WrappedComponent.name || 'Component' 36 | })`; 37 | 38 | return Component; 39 | } 40 | 41 | export default withQueryParams; 42 | 43 | /** 44 | * HOC to provide query parameters via props mapToProps (similar to 45 | * react-redux connect style mapStateToProps) 46 | * NOTE: I couldn't get type to automatically infer generic when 47 | * using the format withQueryParams(config)(component), so I switched 48 | * to withQueryParams(config, component). 49 | * See: https://github.com/microsoft/TypeScript/issues/30134 50 | */ 51 | export function withQueryParamsMapped< 52 | QPCMap extends QueryParamConfigMap, 53 | MappedProps extends object, 54 | P extends MappedProps 55 | >( 56 | paramConfigMap: QPCMap, 57 | mapToProps: ( 58 | query: DecodedValueMap, 59 | setQuery: SetQuery, 60 | props: Diff 61 | ) => MappedProps, 62 | WrappedComponent: React.ComponentType

63 | ) { 64 | // return a FC that takes props excluding query and setQuery 65 | const Component: React.FC> = (props) => { 66 | const [query, setQuery] = useQueryParams(paramConfigMap); 67 | const propsToAdd = mapToProps(query, setQuery, props); 68 | 69 | // see https://github.com/microsoft/TypeScript/issues/28938#issuecomment-450636046 for why `...props as P` 70 | return ; 71 | }; 72 | Component.displayName = `withQueryParams(${ 73 | WrappedComponent.displayName || WrappedComponent.name || 'Component' 74 | })`; 75 | 76 | return Component; 77 | } 78 | -------------------------------------------------------------------------------- /packages/use-query-params/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "src/__tests__/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/use-query-params/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "types"], 4 | "exclude": ["**/*.stories.*"], 5 | "compilerOptions": { 6 | "types": ["vitest/globals"], 7 | "rootDir": "./src", 8 | "baseUrl": "./", 9 | "paths": { 10 | "*": ["src/*", "node_modules/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/use-query-params/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './src/__tests__/setupTests.ts', 8 | testTimeout: 5000, 9 | include: ['**/*[.-]{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "ES6", 5 | "lib": [ 6 | "dom", 7 | "esnext", 8 | "DOM.Iterable" 9 | ], 10 | "importHelpers": true, 11 | "isolatedModules": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strictPropertyInitialization": true, 18 | "noImplicitThis": true, 19 | "alwaysStrict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "moduleResolution": "node", 25 | "resolveJsonModule": true, 26 | "jsx": "react", 27 | "esModuleInterop": true, 28 | "skipLibCheck": true, 29 | "declaration": true, 30 | "baseUrl": "./", 31 | "typeRoots": [ 32 | "./node_modules/@types", 33 | "./@types" 34 | ], 35 | "paths": { 36 | "use-query-params": [ 37 | "packages/use-query-params/src" 38 | ], 39 | "serialize-query-params": [ 40 | "packages/serialize-query-params/src" 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './packages/use-query-params/src/__tests__/setupTests.ts', 8 | testTimeout: 5000, 9 | include: ['**/*[.-]{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------