@usefy/use-object-state 0.19.0

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/README.md ADDED
@@ -0,0 +1,187 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-object-state</h1>
6
+
7
+ <p align="center">
8
+ <strong>Object state with immutable partial updates (patch/merge) and reset</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-object-state"><img src="https://img.shields.io/npm/v/@usefy/use-object-state.svg?style=flat-square&color=007acc" alt="npm version" /></a>
13
+ <a href="https://www.npmjs.com/package/@usefy/use-object-state"><img src="https://img.shields.io/npm/dm/@usefy/use-object-state.svg?style=flat-square&color=007acc" alt="npm downloads" /></a>
14
+ <a href="https://bundlephobia.com/package/@usefy/use-object-state"><img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-object-state?style=flat-square&color=007acc" alt="bundle size" /></a>
15
+ <a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@usefy/use-object-state.svg?style=flat-square&color=007acc" alt="license" /></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="#installation">Installation</a> •
20
+ <a href="#quick-start">Quick Start</a> •
21
+ <a href="#api">API</a> •
22
+ <a href="#testing">Testing</a> •
23
+ <a href="#license">License</a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-useobjectstate--docs" target="_blank" rel="noopener noreferrer">
28
+ <strong>📚 View Storybook Demo</strong>
29
+ </a>
30
+ </p>
31
+
32
+ ---
33
+
34
+ ## Overview
35
+
36
+ `useObjectState` is part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It manages object state with immutable partial updates and reset — the ergonomic middle ground between `useState` (which replaces the whole value) and `useReducer` (which makes you write a reducer).
37
+
38
+ It returns a `useState`-style tuple `[state, patch, reset]`. `patch` shallow-merges a partial into the current state **immutably**, so you update one field without spreading the whole object yourself, and `reset` restores the initial value (or a provided one).
39
+
40
+ ## Features
41
+
42
+ - **Partial merge** — `patch({ field })` shallow-merges immutably (`{ ...prev, ...partial }`); untouched keys are preserved by reference
43
+ - **Functional updater** — `patch(prev => ({ ... }))` computes the next patch from the current state
44
+ - **Reset** — `reset()` restores the captured initial value; `reset(next)` swaps in a provided object
45
+ - **Stable actions** — `patch` and `reset` keep a stable identity for the component's lifetime (safe as `useEffect` dependencies)
46
+ - **Lazy init** — accepts a factory (run once), just like `useState`; the produced value is cached for `reset()`
47
+ - **SSR- & StrictMode-safe** — pure state logic, no side effects, no globals
48
+ - **TypeScript-first** — full type inference and exported types
49
+ - **Tiny & tree-shakeable** — zero dependencies, published as its own package
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ # npm
55
+ npm install @usefy/use-object-state
56
+
57
+ # yarn
58
+ yarn add @usefy/use-object-state
59
+
60
+ # pnpm
61
+ pnpm add @usefy/use-object-state
62
+ ```
63
+
64
+ Requires React 18 or 19 (`peerDependencies: "react": "^18.0.0 || ^19.0.0"`).
65
+
66
+ ## Quick Start
67
+
68
+ ```tsx
69
+ import { useObjectState } from "@usefy/use-object-state";
70
+
71
+ interface FormState {
72
+ name: string;
73
+ email: string;
74
+ subscribe: boolean;
75
+ }
76
+
77
+ function SignupForm() {
78
+ const [form, patch, reset] = useObjectState<FormState>({
79
+ name: "",
80
+ email: "",
81
+ subscribe: false,
82
+ });
83
+
84
+ return (
85
+ <form>
86
+ <input
87
+ value={form.name}
88
+ onChange={(e) => patch({ name: e.target.value })}
89
+ />
90
+ <input
91
+ value={form.email}
92
+ onChange={(e) => patch({ email: e.target.value })}
93
+ />
94
+ <label>
95
+ <input
96
+ type="checkbox"
97
+ checked={form.subscribe}
98
+ onChange={(e) => patch({ subscribe: e.target.checked })}
99
+ />
100
+ Subscribe
101
+ </label>
102
+ <button type="button" onClick={() => reset()}>
103
+ Reset
104
+ </button>
105
+ </form>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ## API
111
+
112
+ ### `useObjectState<T extends object>(initialState)`
113
+
114
+ Returns a `[state, patch, reset]` tuple.
115
+
116
+ #### Parameters
117
+
118
+ | Parameter | Type | Description |
119
+ | -------------- | --------------------------- | ------------------------------------------------------------------------------------ |
120
+ | `initialState` | `T \| (() => T)` | The initial object, or a factory returning it (evaluated once on mount, then cached) |
121
+
122
+ `T` must be a plain object/record — this hook is for objects, not arrays (use [`useList`](https://www.npmjs.com/package/@usefy/use-list)) or primitives (use `useState`).
123
+
124
+ #### Returns `[state, patch, reset]`
125
+
126
+ | Item | Type | Description |
127
+ | ------- | ----------------------- | ---------------------------------------------------------------- |
128
+ | `state` | `T` | The current object |
129
+ | `patch` | `ObjectStatePatch<T>` | Immutably shallow-merge a `Partial<T>` (or a functional updater) |
130
+ | `reset` | `ObjectStateReset<T>` | Restore the initial value, or set a provided object |
131
+
132
+ #### `patch(partial | updater)`
133
+
134
+ ```tsx
135
+ patch({ field: newValue }); // shallow-merge a partial
136
+ patch((prev) => ({ count: prev.count + 1 })); // compute the patch from prev
137
+ ```
138
+
139
+ - Accepts a `Partial<T>` and shallow-merges it immutably: `{ ...prev, ...partial }`. It always produces a **new** object; the previous state is never mutated.
140
+ - Also accepts a functional updater `(prev: T) => Partial<T>` for when the next value depends on the current one.
141
+ - Only the provided keys change; untouched keys are preserved by reference.
142
+ - **Every `patch` triggers a re-render** — there is no shallow-equality dedupe (matching `react-use`'s `useSetState`). This keeps the semantics simple and predictable; add your own guard if you need to skip no-ops.
143
+
144
+ #### `reset(next?)`
145
+
146
+ ```tsx
147
+ reset(); // back to the initial state captured on mount
148
+ reset(nextState); // set to the provided object instead
149
+ ```
150
+
151
+ - `reset()` restores the value captured on mount. If a lazy initializer was used, the value it produced is cached once and reused — the initializer is **not** re-run.
152
+ - `reset(nextState)` replaces the state with the provided object.
153
+
154
+ ### Shallow-merge caveat
155
+
156
+ The merge is **shallow** — a nested object in the patch replaces the previous nested object wholesale, it is not deep-merged:
157
+
158
+ ```tsx
159
+ const [state, patch] = useObjectState({ user: { name: "Alice", age: 30 } });
160
+ patch({ user: { name: "Bob" } });
161
+ // state.user is now { name: "Bob" } — `age` is gone.
162
+
163
+ // Spread the nested object yourself to update one nested field:
164
+ patch({ user: { ...state.user, name: "Bob" } });
165
+ ```
166
+
167
+ ### Types
168
+
169
+ ```tsx
170
+ import {
171
+ useObjectState,
172
+ type ObjectStateInitializer,
173
+ type ObjectStatePatch,
174
+ type ObjectStateReset,
175
+ type UseObjectStateReturn,
176
+ } from "@usefy/use-object-state";
177
+ ```
178
+
179
+ ## Testing
180
+
181
+ 📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-object-state/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages) — **22 tests**, 100% statement coverage.
182
+
183
+ ## License
184
+
185
+ MIT © [mirunamu](https://github.com/mirunamu00)
186
+
187
+ This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Accepted initial value for {@link useObjectState}.
3
+ *
4
+ * Mirrors `useState`'s lazy-initializer support: pass a plain object, or a
5
+ * function returning one (evaluated **once** on mount). The resolved value is
6
+ * captured for {@link ObjectStateReset | reset()}.
7
+ *
8
+ * @template T - The object shape held in state.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * useObjectState({ name: "", age: 0 }); // direct object
13
+ * useObjectState(() => ({ name: "", age: 0 })); // lazy (run once)
14
+ * ```
15
+ */
16
+ type ObjectStateInitializer<T extends object> = T | (() => T);
17
+ /**
18
+ * The `patch` updater returned by {@link useObjectState}.
19
+ *
20
+ * Shallow-merges a partial into the current state **immutably** — it always
21
+ * produces a brand-new object (`{ ...prev, ...partial }`) and never mutates the
22
+ * previous state. Only the provided keys change; untouched keys are preserved by
23
+ * reference.
24
+ *
25
+ * Two forms are accepted:
26
+ * - a `Partial<T>` object — merged directly, and
27
+ * - a functional updater `(prev: T) => Partial<T>` — computes the partial from
28
+ * the current state (use this when the next value depends on the previous one).
29
+ *
30
+ * The merge is **shallow**: a nested object in the partial replaces the previous
31
+ * nested object wholesale, it is not deep-merged.
32
+ *
33
+ * @template T - The object shape held in state.
34
+ */
35
+ type ObjectStatePatch<T extends object> = (patch: Partial<T> | ((prev: T) => Partial<T>)) => void;
36
+ /**
37
+ * The `reset` action returned by {@link useObjectState}.
38
+ *
39
+ * - `reset()` — restores the state to the initial value captured on mount (if a
40
+ * lazy initializer was used, the value it produced is cached once and reused;
41
+ * the initializer is not re-run).
42
+ * - `reset(nextState)` — replaces the state with the provided object instead.
43
+ *
44
+ * @template T - The object shape held in state.
45
+ */
46
+ type ObjectStateReset<T extends object> = (nextState?: T) => void;
47
+ /**
48
+ * Return type of {@link useObjectState}: a tuple of the current state, the
49
+ * `patch` updater, and the `reset` action — mirroring the `useState` tuple shape
50
+ * but with partial-merge semantics.
51
+ *
52
+ * @template T - The object shape held in state.
53
+ */
54
+ type UseObjectStateReturn<T extends object> = [
55
+ T,
56
+ ObjectStatePatch<T>,
57
+ ObjectStateReset<T>
58
+ ];
59
+
60
+ /**
61
+ * A React hook for managing object state with immutable partial updates
62
+ * (patch/merge) and reset — the ergonomic middle ground between `useState`
63
+ * (replace the whole value) and `useReducer` (write a reducer).
64
+ *
65
+ * Returns a `useState`-style tuple `[state, patch, reset]`:
66
+ *
67
+ * - **`patch`** shallow-merges a `Partial<T>` into the current state
68
+ * **immutably** (`{ ...prev, ...partial }`), producing a brand-new object on
69
+ * every call and never mutating the previous state. Only the provided keys
70
+ * change; untouched keys are preserved by reference. It also accepts a
71
+ * functional updater `(prev) => Partial<T>` when the next value depends on the
72
+ * previous one.
73
+ * - **`reset`** restores the state to the initial value captured on mount, or —
74
+ * when passed an argument — to that object instead.
75
+ *
76
+ * Both `patch` and `reset` are referentially stable for the lifetime of the
77
+ * component (safe to use as `useEffect` dependencies). The hook is pure state
78
+ * logic, so it is SSR-safe and StrictMode-safe.
79
+ *
80
+ * @remarks
81
+ * - **Objects only.** Intended for plain objects/records (`T extends object`),
82
+ * not arrays or primitives — for array state use `useList`, and for a single
83
+ * value use `useState`.
84
+ * - **Shallow merge.** A nested object in the patch replaces the previous nested
85
+ * object wholesale; it is not deep-merged. Spread the nested object yourself
86
+ * (`patch({ user: { ...state.user, name } })`) to update one nested field.
87
+ * - **Every patch re-renders.** No shallow-equality dedupe is performed — like
88
+ * `react-use`'s `useSetState`, each `patch` commits a new object and triggers
89
+ * a render, even if the merged values are unchanged. This keeps the semantics
90
+ * simple and predictable; wrap in your own guard if you need to skip no-ops.
91
+ * - **Lazy init is cached once.** If a function initializer is passed, it runs a
92
+ * single time on mount and the produced value is reused by `reset()`; the
93
+ * initializer is not re-evaluated on reset.
94
+ *
95
+ * @template T - The object shape held in state.
96
+ * @param initialState - The initial object, or a factory returning it (run once).
97
+ * @returns `[state, patch, reset]`
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * interface FormState {
102
+ * name: string;
103
+ * email: string;
104
+ * subscribe: boolean;
105
+ * }
106
+ *
107
+ * function SignupForm() {
108
+ * const [form, patch, reset] = useObjectState<FormState>({
109
+ * name: "",
110
+ * email: "",
111
+ * subscribe: false,
112
+ * });
113
+ *
114
+ * return (
115
+ * <form>
116
+ * <input
117
+ * value={form.name}
118
+ * onChange={(e) => patch({ name: e.target.value })}
119
+ * />
120
+ * <input
121
+ * value={form.email}
122
+ * onChange={(e) => patch({ email: e.target.value })}
123
+ * />
124
+ * <label>
125
+ * <input
126
+ * type="checkbox"
127
+ * checked={form.subscribe}
128
+ * onChange={(e) => patch({ subscribe: e.target.checked })}
129
+ * />
130
+ * Subscribe
131
+ * </label>
132
+ * <button type="button" onClick={() => reset()}>Reset</button>
133
+ * </form>
134
+ * );
135
+ * }
136
+ * ```
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * // Functional patch + reset to a provided object
141
+ * const [counter, patch, reset] = useObjectState({ count: 0, step: 1 });
142
+ * patch((prev) => ({ count: prev.count + prev.step })); // compute from prev
143
+ * reset({ count: 10, step: 5 }); // reset to a new object
144
+ * ```
145
+ */
146
+ declare function useObjectState<T extends object>(initialState: ObjectStateInitializer<T>): UseObjectStateReturn<T>;
147
+
148
+ export { type ObjectStateInitializer, type ObjectStatePatch, type ObjectStateReset, type UseObjectStateReturn, useObjectState };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Accepted initial value for {@link useObjectState}.
3
+ *
4
+ * Mirrors `useState`'s lazy-initializer support: pass a plain object, or a
5
+ * function returning one (evaluated **once** on mount). The resolved value is
6
+ * captured for {@link ObjectStateReset | reset()}.
7
+ *
8
+ * @template T - The object shape held in state.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * useObjectState({ name: "", age: 0 }); // direct object
13
+ * useObjectState(() => ({ name: "", age: 0 })); // lazy (run once)
14
+ * ```
15
+ */
16
+ type ObjectStateInitializer<T extends object> = T | (() => T);
17
+ /**
18
+ * The `patch` updater returned by {@link useObjectState}.
19
+ *
20
+ * Shallow-merges a partial into the current state **immutably** — it always
21
+ * produces a brand-new object (`{ ...prev, ...partial }`) and never mutates the
22
+ * previous state. Only the provided keys change; untouched keys are preserved by
23
+ * reference.
24
+ *
25
+ * Two forms are accepted:
26
+ * - a `Partial<T>` object — merged directly, and
27
+ * - a functional updater `(prev: T) => Partial<T>` — computes the partial from
28
+ * the current state (use this when the next value depends on the previous one).
29
+ *
30
+ * The merge is **shallow**: a nested object in the partial replaces the previous
31
+ * nested object wholesale, it is not deep-merged.
32
+ *
33
+ * @template T - The object shape held in state.
34
+ */
35
+ type ObjectStatePatch<T extends object> = (patch: Partial<T> | ((prev: T) => Partial<T>)) => void;
36
+ /**
37
+ * The `reset` action returned by {@link useObjectState}.
38
+ *
39
+ * - `reset()` — restores the state to the initial value captured on mount (if a
40
+ * lazy initializer was used, the value it produced is cached once and reused;
41
+ * the initializer is not re-run).
42
+ * - `reset(nextState)` — replaces the state with the provided object instead.
43
+ *
44
+ * @template T - The object shape held in state.
45
+ */
46
+ type ObjectStateReset<T extends object> = (nextState?: T) => void;
47
+ /**
48
+ * Return type of {@link useObjectState}: a tuple of the current state, the
49
+ * `patch` updater, and the `reset` action — mirroring the `useState` tuple shape
50
+ * but with partial-merge semantics.
51
+ *
52
+ * @template T - The object shape held in state.
53
+ */
54
+ type UseObjectStateReturn<T extends object> = [
55
+ T,
56
+ ObjectStatePatch<T>,
57
+ ObjectStateReset<T>
58
+ ];
59
+
60
+ /**
61
+ * A React hook for managing object state with immutable partial updates
62
+ * (patch/merge) and reset — the ergonomic middle ground between `useState`
63
+ * (replace the whole value) and `useReducer` (write a reducer).
64
+ *
65
+ * Returns a `useState`-style tuple `[state, patch, reset]`:
66
+ *
67
+ * - **`patch`** shallow-merges a `Partial<T>` into the current state
68
+ * **immutably** (`{ ...prev, ...partial }`), producing a brand-new object on
69
+ * every call and never mutating the previous state. Only the provided keys
70
+ * change; untouched keys are preserved by reference. It also accepts a
71
+ * functional updater `(prev) => Partial<T>` when the next value depends on the
72
+ * previous one.
73
+ * - **`reset`** restores the state to the initial value captured on mount, or —
74
+ * when passed an argument — to that object instead.
75
+ *
76
+ * Both `patch` and `reset` are referentially stable for the lifetime of the
77
+ * component (safe to use as `useEffect` dependencies). The hook is pure state
78
+ * logic, so it is SSR-safe and StrictMode-safe.
79
+ *
80
+ * @remarks
81
+ * - **Objects only.** Intended for plain objects/records (`T extends object`),
82
+ * not arrays or primitives — for array state use `useList`, and for a single
83
+ * value use `useState`.
84
+ * - **Shallow merge.** A nested object in the patch replaces the previous nested
85
+ * object wholesale; it is not deep-merged. Spread the nested object yourself
86
+ * (`patch({ user: { ...state.user, name } })`) to update one nested field.
87
+ * - **Every patch re-renders.** No shallow-equality dedupe is performed — like
88
+ * `react-use`'s `useSetState`, each `patch` commits a new object and triggers
89
+ * a render, even if the merged values are unchanged. This keeps the semantics
90
+ * simple and predictable; wrap in your own guard if you need to skip no-ops.
91
+ * - **Lazy init is cached once.** If a function initializer is passed, it runs a
92
+ * single time on mount and the produced value is reused by `reset()`; the
93
+ * initializer is not re-evaluated on reset.
94
+ *
95
+ * @template T - The object shape held in state.
96
+ * @param initialState - The initial object, or a factory returning it (run once).
97
+ * @returns `[state, patch, reset]`
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * interface FormState {
102
+ * name: string;
103
+ * email: string;
104
+ * subscribe: boolean;
105
+ * }
106
+ *
107
+ * function SignupForm() {
108
+ * const [form, patch, reset] = useObjectState<FormState>({
109
+ * name: "",
110
+ * email: "",
111
+ * subscribe: false,
112
+ * });
113
+ *
114
+ * return (
115
+ * <form>
116
+ * <input
117
+ * value={form.name}
118
+ * onChange={(e) => patch({ name: e.target.value })}
119
+ * />
120
+ * <input
121
+ * value={form.email}
122
+ * onChange={(e) => patch({ email: e.target.value })}
123
+ * />
124
+ * <label>
125
+ * <input
126
+ * type="checkbox"
127
+ * checked={form.subscribe}
128
+ * onChange={(e) => patch({ subscribe: e.target.checked })}
129
+ * />
130
+ * Subscribe
131
+ * </label>
132
+ * <button type="button" onClick={() => reset()}>Reset</button>
133
+ * </form>
134
+ * );
135
+ * }
136
+ * ```
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * // Functional patch + reset to a provided object
141
+ * const [counter, patch, reset] = useObjectState({ count: 0, step: 1 });
142
+ * patch((prev) => ({ count: prev.count + prev.step })); // compute from prev
143
+ * reset({ count: 10, step: 5 }); // reset to a new object
144
+ * ```
145
+ */
146
+ declare function useObjectState<T extends object>(initialState: ObjectStateInitializer<T>): UseObjectStateReturn<T>;
147
+
148
+ export { type ObjectStateInitializer, type ObjectStatePatch, type ObjectStateReset, type UseObjectStateReturn, useObjectState };
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useObjectState: () => useObjectState
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useObjectState.ts
28
+ var import_react = require("react");
29
+ function resolveInitial(initial) {
30
+ return typeof initial === "function" ? initial() : initial;
31
+ }
32
+ function useObjectState(initialState) {
33
+ const initialRef = (0, import_react.useRef)(null);
34
+ if (initialRef.current === null) {
35
+ initialRef.current = resolveInitial(initialState);
36
+ }
37
+ const [state, setState] = (0, import_react.useState)(initialRef.current);
38
+ const patch = (0, import_react.useCallback)((update) => {
39
+ setState((prev) => {
40
+ const partial = typeof update === "function" ? update(prev) : update;
41
+ return { ...prev, ...partial };
42
+ });
43
+ }, []);
44
+ const reset = (0, import_react.useCallback)((nextState) => {
45
+ setState(nextState !== void 0 ? nextState : initialRef.current);
46
+ }, []);
47
+ return [state, patch, reset];
48
+ }
49
+ // Annotate the CommonJS export names for ESM import in node:
50
+ 0 && (module.exports = {
51
+ useObjectState
52
+ });
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useObjectState.ts"],"sourcesContent":["export { useObjectState } from \"./useObjectState\";\nexport type {\n ObjectStateInitializer,\n ObjectStatePatch,\n ObjectStateReset,\n UseObjectStateReturn,\n} from \"./types\";\n","import { useCallback, useRef, useState } from \"react\";\nimport type {\n ObjectStateInitializer,\n ObjectStatePatch,\n ObjectStateReset,\n UseObjectStateReturn,\n} from \"./types\";\n\n/**\n * Resolve an {@link ObjectStateInitializer} to a concrete object. A function\n * initializer is invoked once; a direct object is returned as-is.\n */\nfunction resolveInitial<T extends object>(initial: ObjectStateInitializer<T>): T {\n return typeof initial === \"function\" ? (initial as () => T)() : initial;\n}\n\n/**\n * A React hook for managing object state with immutable partial updates\n * (patch/merge) and reset — the ergonomic middle ground between `useState`\n * (replace the whole value) and `useReducer` (write a reducer).\n *\n * Returns a `useState`-style tuple `[state, patch, reset]`:\n *\n * - **`patch`** shallow-merges a `Partial<T>` into the current state\n * **immutably** (`{ ...prev, ...partial }`), producing a brand-new object on\n * every call and never mutating the previous state. Only the provided keys\n * change; untouched keys are preserved by reference. It also accepts a\n * functional updater `(prev) => Partial<T>` when the next value depends on the\n * previous one.\n * - **`reset`** restores the state to the initial value captured on mount, or —\n * when passed an argument — to that object instead.\n *\n * Both `patch` and `reset` are referentially stable for the lifetime of the\n * component (safe to use as `useEffect` dependencies). The hook is pure state\n * logic, so it is SSR-safe and StrictMode-safe.\n *\n * @remarks\n * - **Objects only.** Intended for plain objects/records (`T extends object`),\n * not arrays or primitives — for array state use `useList`, and for a single\n * value use `useState`.\n * - **Shallow merge.** A nested object in the patch replaces the previous nested\n * object wholesale; it is not deep-merged. Spread the nested object yourself\n * (`patch({ user: { ...state.user, name } })`) to update one nested field.\n * - **Every patch re-renders.** No shallow-equality dedupe is performed — like\n * `react-use`'s `useSetState`, each `patch` commits a new object and triggers\n * a render, even if the merged values are unchanged. This keeps the semantics\n * simple and predictable; wrap in your own guard if you need to skip no-ops.\n * - **Lazy init is cached once.** If a function initializer is passed, it runs a\n * single time on mount and the produced value is reused by `reset()`; the\n * initializer is not re-evaluated on reset.\n *\n * @template T - The object shape held in state.\n * @param initialState - The initial object, or a factory returning it (run once).\n * @returns `[state, patch, reset]`\n *\n * @example\n * ```tsx\n * interface FormState {\n * name: string;\n * email: string;\n * subscribe: boolean;\n * }\n *\n * function SignupForm() {\n * const [form, patch, reset] = useObjectState<FormState>({\n * name: \"\",\n * email: \"\",\n * subscribe: false,\n * });\n *\n * return (\n * <form>\n * <input\n * value={form.name}\n * onChange={(e) => patch({ name: e.target.value })}\n * />\n * <input\n * value={form.email}\n * onChange={(e) => patch({ email: e.target.value })}\n * />\n * <label>\n * <input\n * type=\"checkbox\"\n * checked={form.subscribe}\n * onChange={(e) => patch({ subscribe: e.target.checked })}\n * />\n * Subscribe\n * </label>\n * <button type=\"button\" onClick={() => reset()}>Reset</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Functional patch + reset to a provided object\n * const [counter, patch, reset] = useObjectState({ count: 0, step: 1 });\n * patch((prev) => ({ count: prev.count + prev.step })); // compute from prev\n * reset({ count: 10, step: 5 }); // reset to a new object\n * ```\n */\nexport function useObjectState<T extends object>(\n initialState: ObjectStateInitializer<T>\n): UseObjectStateReturn<T> {\n // Capture the resolved initial value exactly once so `reset()` can restore it\n // without re-running a lazy initializer. `T extends object`, so the resolved\n // value is never null — the null sentinel safely means \"not yet resolved\".\n const initialRef = useRef<T | null>(null);\n if (initialRef.current === null) {\n initialRef.current = resolveInitial(initialState);\n }\n\n const [state, setState] = useState<T>(initialRef.current);\n\n const patch = useCallback<ObjectStatePatch<T>>((update) => {\n setState((prev) => {\n const partial =\n typeof update === \"function\" ? update(prev) : update;\n // Immutable shallow merge — always a brand-new object; `prev` is untouched.\n return { ...prev, ...partial };\n });\n }, []);\n\n const reset = useCallback<ObjectStateReset<T>>((nextState) => {\n setState(nextState !== undefined ? nextState : (initialRef.current as T));\n }, []);\n\n return [state, patch, reset];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA8C;AAY9C,SAAS,eAAiC,SAAuC;AAC/E,SAAO,OAAO,YAAY,aAAc,QAAoB,IAAI;AAClE;AAwFO,SAAS,eACd,cACyB;AAIzB,QAAM,iBAAa,qBAAiB,IAAI;AACxC,MAAI,WAAW,YAAY,MAAM;AAC/B,eAAW,UAAU,eAAe,YAAY;AAAA,EAClD;AAEA,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAY,WAAW,OAAO;AAExD,QAAM,YAAQ,0BAAiC,CAAC,WAAW;AACzD,aAAS,CAAC,SAAS;AACjB,YAAM,UACJ,OAAO,WAAW,aAAa,OAAO,IAAI,IAAI;AAEhD,aAAO,EAAE,GAAG,MAAM,GAAG,QAAQ;AAAA,IAC/B,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,YAAQ,0BAAiC,CAAC,cAAc;AAC5D,aAAS,cAAc,SAAY,YAAa,WAAW,OAAa;AAAA,EAC1E,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,OAAO,OAAO,KAAK;AAC7B;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,26 @@
1
+ // src/useObjectState.ts
2
+ import { useCallback, useRef, useState } from "react";
3
+ function resolveInitial(initial) {
4
+ return typeof initial === "function" ? initial() : initial;
5
+ }
6
+ function useObjectState(initialState) {
7
+ const initialRef = useRef(null);
8
+ if (initialRef.current === null) {
9
+ initialRef.current = resolveInitial(initialState);
10
+ }
11
+ const [state, setState] = useState(initialRef.current);
12
+ const patch = useCallback((update) => {
13
+ setState((prev) => {
14
+ const partial = typeof update === "function" ? update(prev) : update;
15
+ return { ...prev, ...partial };
16
+ });
17
+ }, []);
18
+ const reset = useCallback((nextState) => {
19
+ setState(nextState !== void 0 ? nextState : initialRef.current);
20
+ }, []);
21
+ return [state, patch, reset];
22
+ }
23
+ export {
24
+ useObjectState
25
+ };
26
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useObjectState.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport type {\n ObjectStateInitializer,\n ObjectStatePatch,\n ObjectStateReset,\n UseObjectStateReturn,\n} from \"./types\";\n\n/**\n * Resolve an {@link ObjectStateInitializer} to a concrete object. A function\n * initializer is invoked once; a direct object is returned as-is.\n */\nfunction resolveInitial<T extends object>(initial: ObjectStateInitializer<T>): T {\n return typeof initial === \"function\" ? (initial as () => T)() : initial;\n}\n\n/**\n * A React hook for managing object state with immutable partial updates\n * (patch/merge) and reset — the ergonomic middle ground between `useState`\n * (replace the whole value) and `useReducer` (write a reducer).\n *\n * Returns a `useState`-style tuple `[state, patch, reset]`:\n *\n * - **`patch`** shallow-merges a `Partial<T>` into the current state\n * **immutably** (`{ ...prev, ...partial }`), producing a brand-new object on\n * every call and never mutating the previous state. Only the provided keys\n * change; untouched keys are preserved by reference. It also accepts a\n * functional updater `(prev) => Partial<T>` when the next value depends on the\n * previous one.\n * - **`reset`** restores the state to the initial value captured on mount, or —\n * when passed an argument — to that object instead.\n *\n * Both `patch` and `reset` are referentially stable for the lifetime of the\n * component (safe to use as `useEffect` dependencies). The hook is pure state\n * logic, so it is SSR-safe and StrictMode-safe.\n *\n * @remarks\n * - **Objects only.** Intended for plain objects/records (`T extends object`),\n * not arrays or primitives — for array state use `useList`, and for a single\n * value use `useState`.\n * - **Shallow merge.** A nested object in the patch replaces the previous nested\n * object wholesale; it is not deep-merged. Spread the nested object yourself\n * (`patch({ user: { ...state.user, name } })`) to update one nested field.\n * - **Every patch re-renders.** No shallow-equality dedupe is performed — like\n * `react-use`'s `useSetState`, each `patch` commits a new object and triggers\n * a render, even if the merged values are unchanged. This keeps the semantics\n * simple and predictable; wrap in your own guard if you need to skip no-ops.\n * - **Lazy init is cached once.** If a function initializer is passed, it runs a\n * single time on mount and the produced value is reused by `reset()`; the\n * initializer is not re-evaluated on reset.\n *\n * @template T - The object shape held in state.\n * @param initialState - The initial object, or a factory returning it (run once).\n * @returns `[state, patch, reset]`\n *\n * @example\n * ```tsx\n * interface FormState {\n * name: string;\n * email: string;\n * subscribe: boolean;\n * }\n *\n * function SignupForm() {\n * const [form, patch, reset] = useObjectState<FormState>({\n * name: \"\",\n * email: \"\",\n * subscribe: false,\n * });\n *\n * return (\n * <form>\n * <input\n * value={form.name}\n * onChange={(e) => patch({ name: e.target.value })}\n * />\n * <input\n * value={form.email}\n * onChange={(e) => patch({ email: e.target.value })}\n * />\n * <label>\n * <input\n * type=\"checkbox\"\n * checked={form.subscribe}\n * onChange={(e) => patch({ subscribe: e.target.checked })}\n * />\n * Subscribe\n * </label>\n * <button type=\"button\" onClick={() => reset()}>Reset</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Functional patch + reset to a provided object\n * const [counter, patch, reset] = useObjectState({ count: 0, step: 1 });\n * patch((prev) => ({ count: prev.count + prev.step })); // compute from prev\n * reset({ count: 10, step: 5 }); // reset to a new object\n * ```\n */\nexport function useObjectState<T extends object>(\n initialState: ObjectStateInitializer<T>\n): UseObjectStateReturn<T> {\n // Capture the resolved initial value exactly once so `reset()` can restore it\n // without re-running a lazy initializer. `T extends object`, so the resolved\n // value is never null — the null sentinel safely means \"not yet resolved\".\n const initialRef = useRef<T | null>(null);\n if (initialRef.current === null) {\n initialRef.current = resolveInitial(initialState);\n }\n\n const [state, setState] = useState<T>(initialRef.current);\n\n const patch = useCallback<ObjectStatePatch<T>>((update) => {\n setState((prev) => {\n const partial =\n typeof update === \"function\" ? update(prev) : update;\n // Immutable shallow merge — always a brand-new object; `prev` is untouched.\n return { ...prev, ...partial };\n });\n }, []);\n\n const reset = useCallback<ObjectStateReset<T>>((nextState) => {\n setState(nextState !== undefined ? nextState : (initialRef.current as T));\n }, []);\n\n return [state, patch, reset];\n}\n"],"mappings":";AAAA,SAAS,aAAa,QAAQ,gBAAgB;AAY9C,SAAS,eAAiC,SAAuC;AAC/E,SAAO,OAAO,YAAY,aAAc,QAAoB,IAAI;AAClE;AAwFO,SAAS,eACd,cACyB;AAIzB,QAAM,aAAa,OAAiB,IAAI;AACxC,MAAI,WAAW,YAAY,MAAM;AAC/B,eAAW,UAAU,eAAe,YAAY;AAAA,EAClD;AAEA,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,WAAW,OAAO;AAExD,QAAM,QAAQ,YAAiC,CAAC,WAAW;AACzD,aAAS,CAAC,SAAS;AACjB,YAAM,UACJ,OAAO,WAAW,aAAa,OAAO,IAAI,IAAI;AAEhD,aAAO,EAAE,GAAG,MAAM,GAAG,QAAQ;AAAA,IAC/B,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,YAAiC,CAAC,cAAc;AAC5D,aAAS,cAAc,SAAY,YAAa,WAAW,OAAa;AAAA,EAC1E,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,OAAO,OAAO,KAAK;AAC7B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@usefy/use-object-state",
3
+ "version": "0.19.0",
4
+ "description": "A React hook for object state with immutable partial updates (patch/merge) and reset",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "jsdom": "^27.3.0",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "rimraf": "^6.0.1",
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.0.0",
34
+ "vitest": "^4.0.16"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/mirunamu00/usefy.git",
42
+ "directory": "packages/hooks/use-object-state"
43
+ },
44
+ "license": "MIT",
45
+ "keywords": [
46
+ "react",
47
+ "hooks",
48
+ "object",
49
+ "state",
50
+ "setState",
51
+ "merge",
52
+ "patch",
53
+ "immutable",
54
+ "useObjectState"
55
+ ],
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsup --watch",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "typecheck": "tsc --noEmit",
62
+ "clean": "rimraf dist"
63
+ }
64
+ }