@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20

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.
Files changed (265) hide show
  1. package/dist/components/button/button.cjs.js +1 -1
  2. package/dist/components/button/button.es.js +19 -18
  3. package/dist/components/button/components/base-button.cjs.js +1 -1
  4. package/dist/components/button/components/base-button.es.js +20 -20
  5. package/dist/components/calendar/calendar.cjs.js +1 -1
  6. package/dist/components/calendar/calendar.es.js +1 -0
  7. package/dist/components/date-picker/date-input.cjs.js +1 -1
  8. package/dist/components/date-picker/date-input.es.js +92 -75
  9. package/dist/components/date-picker/date-picker.cjs.js +1 -1
  10. package/dist/components/date-picker/date-picker.es.js +104 -95
  11. package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
  12. package/dist/components/date-picker/date-picker.utils.es.js +51 -43
  13. package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
  14. package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
  15. package/dist/components/menu/menu.es.js +1 -9
  16. package/dist/components/otp/otp.cjs.js +2 -0
  17. package/dist/components/otp/otp.es.js +93 -0
  18. package/dist/components/password/password.cjs.js +1 -1
  19. package/dist/components/password/password.es.js +2 -2
  20. package/dist/components/select/select.cjs.js +1 -1
  21. package/dist/components/select/select.es.js +68 -60
  22. package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
  23. package/dist/hooks/internal/is-apple-device.es.js +9 -0
  24. package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
  25. package/dist/hooks/internal/use-latest-ref.es.js +11 -0
  26. package/dist/hooks/use-array/use-array.cjs.js +1 -1
  27. package/dist/hooks/use-array/use-array.es.js +54 -42
  28. package/dist/hooks/use-async/use-async.cjs.js +1 -1
  29. package/dist/hooks/use-async/use-async.es.js +53 -20
  30. package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
  31. package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
  32. package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
  33. package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
  34. package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
  35. package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
  36. package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
  37. package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
  38. package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
  39. package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
  40. package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
  41. package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
  42. package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
  43. package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
  44. package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
  45. package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
  46. package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
  47. package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
  48. package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
  49. package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
  50. package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
  51. package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
  52. package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
  53. package/dist/hooks/use-hover/use-hover.es.js +32 -17
  54. package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
  55. package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
  56. package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
  57. package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
  58. package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
  59. package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
  60. package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
  61. package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
  62. package/dist/hooks/use-object/use-object.cjs.js +1 -1
  63. package/dist/hooks/use-object/use-object.es.js +26 -22
  64. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
  65. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
  66. package/dist/hooks/use-step/use-step.cjs.js +1 -1
  67. package/dist/hooks/use-step/use-step.es.js +25 -24
  68. package/dist/index.cjs.js +1 -1
  69. package/dist/index.es.js +308 -300
  70. package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
  71. package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
  72. package/dist/src/components/index.d.ts +1 -0
  73. package/dist/src/hooks/index.d.ts +2 -2
  74. package/dist/src/hooks/internal/index.d.ts +2 -0
  75. package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
  76. package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
  77. package/dist/src/hooks/use-array/use-array.d.ts +24 -11
  78. package/dist/src/hooks/use-async/use-async.d.ts +16 -13
  79. package/dist/src/hooks/use-boolean/index.d.ts +1 -0
  80. package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
  81. package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
  82. package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
  83. package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
  84. package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
  85. package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
  86. package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
  87. package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
  88. package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
  89. package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
  90. package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
  91. package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
  92. package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
  93. package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
  94. package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
  95. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
  96. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
  97. package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
  98. package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
  99. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
  100. package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
  101. package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
  102. package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
  103. package/dist/src/hooks/use-object/use-object.d.ts +15 -6
  104. package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
  105. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
  106. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
  107. package/dist/src/hooks/use-step/use-step.d.ts +18 -11
  108. package/dist/src/utils/form.d.ts +10 -0
  109. package/package.json +1 -1
  110. package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
  111. package/src/components/auto-complete/auto-complete.test.tsx +4 -14
  112. package/src/components/avatar/avatar.test.tsx +7 -12
  113. package/src/components/button/button.test.tsx +10 -15
  114. package/src/components/button/button.tsx +14 -9
  115. package/src/components/button/components/base-button.tsx +2 -4
  116. package/src/components/calendar/calendar.test.tsx +12 -19
  117. package/src/components/calendar/calendar.tsx +4 -0
  118. package/src/components/card/card.test.tsx +4 -6
  119. package/src/components/checkbox/checkbox.test.tsx +12 -8
  120. package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
  121. package/src/components/combobox/combobox.test.tsx +24 -21
  122. package/src/components/date-picker/date-input-form.test.tsx +77 -0
  123. package/src/components/date-picker/date-input.stories.tsx +30 -18
  124. package/src/components/date-picker/date-input.tsx +77 -44
  125. package/src/components/date-picker/date-picker.stories.tsx +31 -1
  126. package/src/components/date-picker/date-picker.test.tsx +3 -13
  127. package/src/components/date-picker/date-picker.tsx +35 -16
  128. package/src/components/date-picker/date-picker.utils.test.ts +32 -14
  129. package/src/components/date-picker/date-picker.utils.ts +33 -0
  130. package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
  131. package/src/components/date-picker/use-hidden-field-value.ts +23 -0
  132. package/src/components/dialog/dialog.test.tsx +10 -8
  133. package/src/components/dropzone/dropzone.test.tsx +11 -13
  134. package/src/components/empty/empty.test.tsx +4 -3
  135. package/src/components/field/field.test.tsx +12 -13
  136. package/src/components/form/form.stories.tsx +16 -1
  137. package/src/components/index.ts +1 -0
  138. package/src/components/label/label.test.tsx +3 -3
  139. package/src/components/menu/menu.tsx +1 -5
  140. package/src/components/number-input/number-input.test.tsx +6 -2
  141. package/src/components/password/password.test.tsx +20 -6
  142. package/src/components/password/password.tsx +2 -2
  143. package/src/components/popover/popover.test.tsx +4 -4
  144. package/src/components/progress/progress.test.tsx +7 -8
  145. package/src/components/radio-group/radio-group.test.tsx +17 -11
  146. package/src/components/select/select.test.tsx +10 -10
  147. package/src/components/select/select.tsx +9 -1
  148. package/src/components/stepper/stepper.stories.tsx +11 -15
  149. package/src/components/stepper/stepper.test.tsx +6 -4
  150. package/src/components/switch/switch.test.tsx +3 -3
  151. package/src/components/table/table.test.tsx +9 -3
  152. package/src/components/tabs/tabs.test.tsx +6 -2
  153. package/src/components/tag/tag.test.tsx +1 -3
  154. package/src/components/textarea/textarea.test.tsx +4 -1
  155. package/src/components/timeline/timeline.test.tsx +10 -5
  156. package/src/components/toast/toast.test.tsx +11 -14
  157. package/src/components/tooltip/tooltip.test.tsx +1 -5
  158. package/src/components/tree/tree.test.tsx +3 -1
  159. package/src/hooks/index.ts +2 -2
  160. package/src/hooks/internal/index.ts +2 -0
  161. package/src/hooks/internal/is-apple-device.test.ts +41 -0
  162. package/src/hooks/internal/is-apple-device.ts +33 -0
  163. package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
  164. package/src/hooks/internal/use-latest-ref.ts +21 -0
  165. package/src/hooks/use-array/use-array.stories.tsx +435 -64
  166. package/src/hooks/use-array/use-array.test.tsx +398 -15
  167. package/src/hooks/use-array/use-array.ts +105 -66
  168. package/src/hooks/use-async/use-async.stories.tsx +255 -131
  169. package/src/hooks/use-async/use-async.test.ts +397 -0
  170. package/src/hooks/use-async/use-async.ts +117 -39
  171. package/src/hooks/use-boolean/index.ts +1 -0
  172. package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
  173. package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
  174. package/src/hooks/use-boolean/use-boolean.ts +50 -0
  175. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
  176. package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
  177. package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
  178. package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
  179. package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
  180. package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
  181. package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
  182. package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
  183. package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
  184. package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
  185. package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
  186. package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
  187. package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
  188. package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
  189. package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
  190. package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
  191. package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
  192. package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
  193. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
  194. package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
  195. package/src/hooks/use-hotkey/index.ts +9 -1
  196. package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
  197. package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
  198. package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
  199. package/src/hooks/use-hotkey/utils/index.ts +8 -3
  200. package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
  201. package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
  202. package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
  203. package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
  204. package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
  205. package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
  206. package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
  207. package/src/hooks/use-hover/use-hover.test.tsx +266 -26
  208. package/src/hooks/use-hover/use-hover.tsx +93 -28
  209. package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
  210. package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
  211. package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
  212. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
  213. package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
  214. package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
  215. package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
  216. package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
  217. package/src/hooks/use-media-query/use-media-query.ts +39 -14
  218. package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
  219. package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
  220. package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
  221. package/src/hooks/use-mutation/use-mutation.ts +97 -0
  222. package/src/hooks/use-object/use-object.stories.tsx +310 -79
  223. package/src/hooks/use-object/use-object.test.tsx +235 -56
  224. package/src/hooks/use-object/use-object.ts +59 -0
  225. package/src/hooks/use-pagination/use-pagination.tsx +0 -1
  226. package/src/hooks/use-prevent-page-close/index.ts +1 -0
  227. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
  228. package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
  229. package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
  230. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
  231. package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
  232. package/src/hooks/use-selection/use-selection.ts +0 -1
  233. package/src/hooks/use-step/use-step.stories.tsx +178 -65
  234. package/src/hooks/use-step/use-step.test.ts +178 -53
  235. package/src/hooks/use-step/use-step.ts +57 -49
  236. package/src/utils/form.test.tsx +13 -8
  237. package/src/utils/form.tsx +10 -0
  238. package/src/utils/functions/getFormData.test.ts +1 -1
  239. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
  240. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
  241. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
  242. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
  243. package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
  244. package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
  245. package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
  246. package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
  247. package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
  248. package/dist/src/hooks/use-toggle/index.d.ts +0 -1
  249. package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
  250. package/src/hooks/use-async/use-async.test.tsx +0 -68
  251. package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
  252. package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
  253. package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
  254. package/src/hooks/use-mutation/use-mutation.tsx +0 -59
  255. package/src/hooks/use-object/use-object.tsx +0 -46
  256. package/src/hooks/use-prevent-close-window/index.ts +0 -1
  257. package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
  258. package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
  259. package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
  260. package/src/hooks/use-toggle/index.ts +0 -1
  261. package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
  262. package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
  263. package/src/hooks/use-toggle/use-toggle.ts +0 -14
  264. /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
  265. /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
@@ -0,0 +1,359 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useMutation } from "../use-mutation";
4
+
5
+ describe("useMutation", () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ // 1 — Initial state
15
+ it("initializes with idle status and null data/error", () => {
16
+ const { result } = renderHook(() =>
17
+ useMutation({ fn: () => Promise.resolve("x") }),
18
+ );
19
+
20
+ expect(result.current.status).toBe("idle");
21
+ expect(result.current.isIdle).toBe(true);
22
+ expect(result.current.isPending).toBe(false);
23
+ expect(result.current.isSuccess).toBe(false);
24
+ expect(result.current.isError).toBe(false);
25
+ expect(result.current.data).toBeNull();
26
+ expect(result.current.error).toBeNull();
27
+ });
28
+
29
+ // 2 — isPending while in flight
30
+ it("sets isPending=true while mutation is in flight", async () => {
31
+ let resolve!: (v: string) => void;
32
+ const deferred = new Promise<string>((res) => {
33
+ resolve = res;
34
+ });
35
+
36
+ const { result } = renderHook(() => useMutation({ fn: () => deferred }));
37
+
38
+ act(() => {
39
+ result.current.mutate(undefined);
40
+ });
41
+
42
+ expect(result.current.status).toBe("pending");
43
+ expect(result.current.isPending).toBe(true);
44
+ expect(result.current.isIdle).toBe(false);
45
+ expect(result.current.isSuccess).toBe(false);
46
+ expect(result.current.isError).toBe(false);
47
+
48
+ await act(async () => {
49
+ resolve("done");
50
+ await Promise.resolve();
51
+ });
52
+ });
53
+
54
+ // 3 — Success path
55
+ it("sets status=success and data on successful mutation", async () => {
56
+ const { result } = renderHook(() =>
57
+ useMutation({ fn: () => Promise.resolve("ok") }),
58
+ );
59
+
60
+ await act(async () => {
61
+ await result.current.mutateAsync(undefined);
62
+ });
63
+
64
+ expect(result.current.status).toBe("success");
65
+ expect(result.current.isSuccess).toBe(true);
66
+ expect(result.current.data).toBe("ok");
67
+ expect(result.current.error).toBeNull();
68
+ });
69
+
70
+ // 4 — Error path
71
+ it("sets status=error and error on failed mutation", async () => {
72
+ const err = new Error("boom");
73
+ const { result } = renderHook(() =>
74
+ useMutation({ fn: () => Promise.reject(err) }),
75
+ );
76
+
77
+ await act(async () => {
78
+ await result.current.mutateAsync(undefined).catch(() => {});
79
+ });
80
+
81
+ expect(result.current.status).toBe("error");
82
+ expect(result.current.isError).toBe(true);
83
+ expect(result.current.error).toBe(err);
84
+ expect(result.current.data).toBeNull();
85
+ });
86
+
87
+ // 5 — mutateAsync resolves with data
88
+ it("mutateAsync resolves with the value returned by fn", async () => {
89
+ const { result } = renderHook(() =>
90
+ useMutation({ fn: () => Promise.resolve(42) }),
91
+ );
92
+
93
+ let resolved: number | undefined;
94
+ await act(async () => {
95
+ resolved = await result.current.mutateAsync(undefined);
96
+ });
97
+
98
+ expect(resolved).toBe(42);
99
+ });
100
+
101
+ // 6 — mutateAsync re-throws on error
102
+ it("mutateAsync re-throws on error", async () => {
103
+ const err = new Error("rethrow-me");
104
+ const { result } = renderHook(() =>
105
+ useMutation({ fn: () => Promise.reject(err) }),
106
+ );
107
+
108
+ let caught: Error | undefined;
109
+ await act(async () => {
110
+ caught = await result.current.mutateAsync(undefined).catch((e) => e);
111
+ });
112
+
113
+ expect(caught).toBe(err);
114
+ });
115
+
116
+ // 7 — mutate is fire-and-forget (void, no throw)
117
+ it("mutate returns void and does not throw on error", async () => {
118
+ const fn = vi.fn().mockRejectedValue(new Error("ignored"));
119
+ const { result } = renderHook(() => useMutation({ fn }));
120
+
121
+ let threw = false;
122
+ await act(async () => {
123
+ try {
124
+ result.current.mutate(undefined);
125
+ await Promise.resolve();
126
+ } catch {
127
+ threw = true;
128
+ }
129
+ });
130
+
131
+ expect(threw).toBe(false);
132
+ });
133
+
134
+ // 8 — onMutate called before fn resolves
135
+ it("calls onMutate(variables) before fn resolves", async () => {
136
+ const callOrder: string[] = [];
137
+ let resolveFn!: () => void;
138
+ const fn = vi.fn(
139
+ () =>
140
+ new Promise<string>((res) => {
141
+ resolveFn = () => res("done");
142
+ }),
143
+ );
144
+ const onMutate = vi.fn(() => {
145
+ callOrder.push("onMutate");
146
+ });
147
+
148
+ const { result } = renderHook(() => useMutation({ fn, onMutate }));
149
+
150
+ act(() => {
151
+ result.current.mutate(undefined);
152
+ });
153
+
154
+ // onMutate called synchronously — fn has not resolved yet
155
+ expect(onMutate).toHaveBeenCalledTimes(1);
156
+ expect(fn).toHaveBeenCalledTimes(1);
157
+ expect(callOrder).toEqual(["onMutate"]);
158
+
159
+ await act(async () => {
160
+ resolveFn();
161
+ await Promise.resolve();
162
+ });
163
+ });
164
+
165
+ // 9 — onSuccess(data, variables)
166
+ it("calls onSuccess(data, variables) on success", async () => {
167
+ const onSuccess = vi.fn();
168
+ const vars = { id: 99 };
169
+
170
+ const { result } = renderHook(() =>
171
+ useMutation({
172
+ fn: (v: { id: number }) => Promise.resolve(`result-${v.id}`),
173
+ onSuccess,
174
+ }),
175
+ );
176
+
177
+ await act(async () => {
178
+ await result.current.mutateAsync(vars);
179
+ });
180
+
181
+ expect(onSuccess).toHaveBeenCalledOnce();
182
+ expect(onSuccess).toHaveBeenCalledWith("result-99", vars);
183
+ });
184
+
185
+ // 10 — onError(error, variables)
186
+ it("calls onError(error, variables) on error", async () => {
187
+ const err = new Error("fail");
188
+ const onError = vi.fn();
189
+ const vars = { id: 7 };
190
+
191
+ const { result } = renderHook(() =>
192
+ useMutation({
193
+ fn: (_v: { id: number }) => Promise.reject(err),
194
+ onError,
195
+ }),
196
+ );
197
+
198
+ await act(async () => {
199
+ await result.current.mutateAsync(vars).catch(() => {});
200
+ });
201
+
202
+ expect(onError).toHaveBeenCalledOnce();
203
+ expect(onError).toHaveBeenCalledWith(err, vars);
204
+ });
205
+
206
+ // 11 — onSettled(data, null, variables) on success
207
+ it("calls onSettled(data, null, variables) on success", async () => {
208
+ const onSettled = vi.fn();
209
+ const vars = { id: 1 };
210
+
211
+ const { result } = renderHook(() =>
212
+ useMutation({
213
+ fn: (v: { id: number }) => Promise.resolve(`ok-${v.id}`),
214
+ onSettled,
215
+ }),
216
+ );
217
+
218
+ await act(async () => {
219
+ await result.current.mutateAsync(vars);
220
+ });
221
+
222
+ expect(onSettled).toHaveBeenCalledOnce();
223
+ expect(onSettled).toHaveBeenCalledWith("ok-1", null, vars);
224
+ });
225
+
226
+ // 12 — onSettled(null, error, variables) on error
227
+ it("calls onSettled(null, error, variables) on error", async () => {
228
+ const err = new Error("oops");
229
+ const onSettled = vi.fn();
230
+ const vars = { id: 2 };
231
+
232
+ const { result } = renderHook(() =>
233
+ useMutation({
234
+ fn: (_v: { id: number }) => Promise.reject(err),
235
+ onSettled,
236
+ }),
237
+ );
238
+
239
+ await act(async () => {
240
+ await result.current.mutateAsync(vars).catch(() => {});
241
+ });
242
+
243
+ expect(onSettled).toHaveBeenCalledOnce();
244
+ expect(onSettled).toHaveBeenCalledWith(null, err, vars);
245
+ });
246
+
247
+ // 13 — reset after success
248
+ it("reset() after success returns to idle and clears data", async () => {
249
+ const { result } = renderHook(() =>
250
+ useMutation({ fn: () => Promise.resolve("done") }),
251
+ );
252
+
253
+ await act(async () => {
254
+ await result.current.mutateAsync(undefined);
255
+ });
256
+
257
+ expect(result.current.status).toBe("success");
258
+
259
+ act(() => {
260
+ result.current.reset();
261
+ });
262
+
263
+ expect(result.current.status).toBe("idle");
264
+ expect(result.current.data).toBeNull();
265
+ expect(result.current.error).toBeNull();
266
+ });
267
+
268
+ // 14 — reset after error
269
+ it("reset() after error returns to idle and clears error", async () => {
270
+ const { result } = renderHook(() =>
271
+ useMutation({ fn: () => Promise.reject(new Error("e")) }),
272
+ );
273
+
274
+ await act(async () => {
275
+ await result.current.mutateAsync(undefined).catch(() => {});
276
+ });
277
+
278
+ expect(result.current.status).toBe("error");
279
+
280
+ act(() => {
281
+ result.current.reset();
282
+ });
283
+
284
+ expect(result.current.status).toBe("idle");
285
+ expect(result.current.error).toBeNull();
286
+ expect(result.current.data).toBeNull();
287
+ });
288
+
289
+ // 15 — mutate identity stable across fn re-renders
290
+ it("mutate reference is stable when inline fn changes", () => {
291
+ let fn = vi.fn().mockResolvedValue("a");
292
+
293
+ const { result, rerender } = renderHook(() => useMutation({ fn }));
294
+
295
+ const mutateRef1 = result.current.mutate;
296
+
297
+ fn = vi.fn().mockResolvedValue("b");
298
+ rerender();
299
+
300
+ expect(result.current.mutate).toBe(mutateRef1);
301
+ });
302
+
303
+ // 16 — mutate stable when onSuccess changes
304
+ it("mutate reference is stable when onSuccess changes between renders", () => {
305
+ let onSuccess = vi.fn();
306
+
307
+ const { result, rerender } = renderHook(() =>
308
+ useMutation({ fn: () => Promise.resolve("x"), onSuccess }),
309
+ );
310
+
311
+ const mutateRef1 = result.current.mutate;
312
+
313
+ onSuccess = vi.fn();
314
+ rerender();
315
+
316
+ expect(result.current.mutate).toBe(mutateRef1);
317
+ });
318
+
319
+ // 17 — variables forwarded to fn and all callbacks
320
+ it("forwards variables to fn, onMutate, onSuccess, and onSettled", async () => {
321
+ const vars = { userId: 42, name: "Alice" };
322
+ const fn = vi.fn().mockResolvedValue("created");
323
+ const onMutate = vi.fn();
324
+ const onSuccess = vi.fn();
325
+ const onSettled = vi.fn();
326
+
327
+ const { result } = renderHook(() =>
328
+ useMutation({ fn, onMutate, onSuccess, onSettled }),
329
+ );
330
+
331
+ await act(async () => {
332
+ await result.current.mutateAsync(vars);
333
+ });
334
+
335
+ expect(fn).toHaveBeenCalledWith(vars);
336
+ expect(onMutate).toHaveBeenCalledWith(vars);
337
+ expect(onSuccess).toHaveBeenCalledWith("created", vars);
338
+ expect(onSettled).toHaveBeenCalledWith("created", null, vars);
339
+ });
340
+
341
+ // 18 — onSettled called even when onError throws
342
+ it("onSettled is called even when onError throws", async () => {
343
+ const err = new Error("mutation-fail");
344
+ const onError = vi.fn().mockImplementation(() => {
345
+ throw new Error("onError itself throws");
346
+ });
347
+ const onSettled = vi.fn();
348
+
349
+ const { result } = renderHook(() =>
350
+ useMutation({ fn: () => Promise.reject(err), onError, onSettled }),
351
+ );
352
+
353
+ await act(async () => {
354
+ await result.current.mutateAsync(undefined).catch(() => {});
355
+ });
356
+
357
+ expect(onSettled).toHaveBeenCalledOnce();
358
+ });
359
+ });
@@ -0,0 +1,97 @@
1
+ import { useCallback, useState } from "react";
2
+ import { useLatestRef } from "../internal/use-latest-ref";
3
+
4
+ type MutationStatus = "idle" | "pending" | "success" | "error";
5
+
6
+ export interface UseMutationOptions<TVariables = void, TData = unknown> {
7
+ fn: (variables: TVariables) => Promise<TData>;
8
+ onMutate?: (variables: TVariables) => void;
9
+ onSuccess?: (data: TData, variables: TVariables) => void;
10
+ onError?: (error: Error, variables: TVariables) => void;
11
+ onSettled?: (
12
+ data: TData | null,
13
+ error: Error | null,
14
+ variables: TVariables,
15
+ ) => void;
16
+ }
17
+
18
+ export interface UseMutationReturn<TVariables = void, TData = unknown> {
19
+ mutate: (variables: TVariables) => void;
20
+ mutateAsync: (variables: TVariables) => Promise<TData>;
21
+ reset: () => void;
22
+ data: TData | null;
23
+ error: Error | null;
24
+ status: MutationStatus;
25
+ isIdle: boolean;
26
+ isPending: boolean;
27
+ isSuccess: boolean;
28
+ isError: boolean;
29
+ }
30
+
31
+ export const useMutation = <TVariables = void, TData = unknown>(
32
+ options: UseMutationOptions<TVariables, TData>,
33
+ ): UseMutationReturn<TVariables, TData> => {
34
+ const [status, setStatus] = useState<MutationStatus>("idle");
35
+ const [data, setData] = useState<TData | null>(null);
36
+ const [error, setError] = useState<Error | null>(null);
37
+
38
+ const fnRef = useLatestRef(options.fn);
39
+ const onMutateRef = useLatestRef(options.onMutate);
40
+ const onSuccessRef = useLatestRef(options.onSuccess);
41
+ const onErrorRef = useLatestRef(options.onError);
42
+ const onSettledRef = useLatestRef(options.onSettled);
43
+
44
+ const mutateAsync = useCallback(
45
+ async (variables: TVariables): Promise<TData> => {
46
+ onMutateRef.current?.(variables);
47
+ setStatus("pending");
48
+ setData(null);
49
+ setError(null);
50
+
51
+ let result: TData | null = null;
52
+ let caught: Error | null = null;
53
+ try {
54
+ result = await fnRef.current(variables);
55
+ setData(result);
56
+ setStatus("success");
57
+ onSuccessRef.current?.(result, variables);
58
+ return result;
59
+ } catch (err) {
60
+ caught = err as Error;
61
+ setError(caught);
62
+ setStatus("error");
63
+ onErrorRef.current?.(caught, variables);
64
+ throw caught;
65
+ } finally {
66
+ onSettledRef.current?.(result, caught, variables);
67
+ }
68
+ },
69
+ [fnRef, onMutateRef, onSuccessRef, onErrorRef, onSettledRef],
70
+ );
71
+
72
+ const mutate = useCallback(
73
+ (variables: TVariables): void => {
74
+ mutateAsync(variables).catch(() => {});
75
+ },
76
+ [mutateAsync],
77
+ );
78
+
79
+ const reset = useCallback(() => {
80
+ setStatus("idle");
81
+ setData(null);
82
+ setError(null);
83
+ }, []);
84
+
85
+ return {
86
+ mutate,
87
+ mutateAsync,
88
+ reset,
89
+ data,
90
+ error,
91
+ status,
92
+ isIdle: status === "idle",
93
+ isPending: status === "pending",
94
+ isSuccess: status === "success",
95
+ isError: status === "error",
96
+ };
97
+ };