Skip to content

Commit

Permalink
feat(useurlstate): useUrlState hook for react-router@6
Browse files Browse the repository at this point in the history
  • Loading branch information
asmyshlyaev177 committed Sep 30, 2024
1 parent 0ca0fbd commit 1c7cd02
Show file tree
Hide file tree
Showing 37 changed files with 455 additions and 198 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ rollup.config.js
*.test.tsx
*.spec.ts
*.spec.ts
testUtils.ts
dist
exportsTest.ts
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Make sure that what you trying to fix really a bug, or if adding feature that is
2. Write code! Add some feature or fix bug.

3. Check that all tests passed(unit and e2e) and add tests for your code.
You can run unit tests with `npm run test:unit` and cypress tests `npm run test`
Run all tests witn `npm run test`
You can run tests separately with `npm run test:unit` and tests `npm run test:int`
Use `npm run kill` if some processes hang.

4. Update readme and example (if needed)

Expand Down
137 changes: 112 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,24 @@ Add a ⭐️ and <a href="/~https://github.com/asmyshlyaev177" target="_blank">fol
## Table of content

- [Installation](#installation)
- [`useUrlState` for Next.js](#useurlstate-hook-for-nextjs)
- [`useUrlStateBase` for other routers](#useurlstatebase-hook-for-others-routers)
- [`useSharedState` hook for React.js/Next.js](#usesharedstate-hook-for-reactjs)
- [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
- [`encodeState` and `decodeState` for pure JS usage](#encodestate-and-decodestate-helpers)
- [auto sync state with url](#auto-sync-state)
- [Low-level `encode` and `decode` functions](#encode-and-decode-helpers)
- [useUrlState hook](#useurlstate)
- - [Next.js](#useurlstate-hook-for-nextjs)
- - [React-Router](#useurlstate-hook-for-react-router)
- [Other helpers](#other-hooks-and-helpers)
- - [`useUrlStateBase` for other routers](#useurlstatebase-hook-for-others-routers)
- - [`useSharedState` hook for React.js/Next.js](#usesharedstate-hook-for-reactjs)
- - [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
- - [`encodeState` and `decodeState` for pure JS usage](#encodestate-and-decodestate-helpers)
- - [Low-level `encode` and `decode` functions](#encode-and-decode-helpers)
- [Best practices](#best-practices)
- [Gothas](#gothas)
- [Roadmap](#roadmap)
- [Contact & Support](#contact--support)
- [Changelog](#changelog)
- [License](#license)
- [Inspiration](#inspiration)
- - [Gothas](#gothas)
- [Other](#other)
- - [Roadmap](#roadmap)
- - [Contributing](#contribute-andor-run-locally)
- - [Contact & Support](#contact--support)
- - [Changelog](#changelog)
- - [License](#license)
- - [Inspiration](#inspiration)

## installation

Expand All @@ -92,12 +96,14 @@ pnpm add state-in-url
In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Bundler"`, or`"moduleResolution": "Node16"`, or `"moduleResolution": "NodeNext"`.
Possibly need to set `"module": "ES2022"`, or `"module": "ESNext"`

# useUrlState

`useUrlState` is a custom React hook for Next.js/React-Router applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.

## useUrlState hook for Next.js

[Docs](packages/urlstate/next/useUrlState#api)

`useUrlState` is a custom React hook for Next.js applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.

### Usage examples

#### Basic
Expand Down Expand Up @@ -334,6 +340,90 @@ function SettingsComponent() {
}
```

## useUrlState hook for React-Router

API is same as for Next.js version, except can pass options from [NavigateOptions](/~https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) type.

[Docs](packages/urlstate/react-router/useUrlState#api)


### Example

```typescript
export const form: Form = {
name: '',
age: undefined,
'agree to terms': false,
tags: [],
};

type Form = {
name: string;
age?: number;
'agree to terms': boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};

```

```typescript
import { useUrlState } from 'state-in-url/react-router';

import { form } from './form';

function TagsComponent() {
const { state, updateUrl } = useUrlState({ defaultState: form });

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

return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!state.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
</div>
);
}

const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];
```

[Example code](packages/example-react-router6/src/Form-for-test.tsx)

# Other hooks and helpers

## `useUrlStateBase` hook for others routers

Hooks to create your own `useUrlState` hooks with other routers, e.g. react-router or tanstack router.
Expand Down Expand Up @@ -369,7 +459,7 @@ function SettingsComponent() {

[Docs](packages/urlstate/encoder/README.md)

## Best Practices
# Best Practices

- Define your state shape as a constant
- Use TypeScript for enhanced type safety and autocomplete
Expand All @@ -384,32 +474,29 @@ function SettingsComponent() {
2. Vercel servers limit size of headers (query string and other stuff) to **14KB**, so keep your URL state under ~5000 words. <https://vercel.com/docs/errors/URL_TOO_LONG>
3. Tested with `next.js` 14/15 with app router, no plans to support pages.

## Run locally
# Other

Clone this repo, run `npm install` and
## Contribute and/or run locally

```sh
npm run dev
```
See [Contributing doc](CONTRIBUTING.md)

Go to [localhost:3000](http://localhost:3000)

## Roadmap

- [x] hook for `Next.js`
- [ ] hook for 'react-router`
- [x] hook for 'react-router`
- [ ] hook for 'remix`
- [ ] hook for store state in hash ?

## Contact & Support

- Create a [GitHub issue](/~https://github.com/asmyshlyaev177/state-in-url/issues) for bug reports, feature requests, or questions

## [Changelog](/~https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
## [Changelog](CHANGELOG.md)

## License

This project is licensed under the [MIT license](/~https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE).
This project is licensed under the [MIT license](LICENSE).

## Inspiration

Expand Down
1 change: 0 additions & 1 deletion packages/example-nextjs14/next.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const nextConfig = {
...config,
} as NextConfig;
newConfig.resolve.webpack5 = true;
// TODO: minify html
return newConfig;
},
transpilePackages: ['shared', 'shared/components', 'shared/components/Input', 'shared/components/Input.tsx', 'shared/components/Input', 'shared/components/Input.tsx'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ export default async function Layout({
children: React.ReactNode;
}) {
const sp = headers().get('searchParams') || '';
// console.log({
// runtime: process.env.NEXT_RUNTIME,
// sp,
// dec: decodeState(sp, stateShape)
// })

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ export default async function Layout({
children: React.ReactNode;
}) {
const sp = headers().get('searchParams') || '';
// console.log({
// runtime: process.env.NEXT_RUNTIME,
// sp,
// dec: decodeState(sp, stateShape)
// })

return (
<div>
Expand Down
1 change: 0 additions & 1 deletion packages/example-nextjs15/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default async function RootLayout({
<html lang="en">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
{/* TODO: /~https://github.com/vercel/next.js/discussions/53540 */}
<link rel="canonical" href={vercelUrl}></link>
<link rel="alternate" href={netifyUrl}></link>
<body className={`${roboto.className}`}>{children}</body>
Expand Down
2 changes: 1 addition & 1 deletion packages/example-react-router6/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Status } from './Status-for-test';

function App() {
return (
<div className="bg-white rounded-lg shadow-2xl p-8 max-w-4xl w-full">
<div className="bg-white rounded-lg shadow-2xl p-8 max-w-4xl w-full h-[1250px]">
<header className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-800">
<span className="font-mono">state-in-url</span> Demo
Expand Down
30 changes: 30 additions & 0 deletions packages/example-react-router6/src/FewComponents/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/react-router';

import { stateShape } from './stateShape';

export const Component = () => {
const { state, updateUrl } = useUrlState({
defaultState: stateShape,
replace: false,
});

return (
<div className="flex gap-2">
<h2>Per page select</h2>
<select
value={state.perPage}
onChange={(ev) =>
updateUrl((curr) => ({ ...curr, perPage: +ev.target.value }))
}
className="text-black"
data-testid="select"
>
<option>10</option>
<option>20</option>
<option>30</option>
</select>
</div>
);
};
13 changes: 13 additions & 0 deletions packages/example-react-router6/src/FewComponents/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { Outlet } from 'react-router-dom';

import { Component } from './Component';

export function Layout() {
return (
<div>
<Component />
<Outlet />
</div>
);
}
38 changes: 38 additions & 0 deletions packages/example-react-router6/src/FewComponents/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { Link, useLocation, useParams } from 'react-router-dom';

import { Component } from './Component';

export function Page() {
const { id } = useParams<'id'>();
const to = id === '1' ? '2' : '1';
const loc = useLocation();
const newUrl = `../${to}`;
const newUrlFull = `${newUrl}${loc.search}`;

return (
<div className="flex flex-col gap-4 h-[1250px]" data-testid="wrapper">
<h1>Page {id}</h1>

<Component />

<Link to={newUrl} className="text-lg" data-testid="link">
To Page {to}
</Link>

<Link
to={{
pathname: newUrl,
search: loc.search,
}}
className="text-lg"
data-testid="link-sp"
>
To Page {to} with QS
</Link>
<Link to={newUrlFull} data-testid="link-client">
To Page {to} with QS Client
</Link>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const stateShape = {
perPage: 10,
};
Loading

0 comments on commit 1c7cd02

Please sign in to comment.