@1money/hooks 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/es/index.d.ts CHANGED
@@ -9,3 +9,5 @@ export { default as useSyncState } from './useSyncState';
9
9
  export { default as useUpdateEffect } from './useUpdateEffect';
10
10
  export { default as useLayoutState, useTimeoutLock } from './useLayoutState';
11
11
  export type { Updater } from './useLayoutState';
12
+ export { default as useQueryState, queryString, queryNumber, queryBoolean, queryJson, } from './useQueryState';
13
+ export type { Parser, Serializer, QueryStateOptions, } from './useQueryState';
package/es/index.js CHANGED
@@ -8,4 +8,5 @@ export { default as usePrevious } from "./usePrevious";
8
8
  export { default as useSafeState } from "./useSafeState";
9
9
  export { default as useSyncState } from "./useSyncState";
10
10
  export { default as useUpdateEffect } from "./useUpdateEffect";
11
- export { default as useLayoutState, useTimeoutLock } from "./useLayoutState";
11
+ export { default as useLayoutState, useTimeoutLock } from "./useLayoutState";
12
+ export { default as useQueryState, queryString, queryNumber, queryBoolean, queryJson } from "./useQueryState";
@@ -1,3 +1,4 @@
1
+ /// <reference types="react" />
1
2
  /**
2
3
  * A hook that returns a ref object whose `.current` property is always
3
4
  * updated to the latest value. Useful for accessing the latest value
@@ -0,0 +1,75 @@
1
+ /** Turn a raw query string into a typed value. */
2
+ export type Parser<T> = (value: string) => T;
3
+ /** Turn a typed value back into a query string. */
4
+ export type Serializer<T> = (value: T) => string;
5
+ export interface QueryStateOptions<T> {
6
+ /**
7
+ * Parse the raw `string` read from the URL into `T`.
8
+ * Defaults to identity (the raw string).
9
+ */
10
+ parser?: Parser<T>;
11
+ /**
12
+ * Serialize `T` back into a `string` for the URL.
13
+ * Defaults to `String(value)`.
14
+ */
15
+ serializer?: Serializer<T>;
16
+ /**
17
+ * Value returned when the param is absent from
18
+ * the URL. When set, the hook never returns
19
+ * `null`.
20
+ */
21
+ defaultValue?: T;
22
+ /**
23
+ * How the URL is mutated:
24
+ * - `'replace'` (default) — `history.replaceState`,
25
+ * does not add a history entry.
26
+ * - `'push'` — `history.pushState`, back/forward
27
+ * navigates between values.
28
+ */
29
+ history?: 'push' | 'replace';
30
+ }
31
+ type SetQueryState<T> = (value: T | null | ((prev: T | null) => T | null)) => void;
32
+ /** Identity parser used when none is supplied. */
33
+ export declare const queryString: Parser<string>;
34
+ /** Parse a numeric query param. `NaN` for non-numbers. */
35
+ export declare const queryNumber: Parser<number>;
36
+ /** Parse a boolean query param. Only `'true'` is true. */
37
+ export declare const queryBoolean: Parser<boolean>;
38
+ /** Parse a JSON-encoded query param. */
39
+ export declare const queryJson: Parser<unknown>;
40
+ /**
41
+ * Sync a piece of React state with a URL query
42
+ * parameter, so it survives refreshes and is
43
+ * shareable via the link.
44
+ *
45
+ * Reading is reactive via Next's `useSearchParams`;
46
+ * writing goes through the native
47
+ * `history.pushState/replaceState`, which the Next.js
48
+ * App Router intercepts and syncs back into
49
+ * `useSearchParams` *without* a server round-trip (no
50
+ * route re-render, no scroll-to-top). Because the
51
+ * value is reactive, using it as a SWR/React Query key
52
+ * makes dependent requests refetch automatically.
53
+ *
54
+ * Constraints (inherited from `useSearchParams`):
55
+ * - Next.js App Router only; the calling component
56
+ * must be a Client Component (`'use client'`).
57
+ * - On statically rendered routes, wrap the consumer
58
+ * in `<Suspense>`; otherwise that subtree opts into
59
+ * client-side rendering. During prerender the param
60
+ * reads as absent, so `value` falls back to
61
+ * `defaultValue ?? null`.
62
+ *
63
+ * @param key The query parameter name
64
+ * @param options Parsing, serialization and history
65
+ * behaviour
66
+ * @returns A `[value, setValue]` tuple. `setValue`
67
+ * accepts a value, an updater function, or `null`
68
+ * to remove the param.
69
+ */
70
+ declare function useQueryState(key: string): [string | null, SetQueryState<string>];
71
+ declare function useQueryState<T>(key: string, options: QueryStateOptions<T> & {
72
+ defaultValue: T;
73
+ }): [T, SetQueryState<T>];
74
+ declare function useQueryState<T>(key: string, options: QueryStateOptions<T>): [T | null, SetQueryState<T>];
75
+ export default useQueryState;
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ import { useSearchParams } from 'next/navigation';
4
+ import useMemoizedFn from "../useMemoizedFn";
5
+
6
+ /** Turn a raw query string into a typed value. */
7
+
8
+ /** Turn a typed value back into a query string. */
9
+
10
+ /** Identity parser used when none is supplied. */
11
+ export var queryString = function queryString(value) {
12
+ return value;
13
+ };
14
+
15
+ /** Parse a numeric query param. `NaN` for non-numbers. */
16
+ export var queryNumber = function queryNumber(value) {
17
+ return Number(value);
18
+ };
19
+
20
+ /** Parse a boolean query param. Only `'true'` is true. */
21
+ export var queryBoolean = function queryBoolean(value) {
22
+ return value === 'true';
23
+ };
24
+
25
+ /** Parse a JSON-encoded query param. */
26
+ export var queryJson = function queryJson(value) {
27
+ return JSON.parse(value);
28
+ };
29
+ var isBrowser = typeof window !== 'undefined';
30
+
31
+ /**
32
+ * Sync a piece of React state with a URL query
33
+ * parameter, so it survives refreshes and is
34
+ * shareable via the link.
35
+ *
36
+ * Reading is reactive via Next's `useSearchParams`;
37
+ * writing goes through the native
38
+ * `history.pushState/replaceState`, which the Next.js
39
+ * App Router intercepts and syncs back into
40
+ * `useSearchParams` *without* a server round-trip (no
41
+ * route re-render, no scroll-to-top). Because the
42
+ * value is reactive, using it as a SWR/React Query key
43
+ * makes dependent requests refetch automatically.
44
+ *
45
+ * Constraints (inherited from `useSearchParams`):
46
+ * - Next.js App Router only; the calling component
47
+ * must be a Client Component (`'use client'`).
48
+ * - On statically rendered routes, wrap the consumer
49
+ * in `<Suspense>`; otherwise that subtree opts into
50
+ * client-side rendering. During prerender the param
51
+ * reads as absent, so `value` falls back to
52
+ * `defaultValue ?? null`.
53
+ *
54
+ * @param key The query parameter name
55
+ * @param options Parsing, serialization and history
56
+ * behaviour
57
+ * @returns A `[value, setValue]` tuple. `setValue`
58
+ * accepts a value, an updater function, or `null`
59
+ * to remove the param.
60
+ */
61
+
62
+ // eslint-disable-next-line no-redeclare
63
+
64
+ // eslint-disable-next-line no-redeclare
65
+
66
+ // eslint-disable-next-line no-redeclare
67
+ function useQueryState(key) {
68
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
69
+ var parser = options.parser,
70
+ _options$serializer = options.serializer,
71
+ serializer = _options$serializer === void 0 ? String : _options$serializer,
72
+ defaultValue = options.defaultValue,
73
+ _options$history = options.history,
74
+ history = _options$history === void 0 ? 'replace' : _options$history;
75
+ var parse = function parse(raw) {
76
+ if (raw === null) {
77
+ return defaultValue !== null && defaultValue !== void 0 ? defaultValue : null;
78
+ }
79
+ return parser ? parser(raw) : raw;
80
+ };
81
+
82
+ // Reactive read. Next re-renders consumers when the
83
+ // URL changes (including our own history writes).
84
+ var searchParams = useSearchParams();
85
+ var value = parse(searchParams.get(key));
86
+ var set = useMemoizedFn(function (next) {
87
+ if (!isBrowser) {
88
+ return;
89
+ }
90
+ // Build from the live URL (not the captured
91
+ // `searchParams`) so consecutive writes in one
92
+ // tick see each other's result.
93
+ var params = new URLSearchParams(window.location.search);
94
+ var resolved = typeof next === 'function' ? next(parse(params.get(key))) : next;
95
+ if (resolved === null || resolved === undefined) {
96
+ params.delete(key);
97
+ } else {
98
+ params.set(key, serializer(resolved));
99
+ }
100
+ var search = params.toString();
101
+ var url = window.location.pathname + (search ? "?".concat(search) : '') + window.location.hash;
102
+ if (history === 'push') {
103
+ window.history.pushState(null, '', url);
104
+ } else {
105
+ window.history.replaceState(null, '', url);
106
+ }
107
+ });
108
+ return [value, set];
109
+ }
110
+ export default useQueryState;
package/lib/index.d.ts CHANGED
@@ -9,3 +9,5 @@ export { default as useSyncState } from './useSyncState';
9
9
  export { default as useUpdateEffect } from './useUpdateEffect';
10
10
  export { default as useLayoutState, useTimeoutLock } from './useLayoutState';
11
11
  export type { Updater } from './useLayoutState';
12
+ export { default as useQueryState, queryString, queryNumber, queryBoolean, queryJson, } from './useQueryState';
13
+ export type { Parser, Serializer, QueryStateOptions, } from './useQueryState';
package/lib/index.js CHANGED
@@ -29,6 +29,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.ts
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ queryBoolean: () => import_useQueryState.queryBoolean,
33
+ queryJson: () => import_useQueryState.queryJson,
34
+ queryNumber: () => import_useQueryState.queryNumber,
35
+ queryString: () => import_useQueryState.queryString,
32
36
  useControlledState: () => import_useControlledState.default,
33
37
  useEventCallback: () => import_useEventCallback.default,
34
38
  useLatest: () => import_useLatest.default,
@@ -36,6 +40,7 @@ __export(index_exports, {
36
40
  useLayoutState: () => import_useLayoutState.default,
37
41
  useMemoizedFn: () => import_useMemoizedFn.default,
38
42
  usePrevious: () => import_usePrevious.default,
43
+ useQueryState: () => import_useQueryState.default,
39
44
  useSafeState: () => import_useSafeState.default,
40
45
  useSyncState: () => import_useSyncState.default,
41
46
  useTimeoutLock: () => import_useLayoutState.useTimeoutLock,
@@ -52,8 +57,13 @@ var import_useSafeState = __toESM(require("./useSafeState"));
52
57
  var import_useSyncState = __toESM(require("./useSyncState"));
53
58
  var import_useUpdateEffect = __toESM(require("./useUpdateEffect"));
54
59
  var import_useLayoutState = __toESM(require("./useLayoutState"));
60
+ var import_useQueryState = __toESM(require("./useQueryState"));
55
61
  // Annotate the CommonJS export names for ESM import in node:
56
62
  0 && (module.exports = {
63
+ queryBoolean,
64
+ queryJson,
65
+ queryNumber,
66
+ queryString,
57
67
  useControlledState,
58
68
  useEventCallback,
59
69
  useLatest,
@@ -61,6 +71,7 @@ var import_useLayoutState = __toESM(require("./useLayoutState"));
61
71
  useLayoutState,
62
72
  useMemoizedFn,
63
73
  usePrevious,
74
+ useQueryState,
64
75
  useSafeState,
65
76
  useSyncState,
66
77
  useTimeoutLock,
@@ -1,3 +1,4 @@
1
+ /// <reference types="react" />
1
2
  /**
2
3
  * A hook that returns a ref object whose `.current` property is always
3
4
  * updated to the latest value. Useful for accessing the latest value
@@ -0,0 +1,75 @@
1
+ /** Turn a raw query string into a typed value. */
2
+ export type Parser<T> = (value: string) => T;
3
+ /** Turn a typed value back into a query string. */
4
+ export type Serializer<T> = (value: T) => string;
5
+ export interface QueryStateOptions<T> {
6
+ /**
7
+ * Parse the raw `string` read from the URL into `T`.
8
+ * Defaults to identity (the raw string).
9
+ */
10
+ parser?: Parser<T>;
11
+ /**
12
+ * Serialize `T` back into a `string` for the URL.
13
+ * Defaults to `String(value)`.
14
+ */
15
+ serializer?: Serializer<T>;
16
+ /**
17
+ * Value returned when the param is absent from
18
+ * the URL. When set, the hook never returns
19
+ * `null`.
20
+ */
21
+ defaultValue?: T;
22
+ /**
23
+ * How the URL is mutated:
24
+ * - `'replace'` (default) — `history.replaceState`,
25
+ * does not add a history entry.
26
+ * - `'push'` — `history.pushState`, back/forward
27
+ * navigates between values.
28
+ */
29
+ history?: 'push' | 'replace';
30
+ }
31
+ type SetQueryState<T> = (value: T | null | ((prev: T | null) => T | null)) => void;
32
+ /** Identity parser used when none is supplied. */
33
+ export declare const queryString: Parser<string>;
34
+ /** Parse a numeric query param. `NaN` for non-numbers. */
35
+ export declare const queryNumber: Parser<number>;
36
+ /** Parse a boolean query param. Only `'true'` is true. */
37
+ export declare const queryBoolean: Parser<boolean>;
38
+ /** Parse a JSON-encoded query param. */
39
+ export declare const queryJson: Parser<unknown>;
40
+ /**
41
+ * Sync a piece of React state with a URL query
42
+ * parameter, so it survives refreshes and is
43
+ * shareable via the link.
44
+ *
45
+ * Reading is reactive via Next's `useSearchParams`;
46
+ * writing goes through the native
47
+ * `history.pushState/replaceState`, which the Next.js
48
+ * App Router intercepts and syncs back into
49
+ * `useSearchParams` *without* a server round-trip (no
50
+ * route re-render, no scroll-to-top). Because the
51
+ * value is reactive, using it as a SWR/React Query key
52
+ * makes dependent requests refetch automatically.
53
+ *
54
+ * Constraints (inherited from `useSearchParams`):
55
+ * - Next.js App Router only; the calling component
56
+ * must be a Client Component (`'use client'`).
57
+ * - On statically rendered routes, wrap the consumer
58
+ * in `<Suspense>`; otherwise that subtree opts into
59
+ * client-side rendering. During prerender the param
60
+ * reads as absent, so `value` falls back to
61
+ * `defaultValue ?? null`.
62
+ *
63
+ * @param key The query parameter name
64
+ * @param options Parsing, serialization and history
65
+ * behaviour
66
+ * @returns A `[value, setValue]` tuple. `setValue`
67
+ * accepts a value, an updater function, or `null`
68
+ * to remove the param.
69
+ */
70
+ declare function useQueryState(key: string): [string | null, SetQueryState<string>];
71
+ declare function useQueryState<T>(key: string, options: QueryStateOptions<T> & {
72
+ defaultValue: T;
73
+ }): [T, SetQueryState<T>];
74
+ declare function useQueryState<T>(key: string, options: QueryStateOptions<T>): [T | null, SetQueryState<T>];
75
+ export default useQueryState;
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/useQueryState/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => index_default,
34
+ queryBoolean: () => queryBoolean,
35
+ queryJson: () => queryJson,
36
+ queryNumber: () => queryNumber,
37
+ queryString: () => queryString
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+ var import_navigation = require("next/navigation");
41
+ var import_useMemoizedFn = __toESM(require("../useMemoizedFn"));
42
+ var queryString = (value) => value;
43
+ var queryNumber = (value) => Number(value);
44
+ var queryBoolean = (value) => value === "true";
45
+ var queryJson = (value) => JSON.parse(value);
46
+ var isBrowser = typeof window !== "undefined";
47
+ function useQueryState(key, options = {}) {
48
+ const {
49
+ parser,
50
+ serializer = String,
51
+ defaultValue,
52
+ history = "replace"
53
+ } = options;
54
+ const parse = (raw) => {
55
+ if (raw === null) {
56
+ return defaultValue ?? null;
57
+ }
58
+ return parser ? parser(raw) : raw;
59
+ };
60
+ const searchParams = (0, import_navigation.useSearchParams)();
61
+ const value = parse(searchParams.get(key));
62
+ const set = (0, import_useMemoizedFn.default)((next) => {
63
+ if (!isBrowser) {
64
+ return;
65
+ }
66
+ const params = new URLSearchParams(
67
+ window.location.search
68
+ );
69
+ const resolved = typeof next === "function" ? next(
70
+ parse(params.get(key))
71
+ ) : next;
72
+ if (resolved === null || resolved === void 0) {
73
+ params.delete(key);
74
+ } else {
75
+ params.set(key, serializer(resolved));
76
+ }
77
+ const search = params.toString();
78
+ const url = window.location.pathname + (search ? `?${search}` : "") + window.location.hash;
79
+ if (history === "push") {
80
+ window.history.pushState(null, "", url);
81
+ } else {
82
+ window.history.replaceState(null, "", url);
83
+ }
84
+ });
85
+ return [value, set];
86
+ }
87
+ var index_default = useQueryState;
88
+ // Annotate the CommonJS export names for ESM import in node:
89
+ 0 && (module.exports = {
90
+ queryBoolean,
91
+ queryJson,
92
+ queryNumber,
93
+ queryString
94
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1money/hooks",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "React hooks for 1money front-end projects",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -41,6 +41,11 @@
41
41
  "import": "./es/useLayoutState/index.js",
42
42
  "require": "./lib/useLayoutState/index.js"
43
43
  },
44
+ "./useQueryState": {
45
+ "types": "./es/useQueryState/index.d.ts",
46
+ "import": "./es/useQueryState/index.js",
47
+ "require": "./lib/useQueryState/index.js"
48
+ },
44
49
  "./useMemoizedFn": {
45
50
  "types": "./es/useMemoizedFn/index.d.ts",
46
51
  "import": "./es/useMemoizedFn/index.js",
@@ -85,8 +90,14 @@
85
90
  "url": "https://github.com/1Money-Co/1money-hooks"
86
91
  },
87
92
  "peerDependencies": {
93
+ "next": ">=14.0.0",
88
94
  "react": ">=16.8.0"
89
95
  },
96
+ "peerDependenciesMeta": {
97
+ "next": {
98
+ "optional": true
99
+ }
100
+ },
90
101
  "license": "MIT",
91
102
  "devDependencies": {
92
103
  "@eslint/js": "^9.39.4",
@@ -103,6 +114,7 @@
103
114
  "father": "^4.6.17",
104
115
  "globals": "^15.15.0",
105
116
  "jsdom": "^29.0.2",
117
+ "next": "^14.2.0",
106
118
  "prettier": "~3.5.3",
107
119
  "react": "^19.1.0",
108
120
  "react-dom": "^19.2.4",