Skip to content

Commit

Permalink
refactor(useurlstate): remove deprecated things
Browse files Browse the repository at this point in the history
Remove symbols for most of types and keep only props as object for useUrlState

BREAKING CHANGE: 1. `useUrlState` for Next.js now accept only object, eg. `useUrlState({
defaultState: {}})`
 2. urls encoded with versions prior to v2.3.0 could stop working
  • Loading branch information
asmyshlyaev177 committed Sep 30, 2024
1 parent 1c7cd02 commit 87c8c7c
Show file tree
Hide file tree
Showing 14 changed files with 53 additions and 203 deletions.
2 changes: 1 addition & 1 deletion packages/example-react-router6/src/Form-for-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const Form = ({
<Field
id="agree to terms"
text="Agree to terms"
className="text-black select-none flex gap-4"
className="text-black select-none flex gap-4 cursor-pointer"
>
<Input
id="agree to terms"
Expand Down
1 change: 0 additions & 1 deletion packages/urlstate/constants/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# constants

`SYMBOLS` contant contains symbols that will be used to determine type of encoded value.
E.g. `🗵` symbol added before booleans.
5 changes: 0 additions & 5 deletions packages/urlstate/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
export const SYMBOLS = {
date: '⏲',
undefined: '∙undefined',
// TODO: remove
string: '◖',
boolean: '🗵',
null: '∙null',
number: '∓',
};
4 changes: 2 additions & 2 deletions packages/urlstate/encodeState/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { encodeState } from 'state-in-url/encodeState';

const form = { name: '' };
const encodedState = encodeState({ name: 'test' }, form, 'someExistingParam=123');
console.log(encodedState); // Output: name=test&someExistingParam=123
console.log(encodedState); // Output: name=test&someExistingParam=123
```

## `decodeState`
Expand All @@ -45,6 +45,6 @@ The decoded object.
import { decodeState } from 'state-in-url/encodeState';

const form = { name: '', key: '' };
const decodedState = decodeState('key=value&name=Alex', form);
const decodedState = decodeState('key=value&name=Alex', form);
console.log(decodedState); // Output: { name: 'Alex', key: 'value }
```
6 changes: 3 additions & 3 deletions packages/urlstate/encodeState/encodeState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('encodeState', () => {

describe('decodeState', () => {
it('should decode a simple state object', () => {
const uriString = 'key1=%E2%97%96value1&key2=%E2%97%96value2';
const uriString = "key1=%27value1%27&key2=%27value2%27";
const expected = { key1: 'value1', key2: 'value2' };
const result = decodeState(uriString, { key1: '', key2: '' });
expect(result).toEqual(expected);
Expand All @@ -81,9 +81,9 @@ describe('decodeState', () => {

it('should decode a state object with default values', () => {
const expected = { key1: 'value1', key2: 'value2' };
const defaults = { key1: '', key2: '' };
const defaults = { key1: '1', key2: '2' };
expect(decodeState('', defaults)).toEqual(defaults);
expect(decodeState('key1=%E2%97%96value1', defaults)).toEqual({
expect(decodeState("key1=%27value1%27", defaults)).toEqual({
...expected,
key2: defaults.key2,
});
Expand Down
57 changes: 17 additions & 40 deletions packages/urlstate/encoder/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,6 @@ import { parseSPObj } from '../parseSPObj';
import { type JSONCompatible } from '../utils';

describe('encoder', () => {
it('should return payload if it already encoded', () => {
expect(encode('◖test')).toStrictEqual('◖test');
expect(encode('🗵false')).toStrictEqual('🗵false');
expect(encode('∙null')).toStrictEqual('∙null');
expect(encode('∙undefined')).toStrictEqual('∙undefined');
expect(encode('∓-3.14')).toStrictEqual('∓-3.14');
expect(encode('◖test')).toStrictEqual('◖test');
expect(encode('⏲2024-06-28T09:10:38.763Z')).toStrictEqual(
'⏲2024-06-28T09:10:38.763Z',
);
});

describe('string', () => {
it('simple', () => {
expect('').toStrictEqual(decode(encode('')));
Expand Down Expand Up @@ -113,7 +101,7 @@ describe('encoder', () => {
);
expect(
decode(
"{'num':'∓123','num2':'∓3.14','b1':'🗵true','b2':'🗵false','str':'test%20string','n':'∙null','obj1':{'obj2':{'str':'my_str','n':'∓123','n2':'∓-12.3','b':'🗵false','b1':'🗵true','dateIso':'2020-01-01T00%3A00%3A00.000Z'}},'dateIso':'2022-01-01T00%3A00%3A00.000Z'}",
"{'num':123,'num2':3.14,'b1':true,'b2':false,'str':'test string','n':null,'obj1':{'obj2':{'str':'my_str','n':123,'n2':-12.3,'b':false,'b1':true,'dateIso':'2020-01-01T00:00:00.000Z'}},'dateIso':'2022-01-01T00:00:00.000Z'}",
),
).toStrictEqual(obj);
});
Expand Down Expand Up @@ -204,47 +192,36 @@ describe('real life example', () => {
});

describe('decodePrimitive', () => {
it('null', () => {
expect(decodePrimitive('∙null')).toStrictEqual(null);
});

it('undefined', () => {
it('should encode with specia symbols', () => {
expect(decodePrimitive('∙undefined')).toStrictEqual(undefined);
});

it('boolean', () => {
expect(decodePrimitive('🗵false')).toStrictEqual(false);
expect(decodePrimitive('🗵true')).toStrictEqual(true);
});

it('number', () => {
expect(decodePrimitive('∓3')).toStrictEqual(3);
expect(decodePrimitive('∓3.14')).toStrictEqual(3.14);
});

it('date', () => {
const date = new Date('2024-06-28T09:10:38.763Z');
expect((decodePrimitive(`⏲${date}`) as Date).toString()).toStrictEqual(
date.toString(),
);
});

it('string', () => {
expect(decodePrimitive('◖test%20string')).toStrictEqual('test string');
});
it('should return error for other primitive values', () => {
expect(decodePrimitive('null')).toStrictEqual(errorSym);
expect(decodePrimitive('false')).toStrictEqual(errorSym);
expect(decodePrimitive('true')).toStrictEqual(errorSym);
expect(decodePrimitive('3')).toStrictEqual(errorSym);
expect(decodePrimitive('3.14')).toStrictEqual(errorSym);
expect(decodePrimitive('test%20string')).toStrictEqual(errorSym);
})

it('invalid string', () => {
expect(decodePrimitive('')).toStrictEqual(errorSym);
expect(decodePrimitive('invalid')).toStrictEqual(errorSym);
const date = new Date('2024-06-28T09:10:38.763Z');
expect(decodePrimitive(` ⏲${date}`) as Date).toStrictEqual(errorSym);
expect(decodePrimitive(' null')).toStrictEqual(errorSym);
expect(decodePrimitive(' null')).toStrictEqual(errorSym);
expect(decodePrimitive(' ∙undefined')).toStrictEqual(errorSym);
expect(decodePrimitive(' 🗵false')).toStrictEqual(errorSym);
expect(decodePrimitive(' 🗵true')).toStrictEqual(errorSym);
expect(decodePrimitive(' 3')).toStrictEqual(errorSym);
expect(decodePrimitive(' 3.14')).toStrictEqual(errorSym);
expect(decodePrimitive(' test%20string')).toStrictEqual(errorSym);
expect(decodePrimitive(' false')).toStrictEqual(errorSym);
expect(decodePrimitive(' true')).toStrictEqual(errorSym);
expect(decodePrimitive(' 3')).toStrictEqual(errorSym);
expect(decodePrimitive(' 3.14')).toStrictEqual(errorSym);
expect(decodePrimitive(' test%20string')).toStrictEqual(errorSym);
});
});

Expand Down Expand Up @@ -288,7 +265,7 @@ describe('parseSPObj', () => {

it('should parse params to object', () => {
expect(parseSPObj({}, stateShape)).toStrictEqual(stateShape);
expect(parseSPObj({ perPage: '20' }, stateShape)).toStrictEqual({
expect(parseSPObj({ perPage: '20' }, stateShape)).toStrictEqual({
perPage: 20,
});

Expand Down
14 changes: 1 addition & 13 deletions packages/urlstate/encoder/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,6 @@ export const decodePrimitive = (str: string) => {
if (str === SYMBOLS.undefined) return undefined;
if (str?.startsWith?.(SYMBOLS.date)) return new Date(str.slice(1));

// For backward compatibility
// TODO: remove
if (str === SYMBOLS.null) return null;
if (str?.startsWith?.(SYMBOLS.number))
return Number.parseFloat(str.replace(SYMBOLS.number, ''));
if (str?.startsWith?.(SYMBOLS.boolean))
return str.includes('true') ? true : false;
if (str?.startsWith?.(SYMBOLS.string))
return decodeURIComponent(str).replace(/^/, '');

return errorSym;
};

Expand Down Expand Up @@ -96,7 +86,5 @@ export function parseJSON<T extends JSONCompatible>(
}
}

const encReg = new RegExp(
`^(${SYMBOLS.string}|${SYMBOLS.boolean}|${SYMBOLS.null}|${SYMBOLS.undefined}|${SYMBOLS.number}|${SYMBOLS.date})`,
);
const encReg = new RegExp(`^(${SYMBOLS.undefined}|${SYMBOLS.date})`);
const isEncoded = (val: unknown) => encReg.test(String(val));
101 changes: 16 additions & 85 deletions packages/urlstate/next/useUrlState/useUrlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,6 @@ import {
type JSONCompatible,
} from '../../utils';

/**
* @deprecated Pass arguments in a object `useUrlState({ defaultState: form, searchParams })`
*
* NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
*
* @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
* @param {?SearchParams<T>} [searchParams] searchParams from Next server component
*
* * Example:
* ```ts
* export const form = { name: '', age: 0 };
* const { state, updateState, updateUrl } = useUrlState(form);
* // for nextjs seerver components
* // const { state, updateState, updateUrl } = useUrlState(form, searchParams);
*
* updateState({ name: 'test' });
* // by default it's uses router.push with scroll: false
* updateUrl({ name: 'test' }, { replace: true, scroll: true });
* // similar to React.useState
* updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
* ```
*
* * Docs {@link /~https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#api}
*/
export function useUrlState<T extends JSONCompatible>(
defaultState: T,
searchParams?: object,
): {
state: DeepReadonly<T>;
updateState: (
value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T),
) => void;
updateUrl: (
value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T),
) => void;
getState: () => DeepReadonly<T>;
};
/**
* NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
*
Expand All @@ -74,63 +37,44 @@ export function useUrlState<T extends JSONCompatible>(
export function useUrlState<T extends JSONCompatible>({
defaultState,
searchParams,
...opts
}: {
defaultState: T;
searchParams?: object;
replace?: boolean;
}): {
state: DeepReadonly<T>;
updateState: (
value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T),
) => void;
updateUrl: (
value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T),
) => void;
getState: () => DeepReadonly<T>;
};

export function useUrlState<T extends JSONCompatible>(
defaultState:
| T
| { defaultState: T; searchParams?: object; replace?: boolean },
searchParams?: object,
) {
const { _defaultState, _searchParams, _replace } = getArgs(
defaultState,
searchParams,
);

scroll?: boolean;
}) {
const router = useRouter();
const {
state,
updateState,
updateUrl: updateUrlBase,
getState,
} = useUrlStateBase(_defaultState, router, ({ parse }) =>
} = useUrlStateBase(defaultState, router, ({ parse }) =>
isSSR()
? parseSPObj(
filterUnknownParams(_defaultState, _searchParams),
_defaultState,
filterUnknownParams(defaultState, searchParams),
defaultState,
)
: parse(filterUnknownParamsClient(_defaultState)),
: parse(filterUnknownParamsClient(defaultState)),
);

const updateUrl = React.useCallback(
(value?: Parameters<typeof updateUrlBase>[0], options?: Options) => {
const opts = { scroll: false, replace: _replace, ...options };
updateUrlBase(value, opts);
const _opts = { ...defaultOptions, ...opts, ...options };
updateUrlBase(value, _opts);
},
[updateUrlBase, _replace],
[updateUrlBase, opts],
);

const sp = useSearchParams();
React.useEffect(() => {
updateState(
filterUnknownParams(
_defaultState,
defaultState,
parseSPObj(
Object.fromEntries([...sp.entries()]),
_defaultState,
defaultState,
) as Partial<T>,
),
);
Expand Down Expand Up @@ -173,20 +117,7 @@ interface Options extends RouterOptions {
replace?: boolean;
}

function getArgs<T extends JSONCompatible>(
obj: T | { defaultState: T; searchParams?: object; replace?: boolean },
searchParams?: object,
) {
const isObjParam = 'defaultState' in obj;
const _defaultState = (isObjParam ? obj.defaultState : obj) as T;
const _searchParams = (isObjParam ? obj.searchParams : searchParams) as
| object
| undefined;
const _replace = (isObjParam ? (obj.replace ?? true) : false) as boolean;

return {
_defaultState,
_searchParams,
_replace,
};
}
const defaultOptions = {
replace: true,
scroll: false,
};
8 changes: 4 additions & 4 deletions packages/urlstate/parseSPObj.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { parseSPObj } from './parseSPObj';
describe('should decode server components searchParams', () => {
it('without errors', () => {
const sp = {
'agree to terms': '🗵true',
tags: "[{'id':'3','value':{'text':'TailwindCSS','time':'2024-07-07T13%3A51%3A18.069Z'}}]",
'agree to terms': 'true',
tags: "[{'id':'3','value':{'text':'TailwindCSS','time':'2024-07-07T13:51:18.069Z'}}]",
};
const expected = {
...form,
Expand All @@ -22,8 +22,8 @@ describe('should decode server components searchParams', () => {

it('invalid string', () => {
const sp = {
'agree to terms': '🗵true',
tags: "[{'id':'3','value':{'text':'TailwindC",
'agree to terms': 'true',
tags: "[{'id':'3','value':{'text':'TailwindC",
};
const expected = {
...form,
Expand Down
1 change: 0 additions & 1 deletion packages/urlstate/react-router/useUrlState/useUrlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export function useUrlState<T extends JSONCompatible>({

const [sp] = useSearchParams();

// TODO: measure performance
React.useEffect(() => {
updateState(
assignValue(
Expand Down
6 changes: 3 additions & 3 deletions packages/urlstate/useUrlEncode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ const { parse, stringify } = useUrlEncode(form);

// Stringify state to URL query string
const queryString = stringify({ name: 'John' }, 'someExistingParamToKeep=123');
console.log(queryString); // Output: name=John&someExistingParamToKeep=123
console.log(queryString); // Output: name='John'&someExistingParamToKeep=123

// Parse URL query string to state object
const state = parse('name=Tom');
const state = parse("name='Tom'");
console.log(state); // Output: { name: 'Tom' }
```

Expand Down Expand Up @@ -64,6 +64,6 @@ The parsed state object.

```typescript
// Parse URL query string to state object
const state = parse('name=Tom');
const state = parse("name='Tom'");
console.log(state); // Output: { name: 'Tom' }
```
Loading

0 comments on commit 87c8c7c

Please sign in to comment.