@isograph/react-disposable-state 0.0.4 → 0.1.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/dist/CacheItem.d.ts +18 -14
- package/dist/CacheItem.js +65 -61
- package/dist/ParentCache.d.ts +2 -2
- package/dist/ParentCache.js +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/useCachedPrecommitValue.d.ts +2 -2
- package/dist/useCachedPrecommitValue.js +1 -1
- package/dist/useDisposableState.d.ts +3 -3
- package/dist/useDisposableState.js +6 -4
- package/dist/useHasCommittedRef.d.ts +1 -1
- package/dist/useLazyDisposableState.d.ts +1 -1
- package/dist/useLazyDisposableState.js +3 -3
- package/dist/useUpdatableDisposableState.d.ts +1 -1
- package/dist/useUpdatableDisposableState.js +3 -3
- package/docs/managing-complex-state.md +4 -4
- package/package.json +7 -2
- package/src/CacheItem.test.ts +94 -94
- package/src/CacheItem.ts +80 -76
- package/src/ParentCache.test.ts +31 -27
- package/src/ParentCache.ts +3 -3
- package/src/index.ts +8 -8
- package/src/useCachedPrecommitValue.test.tsx +57 -73
- package/src/useCachedPrecommitValue.ts +7 -7
- package/src/useDisposableState.ts +13 -11
- package/src/useHasCommittedRef.ts +1 -1
- package/src/useLazyDisposableState.ts +7 -7
- package/src/useUpdatableDisposableState.test.tsx +334 -327
- package/src/useUpdatableDisposableState.ts +9 -9
@@ -1,13 +1,13 @@
|
|
1
|
-
import { describe, test, vi, expect, assert } from
|
2
|
-
import { ParentCache } from
|
3
|
-
import { ItemCleanupPair } from
|
4
|
-
import { useCachedPrecommitValue } from
|
5
|
-
import React from
|
6
|
-
import { create } from
|
7
|
-
import { CacheItem, CacheItemState } from
|
1
|
+
import { describe, test, vi, expect, assert } from 'vitest';
|
2
|
+
import { ParentCache } from './ParentCache';
|
3
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
4
|
+
import { useCachedPrecommitValue } from './useCachedPrecommitValue';
|
5
|
+
import React from 'react';
|
6
|
+
import { create } from 'react-test-renderer';
|
7
|
+
import { CacheItem, CacheItemState } from './CacheItem';
|
8
8
|
|
9
9
|
function getItem<T>(cache: ParentCache<T>): CacheItem<T> | null {
|
10
|
-
return (cache as any).
|
10
|
+
return (cache as any).__cacheItem;
|
11
11
|
}
|
12
12
|
|
13
13
|
function getState<T>(cacheItem: CacheItem<T>): CacheItemState<T> {
|
@@ -54,14 +54,14 @@ function promiseAndResolver() {
|
|
54
54
|
async function awaitableCreate(Component, isConcurrent) {
|
55
55
|
const element = create(
|
56
56
|
Component,
|
57
|
-
isConcurrent ? { unstable_isConcurrent: true } : undefined
|
57
|
+
isConcurrent ? { unstable_isConcurrent: true } : undefined,
|
58
58
|
);
|
59
59
|
await shortPromise();
|
60
60
|
return element;
|
61
61
|
}
|
62
62
|
|
63
|
-
describe(
|
64
|
-
test(
|
63
|
+
describe('useCachedPrecommitValue', () => {
|
64
|
+
test('on initial render, it should call getOrPopulateAndTemporaryRetain', async () => {
|
65
65
|
const disposeItem = vi.fn();
|
66
66
|
const factory = vi.fn(() => {
|
67
67
|
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
@@ -70,7 +70,7 @@ describe("useCachedPrecommitValue", () => {
|
|
70
70
|
const cache = new ParentCache(factory);
|
71
71
|
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
72
72
|
cache,
|
73
|
-
|
73
|
+
'getOrPopulateAndTemporaryRetain',
|
74
74
|
);
|
75
75
|
|
76
76
|
const componentCommits = vi.fn();
|
@@ -103,7 +103,7 @@ describe("useCachedPrecommitValue", () => {
|
|
103
103
|
expect(render).toHaveBeenCalledTimes(1);
|
104
104
|
});
|
105
105
|
|
106
|
-
test(
|
106
|
+
test('on commit, it should call the provided callback and empty the parent cache', async () => {
|
107
107
|
const disposeItem = vi.fn();
|
108
108
|
const factory = vi.fn(() => {
|
109
109
|
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
@@ -124,7 +124,7 @@ describe("useCachedPrecommitValue", () => {
|
|
124
124
|
expect(componentCommits).toHaveBeenCalledTimes(1);
|
125
125
|
expect(hookOnCommit).toBeCalledTimes(1);
|
126
126
|
expect(hookOnCommit.mock.calls[0][0][0]).toBe(1);
|
127
|
-
expect(typeof hookOnCommit.mock.calls[0][0][1]).toBe(
|
127
|
+
expect(typeof hookOnCommit.mock.calls[0][0][1]).toBe('function');
|
128
128
|
expect(factory).toBeCalledTimes(1);
|
129
129
|
expect(disposeItem).not.toBeCalled();
|
130
130
|
expect(cache.isEmpty()).toBe(true);
|
@@ -140,7 +140,7 @@ describe("useCachedPrecommitValue", () => {
|
|
140
140
|
expect(render).toHaveBeenCalledTimes(1);
|
141
141
|
});
|
142
142
|
|
143
|
-
test(
|
143
|
+
test('after commit, on subsequent renders it should return null', async () => {
|
144
144
|
const disposeItem = vi.fn();
|
145
145
|
const factory = vi.fn(() => {
|
146
146
|
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
@@ -186,8 +186,8 @@ describe("useCachedPrecommitValue", () => {
|
|
186
186
|
});
|
187
187
|
|
188
188
|
test(
|
189
|
-
|
190
|
-
|
189
|
+
'on repeated pre-commit renders, if the temporary retain is not disposed, ' +
|
190
|
+
'it should re-call getOrPopulateAndTemporaryRetain but not call factory again',
|
191
191
|
async () => {
|
192
192
|
const disposeItem = vi.fn();
|
193
193
|
const factory = vi.fn(() => {
|
@@ -197,7 +197,7 @@ describe("useCachedPrecommitValue", () => {
|
|
197
197
|
const cache = new ParentCache(factory);
|
198
198
|
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
199
199
|
cache,
|
200
|
-
|
200
|
+
'getOrPopulateAndTemporaryRetain',
|
201
201
|
);
|
202
202
|
|
203
203
|
const componentCommits = vi.fn();
|
@@ -213,7 +213,7 @@ describe("useCachedPrecommitValue", () => {
|
|
213
213
|
|
214
214
|
renderCount++;
|
215
215
|
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
|
216
|
-
renderCount
|
216
|
+
renderCount,
|
217
217
|
);
|
218
218
|
|
219
219
|
React.useEffect(() => {
|
@@ -233,7 +233,7 @@ describe("useCachedPrecommitValue", () => {
|
|
233
233
|
<TestComponent />
|
234
234
|
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
235
235
|
</React.Suspense>,
|
236
|
-
true
|
236
|
+
true,
|
237
237
|
);
|
238
238
|
|
239
239
|
expect(componentCommits).toHaveBeenCalledTimes(0);
|
@@ -244,12 +244,12 @@ describe("useCachedPrecommitValue", () => {
|
|
244
244
|
|
245
245
|
expect(componentCommits).toHaveBeenCalledTimes(1);
|
246
246
|
expect(render).toHaveBeenCalledTimes(2);
|
247
|
-
}
|
247
|
+
},
|
248
248
|
);
|
249
249
|
|
250
250
|
test(
|
251
|
-
|
252
|
-
|
251
|
+
'on repeated pre-commit renders, if the temporary retain is disposed, ' +
|
252
|
+
'it should re-call getOrPopulateAndTemporaryRetain and factory',
|
253
253
|
async () => {
|
254
254
|
const disposeItem = vi.fn();
|
255
255
|
let factoryValue = 0;
|
@@ -262,7 +262,7 @@ describe("useCachedPrecommitValue", () => {
|
|
262
262
|
|
263
263
|
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
264
264
|
cache,
|
265
|
-
|
265
|
+
'getOrPopulateAndTemporaryRetain',
|
266
266
|
);
|
267
267
|
|
268
268
|
const componentCommits = vi.fn();
|
@@ -305,7 +305,7 @@ describe("useCachedPrecommitValue", () => {
|
|
305
305
|
<TestComponent />
|
306
306
|
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
307
307
|
</React.Suspense>,
|
308
|
-
true
|
308
|
+
true,
|
309
309
|
);
|
310
310
|
|
311
311
|
expect(componentCommits).toHaveBeenCalledTimes(0);
|
@@ -316,13 +316,13 @@ describe("useCachedPrecommitValue", () => {
|
|
316
316
|
|
317
317
|
expect(componentCommits).toHaveBeenCalledTimes(1);
|
318
318
|
expect(render).toHaveBeenCalledTimes(2);
|
319
|
-
}
|
319
|
+
},
|
320
320
|
);
|
321
321
|
|
322
322
|
test(
|
323
|
-
|
324
|
-
|
325
|
-
|
323
|
+
'if the item has been disposed between the render and the commit, ' +
|
324
|
+
'and the parent cache is empty, it will call factory again, re-render an ' +
|
325
|
+
'additional time and called onCommit with the newly generated item',
|
326
326
|
async () => {
|
327
327
|
const disposeItem = vi.fn();
|
328
328
|
let factoryCount = 0;
|
@@ -334,11 +334,11 @@ describe("useCachedPrecommitValue", () => {
|
|
334
334
|
const cache = new ParentCache(factory);
|
335
335
|
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
336
336
|
cache,
|
337
|
-
|
337
|
+
'getOrPopulateAndTemporaryRetain',
|
338
338
|
);
|
339
339
|
const getAndPermanentRetainIfPresent = vi.spyOn(
|
340
340
|
cache,
|
341
|
-
|
341
|
+
'getAndPermanentRetainIfPresent',
|
342
342
|
);
|
343
343
|
|
344
344
|
const componentCommits = vi.fn();
|
@@ -354,7 +354,7 @@ describe("useCachedPrecommitValue", () => {
|
|
354
354
|
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
|
355
355
|
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
356
356
|
expect(getAndPermanentRetainIfPresent.mock.results[0].value).toBe(
|
357
|
-
null
|
357
|
+
null,
|
358
358
|
);
|
359
359
|
expect(factory).toHaveBeenCalledTimes(2);
|
360
360
|
expect(cache.isEmpty()).toBe(true);
|
@@ -402,20 +402,20 @@ describe("useCachedPrecommitValue", () => {
|
|
402
402
|
<TestComponent />
|
403
403
|
<CodeExecutor />
|
404
404
|
</>,
|
405
|
-
false
|
405
|
+
false,
|
406
406
|
);
|
407
407
|
|
408
408
|
// This code executes after the commit and re-render of TestComponent.
|
409
409
|
// The commit triggers a re-render, because the item was disposed.
|
410
410
|
expect(render).toHaveBeenCalledTimes(2);
|
411
411
|
expect(factory).toBeCalledTimes(2);
|
412
|
-
}
|
412
|
+
},
|
413
413
|
);
|
414
414
|
|
415
415
|
test(
|
416
|
-
|
417
|
-
|
418
|
-
|
416
|
+
'if, between the render and the commit, the item has been disposed, ' +
|
417
|
+
'and the parent cache is not empty, it will not call factory again, will re-render ' +
|
418
|
+
'an additional time and will call onCommit with the value in the parent cache',
|
419
419
|
async () => {
|
420
420
|
const disposeItem = vi.fn();
|
421
421
|
let factoryCount = 0;
|
@@ -427,7 +427,7 @@ describe("useCachedPrecommitValue", () => {
|
|
427
427
|
const cache = new ParentCache(factory);
|
428
428
|
const getAndPermanentRetainIfPresent = vi.spyOn(
|
429
429
|
cache,
|
430
|
-
|
430
|
+
'getAndPermanentRetainIfPresent',
|
431
431
|
);
|
432
432
|
|
433
433
|
const componentCommits = vi.fn();
|
@@ -443,7 +443,7 @@ describe("useCachedPrecommitValue", () => {
|
|
443
443
|
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
|
444
444
|
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
445
445
|
expect(getAndPermanentRetainIfPresent.mock.results[0].value[0]).toBe(
|
446
|
-
2
|
446
|
+
2,
|
447
447
|
);
|
448
448
|
expect(factory).toHaveBeenCalledTimes(2);
|
449
449
|
expect(hookOnCommit).toHaveBeenCalledTimes(1);
|
@@ -455,7 +455,7 @@ describe("useCachedPrecommitValue", () => {
|
|
455
455
|
|
456
456
|
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
457
457
|
cache,
|
458
|
-
|
458
|
+
'getOrPopulateAndTemporaryRetain',
|
459
459
|
);
|
460
460
|
|
461
461
|
// wat is going on?
|
@@ -499,7 +499,7 @@ describe("useCachedPrecommitValue", () => {
|
|
499
499
|
<TestComponent />
|
500
500
|
<CodeExecutor />
|
501
501
|
</React.Suspense>,
|
502
|
-
false
|
502
|
+
false,
|
503
503
|
);
|
504
504
|
|
505
505
|
// This code executes after the commit and re-render of TestComponent.
|
@@ -508,13 +508,12 @@ describe("useCachedPrecommitValue", () => {
|
|
508
508
|
// Note that this is the same number of calls as inside of CodeExecutor,
|
509
509
|
// implying that the factory function was not called again.
|
510
510
|
expect(factory).toBeCalledTimes(2);
|
511
|
-
}
|
511
|
+
},
|
512
512
|
);
|
513
513
|
|
514
514
|
test(
|
515
|
-
|
516
|
-
|
517
|
-
"temporarily retained",
|
515
|
+
'After render but before commit, the item will ' +
|
516
|
+
'be in the parent cache, temporarily retained',
|
518
517
|
async () => {
|
519
518
|
const disposeItem = vi.fn();
|
520
519
|
const factory = vi.fn(() => {
|
@@ -541,36 +540,21 @@ describe("useCachedPrecommitValue", () => {
|
|
541
540
|
// wat is going on?
|
542
541
|
//
|
543
542
|
// We want to test a scenario where the component unmounts before committing.
|
543
|
+
// However, we cannot distinguish between an unmount before commit and a
|
544
|
+
// render and a commit that hasn't happened yet.
|
544
545
|
//
|
545
|
-
//
|
546
|
-
//
|
547
|
-
//
|
548
|
-
|
549
|
-
setShowChildren(false);
|
550
|
-
return null;
|
551
|
-
}
|
552
|
-
|
553
|
-
let setShowChildren;
|
554
|
-
function ParentComponent({ children }) {
|
555
|
-
const [showChildren, _setShowChildren] = React.useState(true);
|
556
|
-
setShowChildren = _setShowChildren;
|
557
|
-
|
558
|
-
if (showChildren) {
|
559
|
-
return children;
|
560
|
-
} else {
|
561
|
-
return null;
|
562
|
-
}
|
563
|
-
}
|
546
|
+
// This can be simulated with suspense.
|
547
|
+
//
|
548
|
+
// This test and 'on initial render, it should call getOrPopulateAndTemporaryRetain'
|
549
|
+
// can be merged
|
564
550
|
|
551
|
+
const { promise, isResolvedRef } = promiseAndResolver();
|
565
552
|
const element = await awaitableCreate(
|
566
|
-
<
|
553
|
+
<React.Suspense fallback={null}>
|
567
554
|
<TestComponent />
|
568
|
-
<
|
569
|
-
</
|
570
|
-
|
571
|
-
// unmounting. This perhaps is a bug in react-test-renderer. Regardless,
|
572
|
-
// we're not interested in that scenario.
|
573
|
-
true
|
555
|
+
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
556
|
+
</React.Suspense>,
|
557
|
+
true,
|
574
558
|
);
|
575
559
|
|
576
560
|
// This code executes after the commit and re-render of TestComponent.
|
@@ -579,9 +563,9 @@ describe("useCachedPrecommitValue", () => {
|
|
579
563
|
expect(componentCommits).toHaveBeenCalledTimes(0);
|
580
564
|
const item = getItem(cache)!;
|
581
565
|
const state = getState(item);
|
582
|
-
assert(state.kind ===
|
566
|
+
assert(state.kind === 'InParentCacheAndNotDisposed');
|
583
567
|
expect(state.permanentRetainCount).toBe(0);
|
584
568
|
expect(state.temporaryRetainCount).toBe(1);
|
585
|
-
}
|
569
|
+
},
|
586
570
|
);
|
587
571
|
});
|
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
'use strict';
|
2
2
|
|
3
|
-
import { useEffect, useState } from
|
4
|
-
import { ParentCache } from
|
5
|
-
import { useHasCommittedRef } from
|
6
|
-
import { ItemCleanupPair } from
|
3
|
+
import { useEffect, useState } from 'react';
|
4
|
+
import { ParentCache } from './ParentCache';
|
5
|
+
import { useHasCommittedRef } from './useHasCommittedRef';
|
6
|
+
import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
|
7
7
|
|
8
8
|
/**
|
9
9
|
* usePrecommitValue<T>
|
@@ -38,7 +38,7 @@ import { ItemCleanupPair } from "@isograph/isograph-disposable-types/dist";
|
|
38
38
|
*/
|
39
39
|
export function useCachedPrecommitValue<T>(
|
40
40
|
parentCache: ParentCache<T>,
|
41
|
-
onCommit: (pair: ItemCleanupPair<T>) => void
|
41
|
+
onCommit: (pair: ItemCleanupPair<T>) => void,
|
42
42
|
): { state: T } | null {
|
43
43
|
// TODO: there should be two APIs. One in which we always re-render if the
|
44
44
|
// committed item was not returned during the last render, and one in which
|
@@ -66,7 +66,7 @@ export function useCachedPrecommitValue<T>(
|
|
66
66
|
// After the above, we have a non-disposed item and a cleanup function, which we
|
67
67
|
// can pass to onCommit.
|
68
68
|
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(
|
69
|
-
disposeOfTemporaryRetain
|
69
|
+
disposeOfTemporaryRetain,
|
70
70
|
);
|
71
71
|
if (undisposedPair !== null) {
|
72
72
|
onCommit(undisposedPair);
|
@@ -1,12 +1,12 @@
|
|
1
|
-
import { useEffect, useRef } from
|
2
|
-
import { ParentCache } from
|
3
|
-
import { ItemCleanupPair } from
|
4
|
-
import { useCachedPrecommitValue } from
|
1
|
+
import { useEffect, useRef } from 'react';
|
2
|
+
import { ParentCache } from './ParentCache';
|
3
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
4
|
+
import { useCachedPrecommitValue } from './useCachedPrecommitValue';
|
5
5
|
import {
|
6
6
|
UNASSIGNED_STATE,
|
7
7
|
UnassignedState,
|
8
8
|
useUpdatableDisposableState,
|
9
|
-
} from
|
9
|
+
} from './useUpdatableDisposableState';
|
10
10
|
|
11
11
|
type UseUpdatableDisposableStateReturnValue<T> = {
|
12
12
|
state: T;
|
@@ -14,7 +14,7 @@ type UseUpdatableDisposableStateReturnValue<T> = {
|
|
14
14
|
};
|
15
15
|
|
16
16
|
export function useDisposableState<T = never>(
|
17
|
-
parentCache: ParentCache<T
|
17
|
+
parentCache: ParentCache<T>,
|
18
18
|
): UseUpdatableDisposableStateReturnValue<T> {
|
19
19
|
const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);
|
20
20
|
|
@@ -33,13 +33,13 @@ export function useDisposableState<T = never>(
|
|
33
33
|
itemCleanupPairRef.current = null;
|
34
34
|
} else {
|
35
35
|
throw new Error(
|
36
|
-
|
37
|
-
|
36
|
+
'itemCleanupPairRef.current is unexpectedly null. ' +
|
37
|
+
'This indicates a bug in react-disposable-state.',
|
38
38
|
);
|
39
39
|
}
|
40
40
|
}
|
41
41
|
},
|
42
|
-
[stateFromDisposableStateHook]
|
42
|
+
[stateFromDisposableStateHook],
|
43
43
|
);
|
44
44
|
|
45
45
|
useEffect(function cleanupItemCleanupPairRefIfSetStateNotCalled() {
|
@@ -81,12 +81,14 @@ export function useDisposableState<T = never>(
|
|
81
81
|
function tsTests() {
|
82
82
|
let x: any;
|
83
83
|
const a = useDisposableState(x);
|
84
|
+
// This should be a compiler error, because the generic is inferred to be of
|
85
|
+
// type never. TODO determine why this doesn't break the build!
|
84
86
|
// @ts-expect-error
|
85
|
-
a.setState([
|
87
|
+
a.setState(['asdf', () => {}]);
|
86
88
|
// @ts-expect-error
|
87
89
|
a.setState([UNASSIGNED_STATE, () => {}]);
|
88
90
|
const b = useDisposableState<string | UnassignedState>(x);
|
89
91
|
// @ts-expect-error
|
90
92
|
b.setState([UNASSIGNED_STATE, () => {}]);
|
91
|
-
b.setState([
|
93
|
+
b.setState(['asdf', () => {}]);
|
92
94
|
}
|
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
'use strict';
|
2
2
|
|
3
|
-
import { useEffect, useRef } from
|
4
|
-
import type { ItemCleanupPair } from
|
5
|
-
import { ParentCache } from
|
6
|
-
import { useCachedPrecommitValue } from
|
3
|
+
import { useEffect, useRef } from 'react';
|
4
|
+
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
5
|
+
import { ParentCache } from './ParentCache';
|
6
|
+
import { useCachedPrecommitValue } from './useCachedPrecommitValue';
|
7
7
|
|
8
8
|
/**
|
9
9
|
* useLazyDisposableState<T>
|
@@ -28,7 +28,7 @@ export function useLazyDisposableState<T>(parentCache: ParentCache<T>): {
|
|
28
28
|
// TODO confirm useEffect is called in order.
|
29
29
|
if (cleanupFn == null) {
|
30
30
|
throw new Error(
|
31
|
-
|
31
|
+
'cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.',
|
32
32
|
);
|
33
33
|
}
|
34
34
|
return cleanupFn;
|
@@ -43,6 +43,6 @@ export function useLazyDisposableState<T>(parentCache: ParentCache<T>): {
|
|
43
43
|
// is non-null. During the initial commit, we assign itemCleanupPairRef.current,
|
44
44
|
// so during subsequent renders, itemCleanupPairRef.current is non-null.
|
45
45
|
throw new Error(
|
46
|
-
|
46
|
+
'returnedItem was unexpectedly null. This indicates a bug in react-disposable-state.',
|
47
47
|
);
|
48
48
|
}
|