Skip to content

Commit

Permalink
docs: polish examples
Browse files Browse the repository at this point in the history
  • Loading branch information
asmyshlyaev177 committed Aug 11, 2024
1 parent c5e3db5 commit 081f122
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 91 deletions.
66 changes: 25 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ Add a ⭐️ to support the project!

# Features

- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
- 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts, API similar to React.useState
- 📘 **Typescript support and type Safety**: Preserves data types and structure, good developer experience with IDE suggestions, strong typing and JSDoc comments
- ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
-**Well tested**: Unit tests and Playwright tests
-**Well tested**: Unit tests and Playwright tests, high quality and support
-**Fast**: Minimal rerenders
- 🪶 **Lightweight**: Zero dependencies for a smaller footprint

Expand Down Expand Up @@ -103,11 +103,11 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
1. Define state shape

```typescript
// countState.ts
// userState.ts
// State shape should be stored in a constant, don't pass an object directly
export const countState: CountState = { count: 0 }
export const userState: UserState = { name: '', age: 0 }

type CountState = { count: number }
type UserState = { name: string, age: number }
```
2. Import it and use
Expand All @@ -116,39 +116,37 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
'use client'
import { useUrlState } from 'state-in-url/next';

import { countState } from './countState';
import { userState } from './userState';

function MyComponent() {
// for use searchParams from server component
// e.g. export default async function Home({ searchParams }: { searchParams: object }) {
// const { state, updateState, updateUrl } = useUrlState({ defaultState: countState, searchParams });
// can pass `replace` arg, it's control will `updateUrl` will use `rounter.push` or `router.replace`, default replace=true
// const { state, updateState, updateUrl } = useUrlState({ defaultState: countState, searchParams, replace: false });
const { state, updateState, updateUrl } = useUrlState({ defaultState: countState });
// const { state, updateState, updateUrl } = useUrlState({ defaultState: userState, searchParams, replace: false });
const { state, updateState, updateUrl } = useUrlState({ defaultState: userState });

// won't let you to accidently mutate state directly, requires TS
// state.count = 2 // <- error
// state.name = 'John' // <- error

return (
<div>
<p>Count: {state.count}</p>

<button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
Increment (Update URL)
</button>

// same api as React.useState
<button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
Increment (Local Only)
</button>
<button onClick={() => updateUrl()}>
Sync changes to url
// Or don't sync it and just share state
</button>
<input value={state.name}
onChange={(ev) => { updateState({ name: ev.target.value }) }}
onBlur={() => updateUrl()}
/>
<input value={state.age}
onChange={(ev) => { updateState({ age: +ev.target.value }) }}
onBlur={() => updateUrl()}
/>

// same api as React.useState
<input value={state.name}
onChange={(ev) => { updateState(curr => ({ ...curr, name: ev.target.value })) }}
onBlur={() => updateUrl()}
/>

<button onClick={() => updateUrl(state)}>
Reset
</button>

</div>
)
}
Expand Down Expand Up @@ -187,20 +185,6 @@ function SettingsComponent() {
}));
};

// sync state to url when idle
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
// will compare state by content not by reference and fire update only for new values
updateUrl(state);
}, 500);

return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl]);

return (
<div>
<h2>User Settings</h2>
Expand Down
62 changes: 12 additions & 50 deletions packages/example-nextjs/src/app/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,10 @@ export const Form = ({
className?: string;
searchParams?: object;
}) => {
// const { state, updateState, updateUrl } = useUrlState(form, searchParams);
const { state, updateState, updateUrl } = useUrlState({
defaultState: form,
searchParams,
});
const [autoSync, setAutoSync] = React.useState(true);

// set URI when state change
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
if (!autoSync) {
return () => {};
}

clearTimeout(timer.current);
timer.current = setTimeout(() => {
updateUrl(state);
}, 1000);

return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl, autoSync]);

// OR
// const handleSync = React.useCallback(() => {
// clearTimeout(timer.current)
// if (!autoSync) return false
// timer.current = setTimeout(() => updateUrl(getState()), 1000)
// }, [updateUrl, getState, timer, autoSync])

const onChangeAge = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -65,39 +39,40 @@ export const Form = ({

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

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

const doSync = React.useCallback(() => updateUrl(), [updateUrl]);

return (
<div className={className}>
<div className="flex flex-1 flex-col border border-grey rounded-md p-4 shadow-md">
<div className="font-semibold text-black mb-4">
First client component
</div>

<div
className="space-y-6 flex-col"
// onBlur={handleSync}
// onChange={handleSync}
// onClick={handleSync}
>
<div className="space-y-6 flex-col">
<Field id="name" text="Name">
<Input id="name" value={state.name} onChange={onChangeName} />
<Input
id="name"
value={state.name}
onChange={onChangeName}
onBlur={doSync}
/>
</Field>

<Field id="age" text="Age">
Expand All @@ -106,6 +81,7 @@ export const Form = ({
type="number"
value={state.age}
onChange={onChangeAge}
onBlur={doSync}
/>
</Field>

Expand All @@ -123,20 +99,6 @@ export const Form = ({
/>
</Field>

<Field
id="auto sync"
text="Auto sync"
className="flex gap-2 max-w-[150px] min-w-[150px] flex-row justify-between"
>
<Input
id="auto sync"
type="checkbox"
className="max-w-[24px]"
checked={autoSync}
onChange={(ev) => setAutoSync(ev.target.checked)}
/>
</Field>

<Field id="tags" text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
Expand Down

0 comments on commit 081f122

Please sign in to comment.