diff --git a/README.md b/README.md
index 4db0087..78b2f07 100644
--- a/README.md
+++ b/README.md
@@ -167,9 +167,6 @@ function MyComponent() {
// can pass `searchParams` from server components
const { urlState, setUrl, setState } = useUrlState({ defaultState: userState });
- // won't let you to accidently mutate state directly, requires TS
- // urlState.name = 'John' // <- error
-
return (
=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
@@ -25005,6 +25022,187 @@
"node": ">=6"
}
},
+ "node_modules/pin-github-action": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/pin-github-action/-/pin-github-action-1.9.1.tgz",
+ "integrity": "sha512-ZwICGpJjnQKctwK+Wvq+hYT49PT2M9Psd9P+D4x9dBHuOAPP3AeNk68mvhkLtXhyahH4kjQkUMDohQ86NEvVnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/rest": "^18",
+ "commander": "^9",
+ "debug": "^4.3.4",
+ "matcher": "^4.0.0",
+ "yaml": "^2.1.3"
+ },
+ "bin": {
+ "pin-github-action": "bin.js"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/core": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
+ "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^2.4.4",
+ "@octokit/graphql": "^4.5.8",
+ "@octokit/request": "^5.6.3",
+ "@octokit/request-error": "^2.0.5",
+ "@octokit/types": "^6.0.3",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/graphql": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
+ "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^5.6.0",
+ "@octokit/types": "^6.0.3",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/openapi-types": {
+ "version": "12.11.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+ "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/plugin-paginate-rest": {
+ "version": "2.21.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz",
+ "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.40.0"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=2"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/plugin-request-log": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
+ "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "5.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz",
+ "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.39.0",
+ "deprecation": "^2.3.1"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/request": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
+ "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/rest": {
+ "version": "18.12.0",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz",
+ "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/core": "^3.5.1",
+ "@octokit/plugin-paginate-rest": "^2.16.8",
+ "@octokit/plugin-request-log": "^1.0.4",
+ "@octokit/plugin-rest-endpoint-methods": "^5.12.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/@octokit/types": {
+ "version": "6.41.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+ "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^12.11.0"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/pin-github-action/node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
diff --git a/package.json b/package.json
index 5e28e9f..23a7b72 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
"reinstall": "npm run cleanup && npm install",
"setup": "playwright install --with-deps",
"prepack": "npm run build && cp package.json dist/package.json && npm run build:demo",
+ "pin-gh-deps": "pin-github-action .github/workflows/tests.yml",
"prepare": "npx husky"
},
"wireit": {
@@ -363,6 +364,7 @@
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.9",
"only-allow": "^1.2.1",
+ "pin-github-action": "^1.9.1",
"playwright": "^1.48.2",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.6.8",
diff --git a/packages/example-nextjs14/src/app/Form-for-test.tsx b/packages/example-nextjs14/src/app/Form-for-test.tsx
index d7fdf46..c125df4 100644
--- a/packages/example-nextjs14/src/app/Form-for-test.tsx
+++ b/packages/example-nextjs14/src/app/Form-for-test.tsx
@@ -40,12 +40,6 @@ export const Form = ({
// Just to test ts types
if (urlState?.tags?.length === 10) {
- // @ts-expect-error should be readonly
- urlState.age = 18;
- // @ts-expect-error should be readonly
- urlState.tags[0].value = { text: 'jjj', time: new Date() };
- // @ts-expect-error should be readonly
- urlState.tags[0].value.text = 'jjj';
setState(urlState);
setState((st) => st);
setState((st) => ({ ...st, age: 18 }));
diff --git a/packages/example-nextjs15/src/app/Form-for-test.tsx b/packages/example-nextjs15/src/app/Form-for-test.tsx
index f332eca..e5f71e7 100644
--- a/packages/example-nextjs15/src/app/Form-for-test.tsx
+++ b/packages/example-nextjs15/src/app/Form-for-test.tsx
@@ -40,12 +40,6 @@ export const Form = ({
// Just to test ts types
if (urlState?.tags?.length === 10) {
- // @ts-expect-error should be readonly
- urlState.age = 18;
- // @ts-expect-error should be readonly
- urlState.tags[0].value = { text: 'jjj', time: new Date() };
- // @ts-expect-error should be readonly
- urlState.tags[0].value.text = 'jjj';
setState(urlState);
setState((st) => st);
setState((st) => ({ ...st, age: 18 }));
diff --git a/packages/example-react-router6/src/Form-for-test.tsx b/packages/example-react-router6/src/Form-for-test.tsx
index 14699f0..774e1bc 100644
--- a/packages/example-react-router6/src/Form-for-test.tsx
+++ b/packages/example-react-router6/src/Form-for-test.tsx
@@ -31,12 +31,6 @@ export const Form = ({ className }: { className?: string }) => {
// Just to test ts types
if (urlState?.tags?.length === 10) {
- // @ts-expect-error should be readonly
- urlState.age = 18;
- // @ts-expect-error should be readonly
- urlState.tags[0].value = { text: "jjj", time: new Date() };
- // @ts-expect-error should be readonly
- urlState.tags[0].value.text = "jjj";
setState(urlState);
setState((st) => st);
setState((st) => ({ ...st, age: 18 }));
diff --git a/packages/urlstate/next/useUrlState/README.md b/packages/urlstate/next/useUrlState/README.md
index 6aac70f..ff06376 100644
--- a/packages/urlstate/next/useUrlState/README.md
+++ b/packages/urlstate/next/useUrlState/README.md
@@ -16,7 +16,8 @@ A custom React hook that manages state and synchronizes it with URL search param
### Returns:
An object containing:
-- `urlState: object` - The current state (readonly).
+
+- `urlState: object` - The current state.
- `setState: Function` - Function to update the state without updating the URL.
- `setUrl: Function` - Function to update both the state and the URL.
diff --git a/packages/urlstate/next/useUrlState/useUrlState.ts b/packages/urlstate/next/useUrlState/useUrlState.ts
index 6bced38..66d7c79 100644
--- a/packages/urlstate/next/useUrlState/useUrlState.ts
+++ b/packages/urlstate/next/useUrlState/useUrlState.ts
@@ -5,7 +5,6 @@ import React from "react";
import { parseSPObj } from "../../parseSPObj";
import { useUrlStateBase } from "../../useUrlStateBase";
import {
- type DeepReadonly,
filterUnknownParams,
filterUnknownParamsClient,
isSSR,
@@ -115,11 +114,14 @@ export function useUrlState({
* @deprecated use `setUrl`
*/
updateUrl,
- urlState: state as DeepReadonly,
+ /**
+ * State object. Don't mutate directly, use `setState` or `setUrl`
+ */
+ urlState: state,
/**
* @deprecated use `urlState`
*/
- state: state as DeepReadonly,
+ state,
getState,
};
}
diff --git a/packages/urlstate/react-router/useUrlState/README.md b/packages/urlstate/react-router/useUrlState/README.md
index 03fdbf2..363f664 100644
--- a/packages/urlstate/react-router/useUrlState/README.md
+++ b/packages/urlstate/react-router/useUrlState/README.md
@@ -16,7 +16,8 @@ A custom React hook that manages state and synchronizes it with URL search param
### Returns:
An object containing:
-- `urlState: object` - The current state (readonly).
+
+- `urlState: object` - The current state.
- `setState: Function` - Function to update the state without updating the URL.
- `setUrl: Function` - Function to update both the state and the URL.
diff --git a/packages/urlstate/react-router/useUrlState/useUrlState.ts b/packages/urlstate/react-router/useUrlState/useUrlState.ts
index ff49925..49a07c7 100644
--- a/packages/urlstate/react-router/useUrlState/useUrlState.ts
+++ b/packages/urlstate/react-router/useUrlState/useUrlState.ts
@@ -9,7 +9,6 @@ import { parseSPObj } from "../../parseSPObj";
import { useUrlStateBase } from "../../useUrlStateBase";
import {
assignValue,
- type DeepReadonly,
filterUnknownParams,
filterUnknownParamsClient,
type JSONCompatible,
@@ -121,11 +120,14 @@ export function useUrlState({
* @deprecated use `setUrl`
*/
updateUrl,
- urlState: state as DeepReadonly,
+ /**
+ * State object. Don't mutate directly, use `setState` or `setUrl`
+ */
+ urlState: state,
/**
* @deprecated use `urlState`
*/
- state: state as DeepReadonly,
+ state,
getState,
};
}
diff --git a/packages/urlstate/useSharedState/README.md b/packages/urlstate/useSharedState/README.md
index 88f2ce5..76580bd 100644
--- a/packages/urlstate/useSharedState/README.md
+++ b/packages/urlstate/useSharedState/README.md
@@ -14,7 +14,8 @@ A custom React hook that manages shared state across components.
### Returns:
An object containing:
-- `state: T` - The current state (readonly).
+
+- `state: T` - The current state.
- `getState: () => T` - Function to get the current state.
- `setState: T | Partial | (T) => void` - Function to update the state.
@@ -42,7 +43,7 @@ Updates the shared state.
### Parameters:
-- `value: T | DeepReadonly | ((currState: T) => T)` - New state value or a function that receives the current state and returns the new state.
+- `value: T | ((currState: T) => T)` - New state value or a function that receives the current state and returns the new state.
## `getState`
diff --git a/packages/urlstate/useSharedState/useSharedState.ts b/packages/urlstate/useSharedState/useSharedState.ts
index 62bea33..89ef9d1 100644
--- a/packages/urlstate/useSharedState/useSharedState.ts
+++ b/packages/urlstate/useSharedState/useSharedState.ts
@@ -2,12 +2,7 @@ import React from "react";
import { stateMap, subscribers } from "../subscribers";
import { useInsertionEffect } from "../useInsertionEffect";
-import {
- type DeepReadonly,
- isEqual,
- isSSR,
- type JSONCompatible,
-} from "../utils";
+import { isEqual, isSSR, type JSONCompatible } from "../utils";
/**
* Custom React hook for sharing state between unrelated components.
@@ -51,7 +46,7 @@ export function useSharedState(
const setState = React.useCallback(
(
value:
- | (Partial | Partial>)
+ | Partial
| ((currState: typeof stateShape.current) => typeof stateShape.current),
): void => {
const curr = stateMap.get(stateShape.current);
diff --git a/packages/urlstate/useUrlStateBase/README.md b/packages/urlstate/useUrlStateBase/README.md
index ba973c1..ef85a31 100644
--- a/packages/urlstate/useUrlStateBase/README.md
+++ b/packages/urlstate/useUrlStateBase/README.md
@@ -15,7 +15,8 @@ A custom React hook to create custom `useUrlState` hooks.
### Returns:
An object containing:
-- `state: object` - The current state (readonly).
+
+- `state: object` - The current state.
- `getState: Function` - Function to get state.
- `updateState: Function` - Function to update the state without updating the URL.
- `updateUrl: Function` - Function to update both the state and the URL.
diff --git a/packages/urlstate/useUrlStateBase/useUrlStateBase.ts b/packages/urlstate/useUrlStateBase/useUrlStateBase.ts
index 76c9391..99b95dc 100644
--- a/packages/urlstate/useUrlStateBase/useUrlStateBase.ts
+++ b/packages/urlstate/useUrlStateBase/useUrlStateBase.ts
@@ -4,7 +4,6 @@ import { useInsertionEffect } from "../useInsertionEffect";
import { useSharedState } from "../useSharedState";
import { useUrlEncode } from "../useUrlEncode";
import {
- type DeepReadonly,
filterUnknownParamsClient,
type JSONCompatible,
type Router,
@@ -110,7 +109,7 @@ export function useUrlStateBase(
return {
updateState: setState,
updateUrl,
- state: state as DeepReadonly,
+ state,
getState,
};
}
diff --git a/packages/urlstate/utils.ts b/packages/urlstate/utils.ts
index 1e7887f..8698c1b 100644
--- a/packages/urlstate/utils.ts
+++ b/packages/urlstate/utils.ts
@@ -49,16 +49,6 @@ export type JSONCompatible = {
[prop: string]: JSON | JSON[];
};
-// Always will be some compromise between how strict checks are and readability
-export type DeepReadonly =
- T extends Map
- ? ReadonlyMap, DeepReadonly>
- : T extends Set
- ? ReadonlySet>
- : T extends object
- ? { readonly [K in keyof T]: DeepReadonly }
- : T;
-
export const getParams = (strOrSearchParams?: string | URLSearchParams) =>
new URLSearchParams(
typeof strOrSearchParams === "string"