Skip to content

Commit

Permalink
feat: useHistory option to use native window.history instead of Nex…
Browse files Browse the repository at this point in the history
…t.js router, true by default

BREAKING CHANGE: For Next.js, `useUrlState` hook will use `window.history` for navigation by
default, to opt out pass `useUrlState({ useHistory: false })`
  • Loading branch information
asmyshlyaev177 committed Nov 2, 2024
1 parent a508404 commit 6cb1a41
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 95 deletions.
54 changes: 27 additions & 27 deletions packages/example-nextjs14/src/app/Form-for-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ export const Form = ({
delay?: number;
}) => {
const sp = useSearchParams();
const { state, updateState, updateUrl } = useUrlState({
const { urlState, setState, setUrl } = useUrlState({
defaultState: form,
searchParams,
replace: sp.get('replace') === 'false' ? false : true,
});

React.useEffect(() => {
if (state?.tags?.length) {
const dateObj = state.tags.find((t) => t?.value?.time);
if (urlState?.tags?.length) {
const dateObj = urlState.tags.find((t) => t?.value?.time);
if (dateObj) {
console.assert(
dateObj?.value?.time instanceof Date,
'Date should be a Date instance!',
);
}
const isoDate = state.tags.find((t) => t?.value?.iso);
const isoDate = urlState.tags.find((t) => t?.value?.iso);
if (isoDate) {
console.assert(
typeof isoDate?.value?.iso === 'string',
Expand All @@ -39,54 +39,54 @@ export const Form = ({
}

// Just to test ts types
if (state?.tags?.length === 10) {
if (urlState?.tags?.length === 10) {
// @ts-expect-error should be readonly
state.age = 18;
urlState.age = 18;
// @ts-expect-error should be readonly
state.tags[0].value = { text: 'jjj', time: new Date() };
urlState.tags[0].value = { text: 'jjj', time: new Date() };
// @ts-expect-error should be readonly
state.tags[0].value.text = 'jjj';
updateState(state);
updateState((st) => st);
updateState((st) => ({ ...st, age: 18 }));
updateUrl(state);
updateUrl((st) => ({ ...st, age: 18 }));
urlState.tags[0].value.text = 'jjj';
setState(urlState);
setState((st) => st);
setState((st) => ({ ...st, age: 18 }));
setUrl(urlState);
setUrl((st) => ({ ...st, age: 18 }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onChangeAge = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
const val = +ev.target.value;
updateUrl({ age: val ? val : undefined });
setUrl({ age: val ? val : undefined });
},
[updateUrl],
[setUrl],
);

const onChangeNameUrl = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
updateUrl({ name: ev.target.value });
setUrl({ name: ev.target.value });
},
[updateUrl],
[setUrl],
);


const onChangeTerms = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
updateUrl({ 'agree to terms': ev.target.checked });
setUrl({ 'agree to terms': ev.target.checked });
},
[updateUrl],
[setUrl],
);

const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
updateUrl((curr) => ({
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[updateUrl],
[setUrl],
);

return (
Expand All @@ -98,14 +98,14 @@ export const Form = ({

<div className="space-y-6">
<Field id="name" text="Name">
<Input id="name" value={state.name} onChange={onChangeNameUrl} />
<Input id="name" value={urlState.name} onChange={onChangeNameUrl} />
</Field>

<Field id="age" text="Age">
<Input
id="age"
type="number"
value={state.age}
value={urlState.age}
onChange={onChangeAge}
/>
</Field>
Expand All @@ -114,7 +114,7 @@ export const Form = ({
<Input
id="agree to terms"
type="checkbox"
checked={state['agree to terms']}
checked={urlState['agree to terms']}
onChange={onChangeTerms}
/>
</Field>
Expand All @@ -123,7 +123,7 @@ export const Form = ({
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!state.tags.find((t) => t.id === tag.id)}
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
Expand All @@ -134,15 +134,15 @@ export const Form = ({

<Button
onClick={() => {
updateUrl(form);
setUrl(form);
}}
dataTestId="sync-default"
>
Reset state
</Button>
<Button
onClick={() => {
updateUrl((curr) => ({ ...curr, name: 'My Name', age: 55 }));
setUrl((curr) => ({ ...curr, name: 'My Name', age: 55 }));
}}
dataTestId="sync-object"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import { useUrlState } from 'state-in-url/next';// [!code highlight:1]
import { form } from './form';
export const ComponentA = () => {
const { urlState, setState, setUrlState } = useUrlState({ defaultState: form });// [!code highlight:1]
// \`useHistory\` force to use window.history for navigation,
// no _rsc requests https://github.com/vercel/next.js/discussions/59167
const { urlState, setState, setUrlState } = useUrlState({ defaultState: form, useHistory: true });// [!code highlight:1]
return <>
<input
Expand Down
54 changes: 27 additions & 27 deletions packages/example-nextjs15/src/app/Form-for-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ export const Form = ({
searchParams?: object;
}) => {
const sp = useSearchParams();
const { state, updateState, updateUrl } = useUrlState({
const { urlState, setState, setUrl } = useUrlState({
defaultState: form,
searchParams,
replace: sp.get('replace') === 'false' ? false : true,
});


React.useEffect(() => {
if (state?.tags?.length) {
const dateObj = state.tags.find((t) => t?.value?.time);
if (urlState?.tags?.length) {
const dateObj = urlState.tags.find((t) => t?.value?.time);
if (dateObj) {
console.assert(
dateObj?.value?.time instanceof Date,
'Date should be a Date instance!',
);
}
const isoDate = state.tags.find((t) => t?.value?.iso);
const isoDate = urlState.tags.find((t) => t?.value?.iso);
if (isoDate) {
console.assert(
typeof isoDate?.value?.iso === 'string',
Expand All @@ -39,53 +39,53 @@ export const Form = ({
}

// Just to test ts types
if (state?.tags?.length === 10) {
if (urlState?.tags?.length === 10) {
// @ts-expect-error should be readonly
state.age = 18;
urlState.age = 18;
// @ts-expect-error should be readonly
state.tags[0].value = { text: 'jjj', time: new Date() };
urlState.tags[0].value = { text: 'jjj', time: new Date() };
// @ts-expect-error should be readonly
state.tags[0].value.text = 'jjj';
updateState(state);
updateState((st) => st);
updateState((st) => ({ ...st, age: 18 }));
updateUrl(state);
updateUrl((st) => ({ ...st, age: 18 }));
urlState.tags[0].value.text = 'jjj';
setState(urlState);
setState((st) => st);
setState((st) => ({ ...st, age: 18 }));
setUrl(urlState);
setUrl((st) => ({ ...st, age: 18 }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onChangeAge = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
const val = +ev.target.value;
updateUrl({ age: val ? val : undefined });
setUrl({ age: val ? val : undefined });
},
[updateUrl],
[setUrl],
);

const onChangeNameUrl = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
updateUrl({ name: ev.target.value });
setUrl({ name: ev.target.value });
},
[updateUrl],
[setUrl],
);

const onChangeTerms = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
updateUrl({ 'agree to terms': ev.target.checked });
setUrl({ 'agree to terms': ev.target.checked });
},
[updateUrl],
[setUrl],
);

const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
updateUrl((curr) => ({
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[updateUrl],
[setUrl],
);

return (
Expand All @@ -97,14 +97,14 @@ export const Form = ({

<div className="space-y-6">
<Field id="name" text="Name">
<Input id="name" value={state.name} onChange={onChangeNameUrl} className='text-black' />
<Input id="name" value={urlState.name} onChange={onChangeNameUrl} className='text-black' />
</Field>

<Field id="age" text="Age">
<Input
id="age"
type="number"
value={state.age}
value={urlState.age}
onChange={onChangeAge}
className='text-black'
/>
Expand All @@ -114,7 +114,7 @@ export const Form = ({
<Input
id="agree to terms"
type="checkbox"
checked={state['agree to terms']}
checked={urlState['agree to terms']}
onChange={onChangeTerms}
/>
</Field>
Expand All @@ -123,7 +123,7 @@ export const Form = ({
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!state.tags.find((t) => t.id === tag.id)}
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
Expand All @@ -134,15 +134,15 @@ export const Form = ({

<Button
onClick={() => {
updateUrl(form);
setUrl(form);
}}
dataTestId="sync-default"
>
Reset state
</Button>
<Button
onClick={() => {
updateUrl((curr) => ({ ...curr, name: 'My Name', age: 55 }));
setUrl((curr) => ({ ...curr, name: 'My Name', age: 55 }));
}}
dataTestId="sync-object"
>
Expand Down
Loading

0 comments on commit 6cb1a41

Please sign in to comment.