atomirx 0.0.1
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 +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { useStable } from "./useStable";
|
|
3
|
+
import { isStableFn } from "../core/equality";
|
|
4
|
+
import { wrappers } from "./strictModeTest";
|
|
5
|
+
|
|
6
|
+
describe.each(wrappers)("useStable - $mode", ({ renderHook }) => {
|
|
7
|
+
describe("basic stabilization", () => {
|
|
8
|
+
it("should return stable object reference across renders", () => {
|
|
9
|
+
const { result, rerender } = renderHook(
|
|
10
|
+
(props) =>
|
|
11
|
+
useStable({
|
|
12
|
+
name: props.name,
|
|
13
|
+
count: props.count,
|
|
14
|
+
}),
|
|
15
|
+
{ initialProps: { name: "John", count: 1 } }
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const firstResult = result.current;
|
|
19
|
+
|
|
20
|
+
rerender({ name: "John", count: 1 });
|
|
21
|
+
|
|
22
|
+
expect(result.current).toBe(firstResult);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should update property when value changes", () => {
|
|
26
|
+
const { result, rerender } = renderHook(
|
|
27
|
+
(props) =>
|
|
28
|
+
useStable({
|
|
29
|
+
name: props.name,
|
|
30
|
+
}),
|
|
31
|
+
{ initialProps: { name: "John" } }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(result.current.name).toBe("John");
|
|
35
|
+
|
|
36
|
+
rerender({ name: "Jane" });
|
|
37
|
+
|
|
38
|
+
expect(result.current.name).toBe("Jane");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("function stabilization", () => {
|
|
43
|
+
it("should wrap functions in stable wrappers", () => {
|
|
44
|
+
const callback = vi.fn(() => 42);
|
|
45
|
+
|
|
46
|
+
const { result } = renderHook(() =>
|
|
47
|
+
useStable({
|
|
48
|
+
callback,
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(isStableFn(result.current.callback)).toBe(true);
|
|
53
|
+
expect(result.current.callback()).toBe(42);
|
|
54
|
+
expect(callback).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should maintain stable function reference across renders", () => {
|
|
58
|
+
const { result, rerender } = renderHook(
|
|
59
|
+
(props: { callback: () => number }) =>
|
|
60
|
+
useStable({
|
|
61
|
+
callback: props.callback,
|
|
62
|
+
}),
|
|
63
|
+
{ initialProps: { callback: () => 1 } }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const firstCallback = result.current.callback;
|
|
67
|
+
|
|
68
|
+
rerender({ callback: () => 2 });
|
|
69
|
+
|
|
70
|
+
expect(result.current.callback).toBe(firstCallback);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should call latest function implementation", () => {
|
|
74
|
+
const { result, rerender } = renderHook(
|
|
75
|
+
(props: { callback: () => number }) =>
|
|
76
|
+
useStable({
|
|
77
|
+
callback: props.callback,
|
|
78
|
+
}),
|
|
79
|
+
{ initialProps: { callback: () => 1 } }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.current.callback()).toBe(1);
|
|
83
|
+
|
|
84
|
+
rerender({ callback: () => 2 });
|
|
85
|
+
|
|
86
|
+
expect(result.current.callback()).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle multiple functions", () => {
|
|
90
|
+
const { result, rerender } = renderHook(
|
|
91
|
+
(props: { onClick: () => string; onSubmit: () => string }) =>
|
|
92
|
+
useStable({
|
|
93
|
+
onClick: props.onClick,
|
|
94
|
+
onSubmit: props.onSubmit,
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
initialProps: {
|
|
98
|
+
onClick: () => "click1",
|
|
99
|
+
onSubmit: () => "submit1",
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const firstOnClick = result.current.onClick;
|
|
105
|
+
const firstOnSubmit = result.current.onSubmit;
|
|
106
|
+
|
|
107
|
+
rerender({
|
|
108
|
+
onClick: () => "click2",
|
|
109
|
+
onSubmit: () => "submit2",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.current.onClick).toBe(firstOnClick);
|
|
113
|
+
expect(result.current.onSubmit).toBe(firstOnSubmit);
|
|
114
|
+
expect(result.current.onClick()).toBe("click2");
|
|
115
|
+
expect(result.current.onSubmit()).toBe("submit2");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("array stabilization (default: shallow)", () => {
|
|
120
|
+
it("should stabilize array with same items (shallow equal)", () => {
|
|
121
|
+
const { result, rerender } = renderHook(
|
|
122
|
+
(props) =>
|
|
123
|
+
useStable({
|
|
124
|
+
items: props.items,
|
|
125
|
+
}),
|
|
126
|
+
{ initialProps: { items: [1, 2, 3] } }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const firstItems = result.current.items;
|
|
130
|
+
|
|
131
|
+
rerender({ items: [1, 2, 3] });
|
|
132
|
+
|
|
133
|
+
expect(result.current.items).toBe(firstItems);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should update array when items change", () => {
|
|
137
|
+
const { result, rerender } = renderHook(
|
|
138
|
+
(props) =>
|
|
139
|
+
useStable({
|
|
140
|
+
items: props.items,
|
|
141
|
+
}),
|
|
142
|
+
{ initialProps: { items: [1, 2, 3] } }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const firstItems = result.current.items;
|
|
146
|
+
|
|
147
|
+
rerender({ items: [1, 2, 4] });
|
|
148
|
+
|
|
149
|
+
expect(result.current.items).not.toBe(firstItems);
|
|
150
|
+
expect(result.current.items).toEqual([1, 2, 4]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should update array when length changes", () => {
|
|
154
|
+
const { result, rerender } = renderHook(
|
|
155
|
+
(props) =>
|
|
156
|
+
useStable({
|
|
157
|
+
items: props.items,
|
|
158
|
+
}),
|
|
159
|
+
{ initialProps: { items: [1, 2, 3] } }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const firstItems = result.current.items;
|
|
163
|
+
|
|
164
|
+
rerender({ items: [1, 2] });
|
|
165
|
+
|
|
166
|
+
expect(result.current.items).not.toBe(firstItems);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should work as useEffect dependency", () => {
|
|
170
|
+
const effectFn = vi.fn();
|
|
171
|
+
|
|
172
|
+
const { result, rerender } = renderHook(
|
|
173
|
+
(props) => {
|
|
174
|
+
const stable = useStable({
|
|
175
|
+
items: props.items,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Simulate useEffect behavior - track if dependency changed
|
|
179
|
+
effectFn(stable.items);
|
|
180
|
+
|
|
181
|
+
return stable;
|
|
182
|
+
},
|
|
183
|
+
{ initialProps: { items: [1, 2, 3] } }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const firstItems = result.current.items;
|
|
187
|
+
|
|
188
|
+
// Same content - should be stable
|
|
189
|
+
rerender({ items: [1, 2, 3] });
|
|
190
|
+
|
|
191
|
+
// Effect should receive same reference
|
|
192
|
+
expect(effectFn).toHaveBeenLastCalledWith(firstItems);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("object stabilization (default: shallow)", () => {
|
|
197
|
+
it("should stabilize object with same shallow values", () => {
|
|
198
|
+
const { result, rerender } = renderHook(
|
|
199
|
+
(props) =>
|
|
200
|
+
useStable({
|
|
201
|
+
person: props.person,
|
|
202
|
+
}),
|
|
203
|
+
{ initialProps: { person: { name: "John", age: 30 } } }
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const firstPerson = result.current.person;
|
|
207
|
+
|
|
208
|
+
rerender({ person: { name: "John", age: 30 } });
|
|
209
|
+
|
|
210
|
+
expect(result.current.person).toBe(firstPerson);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should update object when shallow values change", () => {
|
|
214
|
+
const { result, rerender } = renderHook(
|
|
215
|
+
(props) =>
|
|
216
|
+
useStable({
|
|
217
|
+
person: props.person,
|
|
218
|
+
}),
|
|
219
|
+
{ initialProps: { person: { name: "John", age: 30 } } }
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const firstPerson = result.current.person;
|
|
223
|
+
|
|
224
|
+
rerender({ person: { name: "Jane", age: 30 } });
|
|
225
|
+
|
|
226
|
+
expect(result.current.person).not.toBe(firstPerson);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should NOT stabilize nested objects by default (shallow comparison)", () => {
|
|
230
|
+
const { result, rerender } = renderHook(
|
|
231
|
+
(props) =>
|
|
232
|
+
useStable({
|
|
233
|
+
data: props.data,
|
|
234
|
+
}),
|
|
235
|
+
{
|
|
236
|
+
initialProps: {
|
|
237
|
+
data: { nested: { value: 1 } },
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const firstData = result.current.data;
|
|
243
|
+
|
|
244
|
+
// Different nested object reference, even with same content
|
|
245
|
+
rerender({ data: { nested: { value: 1 } } });
|
|
246
|
+
|
|
247
|
+
// Should NOT be stable because nested object has different reference
|
|
248
|
+
expect(result.current.data).not.toBe(firstData);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("Date stabilization (default: deep/timestamp)", () => {
|
|
253
|
+
it("should stabilize Date with same timestamp", () => {
|
|
254
|
+
const { result, rerender } = renderHook(
|
|
255
|
+
(props) =>
|
|
256
|
+
useStable({
|
|
257
|
+
date: props.date,
|
|
258
|
+
}),
|
|
259
|
+
{ initialProps: { date: new Date("2024-01-01") } }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const firstDate = result.current.date;
|
|
263
|
+
|
|
264
|
+
rerender({ date: new Date("2024-01-01") });
|
|
265
|
+
|
|
266
|
+
expect(result.current.date).toBe(firstDate);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should update Date when timestamp changes", () => {
|
|
270
|
+
const { result, rerender } = renderHook(
|
|
271
|
+
(props) =>
|
|
272
|
+
useStable({
|
|
273
|
+
date: props.date,
|
|
274
|
+
}),
|
|
275
|
+
{ initialProps: { date: new Date("2024-01-01") } }
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const firstDate = result.current.date;
|
|
279
|
+
|
|
280
|
+
rerender({ date: new Date("2024-01-02") });
|
|
281
|
+
|
|
282
|
+
expect(result.current.date).not.toBe(firstDate);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("primitive stabilization (default: strict)", () => {
|
|
287
|
+
it("should stabilize primitives with strict equality", () => {
|
|
288
|
+
const { result, rerender } = renderHook(
|
|
289
|
+
(props) =>
|
|
290
|
+
useStable({
|
|
291
|
+
count: props.count,
|
|
292
|
+
name: props.name,
|
|
293
|
+
active: props.active,
|
|
294
|
+
}),
|
|
295
|
+
{ initialProps: { count: 42, name: "test", active: true } }
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const firstResult = result.current;
|
|
299
|
+
|
|
300
|
+
rerender({ count: 42, name: "test", active: true });
|
|
301
|
+
|
|
302
|
+
expect(result.current).toBe(firstResult);
|
|
303
|
+
expect(result.current.count).toBe(42);
|
|
304
|
+
expect(result.current.name).toBe("test");
|
|
305
|
+
expect(result.current.active).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("custom equals option", () => {
|
|
310
|
+
it("should use custom equals for specified properties", () => {
|
|
311
|
+
const { result, rerender } = renderHook(
|
|
312
|
+
(props) =>
|
|
313
|
+
useStable(
|
|
314
|
+
{
|
|
315
|
+
data: props.data,
|
|
316
|
+
},
|
|
317
|
+
{ data: "deep" }
|
|
318
|
+
),
|
|
319
|
+
{
|
|
320
|
+
initialProps: {
|
|
321
|
+
data: { nested: { value: 1 } },
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const firstData = result.current.data;
|
|
327
|
+
|
|
328
|
+
// With deep equals, nested objects with same content should be stable
|
|
329
|
+
rerender({ data: { nested: { value: 1 } } });
|
|
330
|
+
|
|
331
|
+
expect(result.current.data).toBe(firstData);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should override default equals", () => {
|
|
335
|
+
const { result, rerender } = renderHook(
|
|
336
|
+
(props) =>
|
|
337
|
+
useStable(
|
|
338
|
+
{
|
|
339
|
+
items: props.items,
|
|
340
|
+
},
|
|
341
|
+
{ items: "strict" }
|
|
342
|
+
),
|
|
343
|
+
{ initialProps: { items: [1, 2, 3] } }
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const firstItems = result.current.items;
|
|
347
|
+
|
|
348
|
+
// With strict equals, same content but different reference should NOT be stable
|
|
349
|
+
rerender({ items: [1, 2, 3] });
|
|
350
|
+
|
|
351
|
+
expect(result.current.items).not.toBe(firstItems);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should support custom equals function", () => {
|
|
355
|
+
const { result, rerender } = renderHook(
|
|
356
|
+
(props) =>
|
|
357
|
+
useStable(
|
|
358
|
+
{
|
|
359
|
+
user: props.user,
|
|
360
|
+
},
|
|
361
|
+
{ user: (a, b) => a?.id === b?.id }
|
|
362
|
+
),
|
|
363
|
+
{ initialProps: { user: { id: 1, name: "John" } } }
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const firstUser = result.current.user;
|
|
367
|
+
|
|
368
|
+
// Same id, different name - should be stable with custom equals
|
|
369
|
+
rerender({ user: { id: 1, name: "Jane" } });
|
|
370
|
+
|
|
371
|
+
expect(result.current.user).toBe(firstUser);
|
|
372
|
+
|
|
373
|
+
// Different id - should NOT be stable
|
|
374
|
+
rerender({ user: { id: 2, name: "Jane" } });
|
|
375
|
+
|
|
376
|
+
expect(result.current.user).not.toBe(firstUser);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should ignore equals option for functions", () => {
|
|
380
|
+
const { result, rerender } = renderHook(
|
|
381
|
+
(props: { callback: () => number }) =>
|
|
382
|
+
useStable(
|
|
383
|
+
{
|
|
384
|
+
callback: props.callback,
|
|
385
|
+
},
|
|
386
|
+
// Functions are excluded from equals type, so this is ignored at runtime
|
|
387
|
+
{ callback: "deep" } as any
|
|
388
|
+
),
|
|
389
|
+
{ initialProps: { callback: () => 1 } }
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const firstCallback = result.current.callback;
|
|
393
|
+
|
|
394
|
+
rerender({ callback: () => 2 });
|
|
395
|
+
|
|
396
|
+
// Function should still be stabilized regardless of equals option
|
|
397
|
+
expect(result.current.callback).toBe(firstCallback);
|
|
398
|
+
expect(isStableFn(result.current.callback)).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("mixed properties", () => {
|
|
403
|
+
it("should handle mixed property types correctly", () => {
|
|
404
|
+
type Props = {
|
|
405
|
+
person: { name: string; address: { city: string } };
|
|
406
|
+
date: Date;
|
|
407
|
+
items: number[];
|
|
408
|
+
callback: () => string;
|
|
409
|
+
count: number;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const { result, rerender } = renderHook(
|
|
413
|
+
(props: Props) =>
|
|
414
|
+
useStable(
|
|
415
|
+
{
|
|
416
|
+
person: props.person,
|
|
417
|
+
date: props.date,
|
|
418
|
+
items: props.items,
|
|
419
|
+
callback: props.callback,
|
|
420
|
+
count: props.count,
|
|
421
|
+
},
|
|
422
|
+
{ person: "deep" }
|
|
423
|
+
),
|
|
424
|
+
{
|
|
425
|
+
initialProps: {
|
|
426
|
+
person: { name: "John", address: { city: "NYC" } },
|
|
427
|
+
date: new Date("2024-01-01"),
|
|
428
|
+
items: [1, 2, 3],
|
|
429
|
+
callback: () => "hello",
|
|
430
|
+
count: 42,
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const first = {
|
|
436
|
+
person: result.current.person,
|
|
437
|
+
date: result.current.date,
|
|
438
|
+
items: result.current.items,
|
|
439
|
+
callback: result.current.callback,
|
|
440
|
+
count: result.current.count,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Rerender with same logical values but new references
|
|
444
|
+
rerender({
|
|
445
|
+
person: { name: "John", address: { city: "NYC" } },
|
|
446
|
+
date: new Date("2024-01-01"),
|
|
447
|
+
items: [1, 2, 3],
|
|
448
|
+
callback: () => "world",
|
|
449
|
+
count: 42,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// person: deep equals - should be stable
|
|
453
|
+
expect(result.current.person).toBe(first.person);
|
|
454
|
+
|
|
455
|
+
// date: timestamp comparison - should be stable
|
|
456
|
+
expect(result.current.date).toBe(first.date);
|
|
457
|
+
|
|
458
|
+
// items: shallow equals - should be stable
|
|
459
|
+
expect(result.current.items).toBe(first.items);
|
|
460
|
+
|
|
461
|
+
// callback: always stabilized - should be stable reference
|
|
462
|
+
expect(result.current.callback).toBe(first.callback);
|
|
463
|
+
|
|
464
|
+
// count: strict equals - should be stable
|
|
465
|
+
expect(result.current.count).toBe(first.count);
|
|
466
|
+
|
|
467
|
+
// But callback should call new implementation
|
|
468
|
+
expect(result.current.callback()).toBe("world");
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("edge cases", () => {
|
|
473
|
+
it("should handle null values", () => {
|
|
474
|
+
const { result, rerender } = renderHook(
|
|
475
|
+
(props) =>
|
|
476
|
+
useStable({
|
|
477
|
+
value: props.value,
|
|
478
|
+
}),
|
|
479
|
+
{ initialProps: { value: null as string | null } }
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
expect(result.current.value).toBe(null);
|
|
483
|
+
|
|
484
|
+
rerender({ value: "hello" });
|
|
485
|
+
|
|
486
|
+
expect(result.current.value).toBe("hello");
|
|
487
|
+
|
|
488
|
+
rerender({ value: null });
|
|
489
|
+
|
|
490
|
+
expect(result.current.value).toBe(null);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should handle undefined values", () => {
|
|
494
|
+
const { result, rerender } = renderHook(
|
|
495
|
+
(props) =>
|
|
496
|
+
useStable({
|
|
497
|
+
value: props.value,
|
|
498
|
+
}),
|
|
499
|
+
{ initialProps: { value: undefined as string | undefined } }
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
expect(result.current.value).toBe(undefined);
|
|
503
|
+
|
|
504
|
+
rerender({ value: "hello" });
|
|
505
|
+
|
|
506
|
+
expect(result.current.value).toBe("hello");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("should handle empty object", () => {
|
|
510
|
+
const { result } = renderHook(() => useStable({}));
|
|
511
|
+
|
|
512
|
+
expect(result.current).toEqual({});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should handle adding new properties on rerender", () => {
|
|
516
|
+
const { result, rerender } = renderHook((props) => useStable(props), {
|
|
517
|
+
initialProps: { a: 1 } as { a: number; b?: number },
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
expect(result.current.a).toBe(1);
|
|
521
|
+
expect(result.current.b).toBeUndefined();
|
|
522
|
+
|
|
523
|
+
rerender({ a: 1, b: 2 });
|
|
524
|
+
|
|
525
|
+
expect(result.current.a).toBe(1);
|
|
526
|
+
expect(result.current.b).toBe(2);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("type safety", () => {
|
|
531
|
+
it("should preserve property types", () => {
|
|
532
|
+
const { result } = renderHook(() =>
|
|
533
|
+
useStable({
|
|
534
|
+
name: "John",
|
|
535
|
+
age: 30,
|
|
536
|
+
items: [1, 2, 3],
|
|
537
|
+
callback: (x: number) => x * 2,
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// TypeScript should infer these types correctly
|
|
542
|
+
const name: string = result.current.name;
|
|
543
|
+
const age: number = result.current.age;
|
|
544
|
+
const items: number[] = result.current.items;
|
|
545
|
+
const doubled: number = result.current.callback(5);
|
|
546
|
+
|
|
547
|
+
expect(name).toBe("John");
|
|
548
|
+
expect(age).toBe(30);
|
|
549
|
+
expect(items).toEqual([1, 2, 3]);
|
|
550
|
+
expect(doubled).toBe(10);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|