@isograph/react-disposable-state 0.0.4
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 +144 -0
- package/dist/CacheItem.d.ts +54 -0
- package/dist/CacheItem.js +265 -0
- package/dist/ParentCache.d.ts +39 -0
- package/dist/ParentCache.js +86 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +24 -0
- package/dist/useCachedPrecommitValue.d.ts +36 -0
- package/dist/useCachedPrecommitValue.js +93 -0
- package/dist/useDisposableState.d.ts +9 -0
- package/dist/useDisposableState.js +68 -0
- package/dist/useHasCommittedRef.d.ts +5 -0
- package/dist/useHasCommittedRef.js +15 -0
- package/dist/useLazyDisposableState.d.ts +13 -0
- package/dist/useLazyDisposableState.js +39 -0
- package/dist/useUpdatableDisposableState.d.ts +39 -0
- package/dist/useUpdatableDisposableState.js +92 -0
- package/docs/managing-complex-state.md +151 -0
- package/package.json +28 -0
- package/src/CacheItem.test.ts +788 -0
- package/src/CacheItem.ts +364 -0
- package/src/ParentCache.test.ts +70 -0
- package/src/ParentCache.ts +100 -0
- package/src/index.ts +9 -0
- package/src/useCachedPrecommitValue.test.tsx +587 -0
- package/src/useCachedPrecommitValue.ts +104 -0
- package/src/useDisposableState.ts +92 -0
- package/src/useHasCommittedRef.ts +12 -0
- package/src/useLazyDisposableState.ts +48 -0
- package/src/useUpdatableDisposableState.test.tsx +482 -0
- package/src/useUpdatableDisposableState.ts +134 -0
- package/tsconfig.pkg.json +9 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
import { ItemCleanupPair } from "@isograph/disposable-types";
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
3
|
+
import { useHasCommittedRef } from "./useHasCommittedRef";
|
4
|
+
|
5
|
+
export const UNASSIGNED_STATE = Symbol();
|
6
|
+
export type UnassignedState = typeof UNASSIGNED_STATE;
|
7
|
+
|
8
|
+
type UseUpdatableDisposableStateReturnValue<T> = {
|
9
|
+
state: T | UnassignedState;
|
10
|
+
setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;
|
11
|
+
};
|
12
|
+
|
13
|
+
/**
|
14
|
+
* ICI stands for ItemCleanupIndex. Use a short name because it shows up a lot.
|
15
|
+
*
|
16
|
+
* Like an ItemCleanupPair<T>, but with an additional index and as a struct
|
17
|
+
* for readability.
|
18
|
+
*
|
19
|
+
* Interally, we must keep track of the index of the state change. The disposable
|
20
|
+
* items that are set in state are not guaranteed to be distinct according to ===,
|
21
|
+
* as in `setState([1, dispose1]); setState([1, dispose2])`. Following this, we
|
22
|
+
* would expect dispose1 to be called on commit. If state items were compared for
|
23
|
+
* ===, then dispose1 would not be called in such a situation.
|
24
|
+
*
|
25
|
+
* Note that this comes at a runtime cost, and we may want to expose APIs that do
|
26
|
+
* not incur this runtime cost, for example in cases where every disposable item is
|
27
|
+
* distinguishable via ===.
|
28
|
+
*/
|
29
|
+
type ICI<T> = { item: T; cleanup: () => void; index: number };
|
30
|
+
|
31
|
+
/**
|
32
|
+
* useUpdatableDisposableState
|
33
|
+
* - Returns a { state, setItem } object.
|
34
|
+
* - setItem accepts an ItemCleanupPair<T>, and throws if called before commit.
|
35
|
+
* - setItem sets the T in state and adds it to a set.
|
36
|
+
* - React's behavior is that when the hook commits, whatever item is currently
|
37
|
+
* returned from the useState hook is the oldest item which will ever be returned
|
38
|
+
* from that useState hook. (More newly created ones can later be returned with
|
39
|
+
* concurrent mode.)
|
40
|
+
* - When this hook commits, all items up to, but not including, the item currently
|
41
|
+
* returned from the useState hook are disposed and removed from the set.
|
42
|
+
*
|
43
|
+
* Calling setState before the hook commits:
|
44
|
+
* - Calling setState before the hook commits is disallowed because until the hook
|
45
|
+
* commits, React will not schedule any unmount callbacks, meaning that if this
|
46
|
+
* hook never commits, any disposable items passed to setState will never be
|
47
|
+
* disposed.
|
48
|
+
* - We also cannot store them in some cache, because multiple components can share
|
49
|
+
* the same cache location (for example, if they are loading the same query from
|
50
|
+
* multiple components), so updating the cache with the disposable item will cause
|
51
|
+
* both components to show the updated data, which is almost certainly a bug.
|
52
|
+
* - Note that calling setState before commit is probably an anti-pattern! Consider
|
53
|
+
* not doing it.
|
54
|
+
* - If you must, the workaround is to lazily load the disposable item with
|
55
|
+
* useDisposableState or useLazyDisposableState, and update the cache location
|
56
|
+
* instead of calling setState before commit. One can update the cache location
|
57
|
+
* by calling setState on some parent component that has already mounted, and
|
58
|
+
* therefore passing in different props.
|
59
|
+
* - This may only work in concurrent mode, though.
|
60
|
+
*/
|
61
|
+
export function useUpdatableDisposableState<
|
62
|
+
T = never
|
63
|
+
>(): UseUpdatableDisposableStateReturnValue<T> {
|
64
|
+
const hasCommittedRef = useHasCommittedRef();
|
65
|
+
|
66
|
+
const undisposedICIs = useRef(new Set<ICI<T>>());
|
67
|
+
const setStateCountRef = useRef(0);
|
68
|
+
|
69
|
+
const [stateICI, setStateICI] = useState<ICI<T> | UnassignedState>(
|
70
|
+
UNASSIGNED_STATE
|
71
|
+
);
|
72
|
+
|
73
|
+
const setStateAfterCommit = useCallback(
|
74
|
+
(itemCleanupPair: ItemCleanupPair<T>) => {
|
75
|
+
if (!hasCommittedRef.current) {
|
76
|
+
throw new Error(
|
77
|
+
"Calling setState before the component has committed is unsafe and disallowed."
|
78
|
+
);
|
79
|
+
}
|
80
|
+
|
81
|
+
const ici: ICI<T> = {
|
82
|
+
item: itemCleanupPair[0],
|
83
|
+
cleanup: itemCleanupPair[1],
|
84
|
+
index: setStateCountRef.current,
|
85
|
+
};
|
86
|
+
setStateCountRef.current++;
|
87
|
+
undisposedICIs.current.add(ici);
|
88
|
+
setStateICI(ici);
|
89
|
+
},
|
90
|
+
[setStateICI]
|
91
|
+
);
|
92
|
+
|
93
|
+
useEffect(function cleanupUnreachableItems() {
|
94
|
+
const indexInState = stateICI !== UNASSIGNED_STATE ? stateICI.index : 0;
|
95
|
+
|
96
|
+
if (indexInState === 0) {
|
97
|
+
return;
|
98
|
+
}
|
99
|
+
|
100
|
+
for (const undisposedICI of undisposedICIs.current) {
|
101
|
+
if (undisposedICI.index === indexInState) {
|
102
|
+
break;
|
103
|
+
}
|
104
|
+
undisposedICIs.current.delete(undisposedICI);
|
105
|
+
undisposedICI.cleanup();
|
106
|
+
}
|
107
|
+
});
|
108
|
+
|
109
|
+
useEffect(() => {
|
110
|
+
return function disposeAllRemainingItems() {
|
111
|
+
for (const undisposedICI of undisposedICIs.current) {
|
112
|
+
undisposedICI.cleanup();
|
113
|
+
}
|
114
|
+
};
|
115
|
+
}, []);
|
116
|
+
|
117
|
+
return {
|
118
|
+
setState: setStateAfterCommit,
|
119
|
+
state: stateICI !== UNASSIGNED_STATE ? stateICI.item : UNASSIGNED_STATE,
|
120
|
+
};
|
121
|
+
}
|
122
|
+
|
123
|
+
// @ts-ignore
|
124
|
+
function tsTests() {
|
125
|
+
const a = useUpdatableDisposableState();
|
126
|
+
// @ts-expect-error
|
127
|
+
a.setState([UNASSIGNED_STATE, () => {}]);
|
128
|
+
// @ts-expect-error
|
129
|
+
a.setState(["asdf", () => {}]);
|
130
|
+
const b = useUpdatableDisposableState<string | UnassignedState>();
|
131
|
+
// @ts-expect-error
|
132
|
+
b.setState([UNASSIGNED_STATE, () => {}]);
|
133
|
+
b.setState(["asdf", () => {}]);
|
134
|
+
}
|