diff --git a/README.md b/README.md
index 3c769fd..361c87e 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ import {
} from 'use-query-params';
const UseQueryParamsExample = () => {
- // something like: ?x=123&q=foo&filters=a_b_c in the URL
+ // something like: ?x=123&q=foo&filters=a&filters=b&filters=c in the URL
const [query, setQuery] = useQueryParams({
x: NumberParam,
q: StringParam,
@@ -153,17 +153,20 @@ See [all param definitions here](https://github.com/pbeshai/use-query-params/blo
Note that all nully values will encode and decode as `undefined`.
+Examples in this table assume query parameter named `qp`.
+
| Param | Type | Example Decoded | Example Encoded |
| --- | --- | --- | --- |
-| StringParam | string | `'foo'` | `'foo'` |
-| NumberParam | number | `123` | `'123'` |
-| ObjectParam | { key: string } | `{ foo: 'bar', baz: 'zzz' }` | `'foo-bar_baz-zzz'` |
-| ArrayParam | string[] | `['a','b','c']` | `'a_b_c'` |
-| JsonParam | any | `{ foo: 'bar' }` | `'{"foo":"bar"}'` |
-| DateParam | Date | `Date(2019, 2, 1)` | `'2019-03-01'` |
-| BooleanParam | boolean | `true` | `'1'` |
-| NumericObjectParam | { key: number } | `{ foo: 1, bar: 2 }` | `'foo-1_bar-2'` |
-| NumericArrayParam | number[] | `[1, 2, 3]` | `'1_2_3'` |
+| StringParam | string | `'foo'` | `?qp=foo` |
+| NumberParam | number | `123` | `?qp=123` |
+| ObjectParam | { key: string } | `{ foo: 'bar', baz: 'zzz' }` | `?qp=foo-bar_baz-zzz` |
+| ArrayParam | string[] | `['a','b','c']` | `?qp=a&qp=b&qp=c` |
+| JsonParam | any | `{ foo: 'bar' }` | `?qp=%7B%22foo%22%3A%22bar%22%7D` |
+| DateParam | Date | `Date(2019, 2, 1)` | `?qp=2019-03-01` |
+| BooleanParam | boolean | `true` | `?qp=1` |
+| NumericObjectParam | { key: number } | `{ foo: 1, bar: 2 }` | `?qp=foo-1_bar-2` |
+| DelimitedArrayParam | string[] | `['a','b','c']` | `?qp=a_b_c'` |
+| DelimitedNumericArrayParam | number[] | `[1, 2, 3]` | `?qp=1_2_3'` |
**Example**
@@ -176,6 +179,26 @@ const [foo, setFoo] = useQueryParam('foo', ArrayParam);
const [query, setQuery] = useQueryParams({ foo: ArrayParam });
```
+**Example with Custom Param**
+
+You can define your own params if the ones shipped with this package don't work for you. There are included [serialization utility functions](https://github.com/pbeshai/use-query-params/blob/master/src/serialize.ts) to make this easier, but you can use whatever you like.
+
+```js
+import {
+ encodeDelimitedArray,
+ decodeDelimitedArray
+} from 'use-query-params';
+
+/** Uses a comma to delimit entries. e.g. ['a', 'b'] => qp?=a,b */
+const CommaArrayParam = {
+ encode: (array: string[] | null | undefined) =>
+ encodeDelimitedArray(array, ','),
+
+ decode: (arrayStr: string | string[] | null | undefined) =>
+ decodeDelimitedArray(arrayStr, ',')
+};
+```
+
diff --git a/examples/react-router/src/UseQueryParamExample.tsx b/examples/react-router/src/UseQueryParamExample.tsx
index f8ca1eb..71e3e8b 100644
--- a/examples/react-router/src/UseQueryParamExample.tsx
+++ b/examples/react-router/src/UseQueryParamExample.tsx
@@ -8,8 +8,10 @@ import {
const MyParam = {
encode: (val: number) => `MY_${val}`,
- decode: (str: string | undefined) =>
- str == null ? undefined : +str.split('_')[1],
+ decode: (input: string | string[] | undefined) => {
+ const str = input instanceof Array ? input[0] : input;
+ return str == null ? undefined : +str.split('_')[1];
+ },
};
const UseQueryParamExample = () => {
diff --git a/package.json b/package.json
index 5aeb569..eac4e4a 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"dev": "NODE_ENV=development tsc -w",
"test": "jest",
"test-watch": "jest --watch",
- "test-coverage": "jest --coverage"
+ "test-coverage": "jest --coverage",
+ "prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
diff --git a/src/__tests__/params-test.ts b/src/__tests__/params-test.ts
index ba9e8bb..26140c4 100644
--- a/src/__tests__/params-test.ts
+++ b/src/__tests__/params-test.ts
@@ -3,11 +3,13 @@ import {
NumberParam,
ObjectParam,
ArrayParam,
+ NumericArrayParam,
JsonParam,
DateParam,
BooleanParam,
NumericObjectParam,
- NumericArrayParam,
+ DelimitedArrayParam,
+ DelimitedNumericArrayParam,
} from '../index';
describe('params', () => {
@@ -25,8 +27,12 @@ describe('params', () => {
expect(ObjectParam.decode('foo-bar')).toEqual({ foo: 'bar' });
});
it('ArrayParam', () => {
- expect(ArrayParam.encode(['foo', 'bar'])).toBe('foo_bar');
- expect(ArrayParam.decode('foo_bar')).toEqual(['foo', 'bar']);
+ expect(ArrayParam.encode(['foo', 'bar'])).toEqual(['foo', 'bar']);
+ expect(ArrayParam.decode(['foo', 'bar'])).toEqual(['foo', 'bar']);
+ });
+ it('NumericArrayParam', () => {
+ expect(NumericArrayParam.encode([1, 2])).toEqual(['1', '2']);
+ expect(NumericArrayParam.decode(['1', '2'])).toEqual([1, 2]);
});
it('JsonParam', () => {
expect(JsonParam.encode({ foo: 'bar' })).toBe('{"foo":"bar"}');
@@ -47,9 +53,13 @@ describe('params', () => {
expect(NumericObjectParam.encode({ foo: 123 })).toBe('foo-123');
expect(NumericObjectParam.decode('foo-123')).toEqual({ foo: 123 });
});
- it('NumericArrayParam', () => {
- expect(NumericArrayParam.encode([1, 2])).toBe('1_2');
- expect(NumericArrayParam.decode('1_2')).toEqual([1, 2]);
+ it('DelimitedArrayParam', () => {
+ expect(DelimitedArrayParam.encode(['foo', 'bar'])).toBe('foo_bar');
+ expect(DelimitedArrayParam.decode('foo_bar')).toEqual(['foo', 'bar']);
+ });
+ it('DelimitedNumericArrayParam', () => {
+ expect(DelimitedNumericArrayParam.encode([1, 2])).toBe('1_2');
+ expect(DelimitedNumericArrayParam.decode('1_2')).toEqual([1, 2]);
});
});
});
diff --git a/src/__tests__/serialize-test.ts b/src/__tests__/serialize-test.ts
index 1e6fe54..209ba6f 100644
--- a/src/__tests__/serialize-test.ts
+++ b/src/__tests__/serialize-test.ts
@@ -11,12 +11,16 @@ import {
decodeJson,
encodeArray,
decodeArray,
+ encodeNumericArray,
+ decodeNumericArray,
encodeObject,
decodeObject,
encodeNumericObject,
- encodeNumericArray,
decodeNumericObject,
- decodeNumericArray,
+ encodeDelimitedNumericArray,
+ decodeDelimitedNumericArray,
+ encodeDelimitedArray,
+ decodeDelimitedArray,
} from '../index';
describe('serialize', () => {
@@ -60,6 +64,13 @@ describe('serialize', () => {
const result = decodeDate('foo-one-two');
expect(result).not.toBeDefined();
});
+
+ it('handles array of values', () => {
+ const result = decodeDate(['2019-03-01', '2019-05-01']);
+ expect(result.getFullYear()).toBe(2019);
+ expect(result.getMonth()).toBe(2);
+ expect(result.getDate()).toBe(1);
+ });
});
describe('encodeBoolean', () => {
@@ -81,6 +92,10 @@ describe('serialize', () => {
it('handles malformed input', () => {
expect(decodeBoolean('foo')).not.toBeDefined();
});
+
+ it('handles array of values', () => {
+ expect(decodeBoolean(['1', '0'])).toBe(true);
+ });
});
describe('encodeNumber', () => {
@@ -102,6 +117,10 @@ describe('serialize', () => {
it('handles malformed input', () => {
expect(decodeNumber('foo')).not.toBeDefined();
});
+
+ it('handles array of values', () => {
+ expect(decodeNumber(['1', '0'])).toBe(1);
+ });
});
describe('encodeString', () => {
@@ -118,6 +137,10 @@ describe('serialize', () => {
expect(decodeString(undefined)).not.toBeDefined();
expect(decodeString(null)).not.toBeDefined();
});
+
+ it('handles array of values', () => {
+ expect(decodeString(['foo', 'bar'])).toBe('foo');
+ });
});
describe('encodeJson', () => {
@@ -146,19 +169,23 @@ describe('serialize', () => {
it('handles malformed input', () => {
expect(decodeJson('foo')).not.toBeDefined();
});
+
+ it('handles array of values', () => {
+ expect(decodeJson(['"foo"', '"bar"'])).toBe('foo');
+ });
});
describe('encodeArray', () => {
it('produces the correct value', () => {
const input = ['a', 'b', 'c'];
- expect(encodeArray(input)).toBe('a_b_c');
+ expect(encodeArray(input)).toEqual(['a', 'b', 'c']);
expect(encodeArray(undefined)).not.toBeDefined();
});
});
describe('decodeArray', () => {
it('produces the correct value', () => {
- const output = decodeArray('a_b_c');
+ const output = decodeArray(['a', 'b', 'c']);
const expectedOutput = ['a', 'b', 'c'];
expect(output).toEqual(expectedOutput);
@@ -167,7 +194,30 @@ describe('serialize', () => {
});
it('filters empty values', () => {
- expect(decodeArray('__')).toEqual([]);
+ expect(decodeArray(['a', '', 'b'])).toEqual(['a', 'b']);
+ });
+ });
+
+ describe('encodeNumericArray', () => {
+ it('produces the correct value', () => {
+ const input = [1, 2, 3];
+ expect(encodeNumericArray(input)).toEqual(['1', '2', '3']);
+ expect(encodeNumericArray(undefined)).not.toBeDefined();
+ });
+ });
+
+ describe('decodeNumericArray', () => {
+ it('produces the correct value', () => {
+ const output = decodeNumericArray(['1', '2', '3']);
+ const expectedOutput = [1, 2, 3];
+
+ expect(output).toEqual(expectedOutput);
+ expect(decodeNumericArray(undefined)).not.toBeDefined();
+ expect(decodeNumericArray('')).not.toBeDefined();
+ });
+
+ it('filters empty and NaN values', () => {
+ expect(decodeNumericArray(['1', '', '2', 'foo', '3'])).toEqual([1, 2, 3]);
});
});
@@ -203,28 +253,66 @@ describe('serialize', () => {
grill: undefined,
});
});
+
+ it('handles array of values', () => {
+ expect(decodeObject(['foo-bar', 'jim-grill'])).toEqual({ foo: 'bar' });
+ });
});
- describe('encodeNumericArray', () => {
+ describe('encodeDelimitedArray', () => {
+ it('produces the correct value', () => {
+ const input = ['a', 'b', 'c'];
+ expect(encodeDelimitedArray(input)).toBe('a_b_c');
+ expect(encodeDelimitedArray(undefined)).not.toBeDefined();
+ });
+ });
+
+ describe('decodeDelimitedArray', () => {
+ it('produces the correct value', () => {
+ const output = decodeDelimitedArray('a_b_c');
+ const expectedOutput = ['a', 'b', 'c'];
+
+ expect(output).toEqual(expectedOutput);
+ expect(decodeDelimitedArray(undefined)).not.toBeDefined();
+ expect(decodeDelimitedArray('')).not.toBeDefined();
+ });
+
+ it('filters empty values', () => {
+ expect(decodeDelimitedArray('__')).toEqual([]);
+ });
+
+ it('handles array of values', () => {
+ expect(decodeDelimitedArray(['foo_bar', 'jim_grill'])).toEqual([
+ 'foo',
+ 'bar',
+ ]);
+ });
+ });
+
+ describe('encodeDelimitedNumericArray', () => {
it('produces the correct value', () => {
const input = [9, 4, 0];
- expect(encodeNumericArray(input)).toBe('9_4_0');
- expect(encodeNumericArray(undefined)).not.toBeDefined();
+ expect(encodeDelimitedNumericArray(input)).toBe('9_4_0');
+ expect(encodeDelimitedNumericArray(undefined)).not.toBeDefined();
});
});
- describe('decodeNumericArray', () => {
+ describe('decodeDelimitedNumericArray', () => {
it('produces the correct value', () => {
- const output = decodeNumericArray('9_4_0');
+ const output = decodeDelimitedNumericArray('9_4_0');
const expectedOutput = [9, 4, 0];
expect(output).toEqual(expectedOutput);
- expect(decodeNumericArray(undefined)).not.toBeDefined();
- expect(decodeNumericArray('')).not.toBeDefined();
+ expect(decodeDelimitedNumericArray(undefined)).not.toBeDefined();
+ expect(decodeDelimitedNumericArray('')).not.toBeDefined();
});
it('filters empty values', () => {
- expect(decodeNumericArray('__')).toEqual([]);
+ expect(decodeDelimitedNumericArray('__')).toEqual([]);
+ });
+
+ it('handles array of values', () => {
+ expect(decodeDelimitedNumericArray(['1_2', '3_4'])).toEqual([1, 2]);
});
});
@@ -262,5 +350,11 @@ describe('serialize', () => {
grill: undefined,
});
});
+
+ it('handles array of values', () => {
+ expect(decodeNumericObject(['foo-55', 'jim-100'])).toEqual({
+ foo: 55,
+ });
+ });
});
});
diff --git a/src/__tests__/useQueryParam-test.tsx b/src/__tests__/useQueryParam-test.tsx
index cf0b281..5812448 100644
--- a/src/__tests__/useQueryParam-test.tsx
+++ b/src/__tests__/useQueryParam-test.tsx
@@ -8,7 +8,7 @@ import {
calledPushQuery,
calledReplaceQuery,
} from './helpers';
-import { NumberParam, ArrayParam } from '../params';
+import { NumberParam, NumericArrayParam } from '../params';
// helper to setup tests
function setupWrapper(query: ParsedQuery) {
@@ -49,30 +49,38 @@ describe('useQueryParam', () => {
});
it("doesn't decode more than necessary", () => {
- const { wrapper, history, location } = setupWrapper({ foo: 'a_b_c' });
+ const { wrapper, history, location } = setupWrapper({
+ foo: ['1', '2', '3'],
+ });
const { result, rerender } = renderHook(
- () => useQueryParam('foo', ArrayParam),
+ () => useQueryParam('foo', NumericArrayParam),
{
wrapper,
}
);
const [decodedValue, setter] = result.current;
- expect(decodedValue).toEqual(['a', 'b', 'c']);
+ expect(decodedValue).toEqual([1, 2, 3]);
rerender();
const [decodedValue2, setter2] = result.current;
expect(decodedValue).toBe(decodedValue2);
- setter2(['d', 'e', 'f'], 'replaceIn');
+ setter2([4, 5, 6], 'replaceIn');
rerender();
const [decodedValue3, setter3] = result.current;
expect(decodedValue).not.toBe(decodedValue3);
- expect(decodedValue3).toEqual(['d', 'e', 'f']);
+ expect(decodedValue3).toEqual([4, 5, 6]);
- setter3(['d', 'e', 'f'], 'push');
+ setter3([4, 5, 6], 'push');
rerender();
const [decodedValue4, setter4] = result.current;
expect(decodedValue3).toBe(decodedValue4);
+
+ // if another parameter changes, this one shouldn't be affected
+ location.search = `${location.search}&zzz=123`;
+ rerender();
+ const [decodedValue5, setter5] = result.current;
+ expect(decodedValue5).toBe(decodedValue3);
});
});
diff --git a/src/__tests__/useQueryParams-test.tsx b/src/__tests__/useQueryParams-test.tsx
index b8524b5..0aa05b8 100644
--- a/src/__tests__/useQueryParams-test.tsx
+++ b/src/__tests__/useQueryParams-test.tsx
@@ -51,7 +51,10 @@ describe('useQueryParams', () => {
expect(decodedQuery).toEqual({ foo: 123, bar: 'xxx' });
setter({ foo: 555, baz: ['a', 'b'] }, 'push');
- expect(calledPushQuery(history, 0)).toEqual({ foo: '555', baz: 'a_b' });
+ expect(calledPushQuery(history, 0)).toEqual({
+ foo: '555',
+ baz: ['a', 'b'],
+ });
});
it('ignores unconfigured parameter', () => {
diff --git a/src/params.ts b/src/params.ts
index 3585f4b..3133109 100644
--- a/src/params.ts
+++ b/src/params.ts
@@ -49,6 +49,17 @@ export const ArrayParam: QueryParamConfig<
decode: Serialize.decodeArray,
};
+/**
+ * For flat arrays of strings, filters out undefined values during decode
+ */
+export const NumericArrayParam: QueryParamConfig<
+ number[] | null | undefined,
+ number[] | undefined
+> = {
+ encode: Serialize.encodeNumericArray,
+ decode: Serialize.decodeNumericArray,
+};
+
/**
* For any type of data, encoded via JSON.stringify
*/
@@ -94,13 +105,24 @@ export const NumericObjectParam: QueryParamConfig<
decode: Serialize.decodeNumericObject,
};
+/**
+ * For flat arrays of strings, filters out undefined values during decode
+ */
+export const DelimitedArrayParam: QueryParamConfig<
+ string[] | null | undefined,
+ string[] | undefined
+> = {
+ encode: Serialize.encodeDelimitedArray,
+ decode: Serialize.decodeDelimitedArray,
+};
+
/**
* For flat arrays where the values are numbers, filters out undefined values during decode
*/
-export const NumericArrayParam: QueryParamConfig<
+export const DelimitedNumericArrayParam: QueryParamConfig<
number[] | null | undefined,
number[] | undefined
> = {
- encode: Serialize.encodeNumericArray,
- decode: Serialize.decodeNumericArray,
+ encode: Serialize.encodeDelimitedNumericArray,
+ decode: Serialize.decodeDelimitedNumericArray,
};
diff --git a/src/serialize.ts b/src/serialize.ts
index a677fde..6cdc419 100644
--- a/src/serialize.ts
+++ b/src/serialize.ts
@@ -24,12 +24,19 @@ export function encodeDate(date: Date | null | undefined): string | undefined {
* as necessary (aka, '2015', '2015-10', '2015-10-01').
* It will not work for dates that have times included in them.
*
- * @param {String} dateString String date form like '2015-10-01'
+ * If an array is provided, only the first entry is used.
+ *
+ * @param {String} input String date form like '2015-10-01'
* @return {Date} parsed date
*/
export function decodeDate(
- dateString: string | null | undefined
+ input: string | string[] | null | undefined
): Date | undefined {
+ if (input == null || !input.length) {
+ return undefined;
+ }
+
+ const dateString = input instanceof Array ? input[0] : input;
if (dateString == null || !dateString.length) {
return undefined;
}
@@ -73,12 +80,20 @@ export function encodeBoolean(
* Decodes a boolean from a string. "1" -> true, "0" -> false.
* Everything else maps to undefined.
*
- * @param {String} boolStr the encoded boolean string
+ * If an array is provided, only the first entry is used.
+ *
+ * @param {String} input the encoded boolean string
* @return {Boolean} the boolean value
*/
export function decodeBoolean(
- boolStr: string | null | undefined
+ input: string | string[] | null | undefined
): boolean | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const boolStr = input instanceof Array ? input[0] : input;
+
if (boolStr === '1') {
return true;
} else if (boolStr === '0') {
@@ -105,15 +120,23 @@ export function encodeNumber(
}
/**
- * Decodes a number from a string via parseFloat. If the number is invalid,
+ * Decodes a number from a string. If the number is invalid,
* it returns undefined.
*
- * @param {String} numStr the encoded number string
+ * If an array is provided, only the first entry is used.
+ *
+ * @param {String} input the encoded number string
* @return {Number} the number value
*/
export function decodeNumber(
- numStr: string | null | undefined
+ input: string | string[] | null | undefined
): number | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const numStr = input instanceof Array ? input[0] : input;
+
if (numStr == null || numStr === '') {
return undefined;
}
@@ -130,11 +153,11 @@ export function decodeNumber(
/**
* Encodes a string while safely handling null and undefined values.
*
- * @param {String} string
+ * @param {String} str a string to encode
* @return {String} the encoded string
*/
export function encodeString(
- str: string | null | undefined
+ str: string | string[] | null | undefined
): string | undefined {
if (str == null) {
return undefined;
@@ -146,12 +169,20 @@ export function encodeString(
/**
* Decodes a string while safely handling null and undefined values.
*
- * @param {String} str the encoded string
+ * If an array is provided, only the first entry is used.
+ *
+ * @param {String} input the encoded string
* @return {String} the string value
*/
export function decodeString(
- str: string | null | undefined
+ input: string | string[] | null | undefined
): string | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const str = input instanceof Array ? input[0] : input;
+
if (str == null) {
return undefined;
}
@@ -176,12 +207,20 @@ export function encodeJson(any: any | null | undefined): string | undefined {
/**
* Decodes a JSON string into javascript
*
- * @param {String} jsonStr The JSON string representation
+ * If an array is provided, only the first entry is used.
+ *
+ * @param {String} input The JSON string representation
* @return {Any} The javascript representation
*/
export function decodeJson(
- jsonStr: string | null | undefined
+ input: string | string[] | null | undefined
): any | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const jsonStr = input instanceof Array ? input[0] : input;
+
if (!jsonStr) {
return undefined;
}
@@ -200,9 +239,90 @@ export function decodeJson(
* Encodes an array as a JSON string.
*
* @param {Array} array The array to be encoded
- * @return {String} The JSON string representation of array
+ * @return {String[]} The array of strings to be put in the URL
+ * as repeated query parameters
*/
export function encodeArray(
+ array: string[] | null | undefined
+): string[] | undefined {
+ if (!array) {
+ return undefined;
+ }
+
+ return array;
+}
+
+/**
+ * Decodes an array or singular value and returns it as an array
+ * or undefined if falsy. Filters out undefined values.
+ *
+ * @param {String | Array} input The input value
+ * @return {Array} The javascript representation
+ */
+export function decodeArray(
+ input: string | string[] | null | undefined
+): string[] | undefined {
+ if (!input) {
+ return undefined;
+ }
+
+ if (!(input instanceof Array)) {
+ return [input];
+ }
+
+ return input
+ .map(item => (item === '' ? undefined : item))
+ .filter(item => item !== undefined) as string[];
+}
+
+/**
+ * Encodes a numeric array as a JSON string.
+ *
+ * @param {Array} array The array to be encoded
+ * @return {String[]} The array of strings to be put in the URL
+ * as repeated query parameters
+ */
+export function encodeNumericArray(
+ array: number[] | null | undefined
+): string[] | undefined {
+ if (!array) {
+ return undefined;
+ }
+
+ return array.map(d => `${d}`);
+}
+
+/**
+ * Decodes an array or singular value and returns it as an array
+ * or undefined if falsy. Filters out undefined and NaN values.
+ *
+ * @param {String | Array} input The input value
+ * @return {Array} The javascript representation
+ */
+export function decodeNumericArray(
+ input: string | string[] | null | undefined
+): number[] | undefined {
+ const arr = decodeArray(input);
+
+ if (!arr) {
+ return undefined;
+ }
+
+ return arr
+ .map(item => +item)
+ .filter(item => item !== undefined && !isNaN(item)) as number[];
+}
+
+/**
+ * Encodes an array as a delimited string. For example,
+ * ['a', 'b'] -> 'a_b' with entrySeparator='_'
+ *
+ * @param array The array to be encoded
+ * @param entrySeparator The string used to delimit entries
+ * @return The array as a string with elements joined by the
+ * entry separator
+ */
+export function encodeDelimitedArray(
array: string[] | null | undefined,
entrySeparator = '_'
): string | undefined {
@@ -214,15 +334,26 @@ export function encodeArray(
}
/**
- * Decodes a JSON string into javascript array
+ * Decodes a delimited string into javascript array. For example,
+ * 'a_b' -> ['a', 'b'] with entrySeparator='_'
*
- * @param {String} jsonStr The JSON string representation
+ * If an array is provided as input, only the first entry is used.
+ *
+ * @param {String} input The JSON string representation
+ * @param entrySeparator The array as a string with elements joined by the
+ * entry separator
* @return {Array} The javascript representation
*/
-export function decodeArray(
- arrayStr: string | null | undefined,
+export function decodeDelimitedArray(
+ input: string | string[] | null | undefined,
entrySeparator = '_'
): string[] | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const arrayStr = input instanceof Array ? input[0] : input;
+
if (!arrayStr) {
return undefined;
}
@@ -234,27 +365,31 @@ export function decodeArray(
}
/**
- * Encodes a numeric array as a JSON string. (alias of encodeArray)
+ * Encodes a numeric array as a delimited string. (alias of encodeDelimitedArray)
+ * For example, [1, 2] -> '1_2' with entrySeparator='_'
*
* @param {Array} array The array to be encoded
* @return {String} The JSON string representation of array
*/
-export const encodeNumericArray = encodeArray as (
+export const encodeDelimitedNumericArray = encodeDelimitedArray as (
array: number[] | null | undefined,
entrySeparator?: string
) => string | undefined;
/**
- * Decodes a JSON string into javascript array where all entries are numbers
+ * Decodes a delimited string into javascript array where all entries are numbers
+ * For example, '1_2' -> [1, 2] with entrySeparator='_'
+ *
+ * If an array is provided as input, only the first entry is used.
*
* @param {String} jsonStr The JSON string representation
* @return {Array} The javascript representation
*/
-export function decodeNumericArray(
- arrayStr: string | null | undefined,
+export function decodeDelimitedNumericArray(
+ arrayStr: string | string[] | null | undefined,
entrySeparator = '_'
): number[] | undefined {
- const decoded = decodeArray(arrayStr, entrySeparator);
+ const decoded = decodeDelimitedArray(arrayStr, entrySeparator);
if (!decoded) {
return undefined;
@@ -262,12 +397,12 @@ export function decodeNumericArray(
return decoded
.map(d => (d == null ? undefined : +d))
- .filter(d => d !== undefined) as number[];
+ .filter(d => d !== undefined && !isNaN(d)) as number[];
}
/**
- * Encode simple objects as readable strings. Currently works only for simple,
- * flat objects where values are numbers, booleans or strings.
+ * Encode simple objects as readable strings. Works only for simple,
+ * flat objects where values are numbers, strings.
*
* For example { foo: bar, boo: baz } -> "foo-bar_boo-baz"
*
@@ -277,7 +412,7 @@ export function decodeNumericArray(
* @return {String} The encoded object
*/
export function encodeObject(
- obj: { [key: string]: string | undefined } | null | undefined,
+ obj: { [key: string]: string | number | undefined } | null | undefined,
keyValSeparator = '-',
entrySeparator = '_'
): string | undefined {
@@ -292,20 +427,28 @@ export function encodeObject(
/**
* Decodes a simple object to javascript. Currently works only for simple,
- * flat objects where values are numbers, booleans or strings.
+ * flat objects where values are strings.
*
* For example "foo-bar_boo-baz" -> { foo: bar, boo: baz }
*
- * @param {String} objStr The object string to decode
+ * If an array is provided as input, only the first entry is used.
+ *
+ * @param {String} input The object string to decode
* @param {String} keyValSeparator="-" The separator between keys and values
* @param {String} entrySeparator="_" The separator between entries
* @return {Object} The javascript object
*/
export function decodeObject(
- objStr: string | null | undefined,
+ input: string | string[] | null | undefined,
keyValSeparator = '-',
entrySeparator = '_'
): { [key: string]: string | undefined } | undefined {
+ if (input == null) {
+ return undefined;
+ }
+
+ const objStr = input instanceof Array ? input[0] : input;
+
if (!objStr || !objStr.length) {
return undefined;
}
@@ -341,19 +484,21 @@ export const encodeNumericObject = encodeObject as (
*
* For example "foo-123_boo-521" -> { foo: 123, boo: 521 }
*
- * @param {String} objStr The object string to decode
+ * If an array is provided as input, only the first entry is used.
+ *
+ * @param {String} input The object string to decode
* @param {String} keyValSeparator="-" The separator between keys and values
* @param {String} entrySeparator="_" The separator between entries
* @return {Object} The javascript object
*/
export function decodeNumericObject(
- objStr: string | null | undefined,
+ input: string | string[] | null | undefined,
keyValSeparator = '-',
entrySeparator = '_'
): { [key: string]: number | undefined } | undefined {
const decoded:
| { [key: string]: number | string | undefined }
- | undefined = decodeObject(objStr, keyValSeparator, entrySeparator);
+ | undefined = decodeObject(input, keyValSeparator, entrySeparator);
if (!decoded) {
return undefined;
diff --git a/src/types.ts b/src/types.ts
index 345a524..a605c91 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -28,16 +28,15 @@ export interface ExtendedLocation extends Location {
* Encoded query parameters (all strings)
*/
export interface EncodedQuery {
- [key: string]: string;
+ [key: string]: string | string[];
}
/**
* Encoded query parameters, possibly including null or undefined values
*/
export interface EncodedQueryWithNulls {
- [key: string]: string | null | undefined;
+ [key: string]: string | string[] | null | undefined;
}
-
/**
* Configuration for a query param specifying how to encode it
* (convert it to a string) and decode it (convert it from a string
@@ -48,10 +47,10 @@ export interface EncodedQueryWithNulls {
*/
export interface QueryParamConfig {
/** Convert the query param value to a string */
- encode: (value: D) => string | undefined;
+ encode: (value: D) => string | string[] | undefined;
/** Convert the query param string value to its native type */
- decode: (value: string) => D2;
+ decode: (value: string | string[]) => D2;
}
/**
diff --git a/src/useQueryParam.ts b/src/useQueryParam.ts
index 7a49afc..e098d1c 100644
--- a/src/useQueryParam.ts
+++ b/src/useQueryParam.ts
@@ -1,5 +1,9 @@
import * as React from 'react';
-import { parse as parseQueryString, ParsedQuery } from 'query-string';
+import {
+ parse as parseQueryString,
+ stringify,
+ ParsedQuery,
+} from 'query-string';
import { QueryParamContext } from './QueryParamProvider';
import { StringParam } from './params';
import { updateUrlQuery } from './updateUrlQuery';
@@ -28,18 +32,33 @@ export const useQueryParam = (
// read in the raw query
if (!rawQuery) {
- rawQuery =
- (location.query as ParsedQuery) ||
- parseQueryString(location.search) ||
- {};
+ rawQuery = React.useMemo(
+ () =>
+ (location.query as ParsedQuery) ||
+ parseQueryString(location.search) ||
+ {},
+ [location.query, location.search]
+ );
}
// read in the encoded string value
- const encodedValue = rawQuery[name] as string;
+ const encodedValue = rawQuery[name];
- // decode if the encoded value has changed, otherwise re-use memoized value
- const decodedValue = React.useMemo(() => paramConfig.decode(encodedValue), [
- encodedValue,
+ // decode if the encoded value has changed, otherwise
+ // re-use memoized value
+ const decodedValue = React.useMemo(() => {
+ if (encodedValue == null) {
+ return undefined;
+ }
+ return paramConfig.decode(encodedValue);
+
+ // note that we use the stringified encoded value since the encoded
+ // value may be an array that is recreated if a different query param
+ // changes.
+ }, [
+ encodedValue instanceof Array
+ ? stringify({ name: encodedValue })
+ : encodedValue,
]);
// create the setter, memoizing via useCallback
diff --git a/src/useQueryParams.ts b/src/useQueryParams.ts
index a4a771f..0cdde52 100644
--- a/src/useQueryParams.ts
+++ b/src/useQueryParams.ts
@@ -50,8 +50,13 @@ export const useQueryParams = (
const { history, location } = React.useContext(QueryParamContext);
// read in the raw query
- const rawQuery =
- (location.query as ParsedQuery) || parseQueryString(location.search) || {};
+ const rawQuery = React.useMemo(
+ () =>
+ (location.query as ParsedQuery) ||
+ parseQueryString(location.search) ||
+ {},
+ [location.query, location.search]
+ );
// parse each parameter via usQueryParam
const paramNames = Object.keys(paramConfigMap);