@isograph/react-disposable-state 0.0.0-main-a8e129cf → 0.0.0-main-1ba744d7
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/.turbo/turbo-compile-libs.log +2 -2
- package/.turbo/turbo-tsc.log +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/useCachedResponsivePrecommitValue.cjs +13 -7
- package/dist/useCachedResponsivePrecommitValue.d.cts.map +1 -1
- package/dist/useCachedResponsivePrecommitValue.d.mts.map +1 -1
- package/dist/useCachedResponsivePrecommitValue.mjs +13 -7
- package/dist/useCachedResponsivePrecommitValue.mjs.map +1 -1
- package/dist/useDisposableState.cjs +7 -6
- package/dist/useDisposableState.mjs +7 -6
- package/dist/useDisposableState.mjs.map +1 -1
- package/dist/useEffectsRerunWithoutRender.cjs +27 -0
- package/dist/useEffectsRerunWithoutRender.d.cts +19 -0
- package/dist/useEffectsRerunWithoutRender.d.cts.map +1 -0
- package/dist/useEffectsRerunWithoutRender.d.mts +19 -0
- package/dist/useEffectsRerunWithoutRender.d.mts.map +1 -0
- package/dist/useEffectsRerunWithoutRender.mjs +28 -0
- package/dist/useEffectsRerunWithoutRender.mjs.map +1 -0
- package/dist/useLazyDisposableState.cjs +3 -3
- package/dist/useLazyDisposableState.mjs +3 -3
- package/dist/useLazyDisposableState.mjs.map +1 -1
- package/dist/useUpdatableDisposableState.cjs +5 -1
- package/dist/useUpdatableDisposableState.mjs +5 -1
- package/dist/useUpdatableDisposableState.mjs.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +1 -0
- package/src/useCachedResponsivePrecommitValue.test.tsx +8 -3
- package/src/useCachedResponsivePrecommitValue.ts +32 -20
- package/src/useDisposableState.ts +13 -15
- package/src/useEffectsRerunWithoutRender.test.tsx +54 -0
- package/src/useEffectsRerunWithoutRender.ts +23 -0
- package/src/useLazyDisposableState.test.tsx +73 -34
- package/src/useLazyDisposableState.ts +3 -4
- package/src/useUpdatableDisposableState.test.tsx +3 -2
- package/src/useUpdatableDisposableState.ts +2 -0
- package/vitest.config.ts +8 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
../.. | WARN Unsupported engine: wanted: {"node":"24.12.0"} (current: {"node":"v24.20.0","pnpm":"10.15.0"})
|
|
2
2
|
|
|
3
|
-
> @isograph/react-disposable-state@0.0.0-main-
|
|
3
|
+
> @isograph/react-disposable-state@0.0.0-main-1ba744d7 compile-libs /home/runner/work/isograph/isograph/libs/isograph-react-disposable-state
|
|
4
4
|
> tsdown
|
|
5
5
|
|
|
6
6
|
[34mℹ[39m tsdown [2mv0.20.1[22m powered by rolldown [2mv1.0.0-rc.1[22m
|
|
7
7
|
[34mℹ[39m config file: [4m/home/runner/work/isograph/isograph/tsdown.config.ts[24m
|
|
8
|
-
(node:
|
|
8
|
+
(node:2646) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/isograph/isograph/tsdown.config.ts?no-cache=c042b2e9-7e41-49bd-a93d-2e3cc4113e4d is not specified and it doesn't parse as CommonJS.
|
|
9
9
|
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
|
|
10
10
|
To eliminate this warning, add "type": "module" to /home/runner/work/isograph/isograph/package.json.
|
|
11
11
|
(Use `node --trace-warnings ...` to show where the warning was created)
|
package/.turbo/turbo-tsc.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
../.. | WARN Unsupported engine: wanted: {"node":"24.12.0"} (current: {"node":"v24.20.0","pnpm":"10.15.0"})
|
|
2
2
|
|
|
3
|
-
> @isograph/react-disposable-state@0.0.0-main-
|
|
3
|
+
> @isograph/react-disposable-state@0.0.0-main-1ba744d7 tsc /home/runner/work/isograph/isograph/libs/isograph-react-disposable-state
|
|
4
4
|
> tsc
|
|
5
5
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_CacheItem = require('./CacheItem.cjs');
|
|
2
2
|
const require_ParentCache = require('./ParentCache.cjs');
|
|
3
|
+
const require_useEffectsRerunWithoutRender = require('./useEffectsRerunWithoutRender.cjs');
|
|
3
4
|
const require_useCachedResponsivePrecommitValue = require('./useCachedResponsivePrecommitValue.cjs');
|
|
4
5
|
const require_useHasCommittedRef = require('./useHasCommittedRef.cjs');
|
|
5
6
|
const require_useUpdatableDisposableState = require('./useUpdatableDisposableState.cjs');
|
|
@@ -12,6 +13,7 @@ exports.UNASSIGNED_STATE = require_useUpdatableDisposableState.UNASSIGNED_STATE;
|
|
|
12
13
|
exports.createTemporarilyRetainedCacheItem = require_CacheItem.createTemporarilyRetainedCacheItem;
|
|
13
14
|
exports.useCachedResponsivePrecommitValue = require_useCachedResponsivePrecommitValue.useCachedResponsivePrecommitValue;
|
|
14
15
|
exports.useDisposableState = require_useDisposableState.useDisposableState;
|
|
16
|
+
exports.useEffectsRerunWithoutRenderRef = require_useEffectsRerunWithoutRender.useEffectsRerunWithoutRenderRef;
|
|
15
17
|
exports.useHasCommittedRef = require_useHasCommittedRef.useHasCommittedRef;
|
|
16
18
|
exports.useLazyDisposableState = require_useLazyDisposableState.useLazyDisposableState;
|
|
17
19
|
exports.useUpdatableDisposableState = require_useUpdatableDisposableState.useUpdatableDisposableState;
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,8 @@ import { ParentCache } from "./ParentCache.cjs";
|
|
|
3
3
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.cjs";
|
|
4
4
|
import { UNASSIGNED_STATE, UnassignedState, useUpdatableDisposableState } from "./useUpdatableDisposableState.cjs";
|
|
5
5
|
import { useDisposableState } from "./useDisposableState.cjs";
|
|
6
|
+
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.cjs";
|
|
6
7
|
import { useHasCommittedRef } from "./useHasCommittedRef.cjs";
|
|
7
8
|
import { useLazyDisposableState } from "./useLazyDisposableState.cjs";
|
|
8
9
|
export * from "@isograph/disposable-types";
|
|
9
|
-
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
|
10
|
+
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
package/dist/index.d.mts
CHANGED
|
@@ -3,7 +3,8 @@ import { ParentCache } from "./ParentCache.mjs";
|
|
|
3
3
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.mjs";
|
|
4
4
|
import { UNASSIGNED_STATE, UnassignedState, useUpdatableDisposableState } from "./useUpdatableDisposableState.mjs";
|
|
5
5
|
import { useDisposableState } from "./useDisposableState.mjs";
|
|
6
|
+
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.mjs";
|
|
6
7
|
import { useHasCommittedRef } from "./useHasCommittedRef.mjs";
|
|
7
8
|
import { useLazyDisposableState } from "./useLazyDisposableState.mjs";
|
|
8
9
|
export * from "@isograph/disposable-types";
|
|
9
|
-
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
|
10
|
+
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CacheItem, createTemporarilyRetainedCacheItem } from "./CacheItem.mjs";
|
|
2
2
|
import { ParentCache } from "./ParentCache.mjs";
|
|
3
|
+
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.mjs";
|
|
3
4
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.mjs";
|
|
4
5
|
import { useHasCommittedRef } from "./useHasCommittedRef.mjs";
|
|
5
6
|
import { UNASSIGNED_STATE, useUpdatableDisposableState } from "./useUpdatableDisposableState.mjs";
|
|
@@ -8,4 +9,4 @@ import { useLazyDisposableState } from "./useLazyDisposableState.mjs";
|
|
|
8
9
|
|
|
9
10
|
export * from "@isograph/disposable-types"
|
|
10
11
|
|
|
11
|
-
export { CacheItem, ParentCache, UNASSIGNED_STATE, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
|
12
|
+
export { CacheItem, ParentCache, UNASSIGNED_STATE, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableState };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const require_useEffectsRerunWithoutRender = require('./useEffectsRerunWithoutRender.cjs');
|
|
3
4
|
let react = require("react");
|
|
4
5
|
|
|
5
6
|
//#region src/useCachedResponsivePrecommitValue.ts
|
|
@@ -38,16 +39,21 @@ let react = require("react");
|
|
|
38
39
|
function useCachedResponsivePrecommitValue(parentCache, onCommit) {
|
|
39
40
|
const [, rerender] = (0, react.useState)(null);
|
|
40
41
|
const lastCommittedParentCache = (0, react.useRef)(null);
|
|
42
|
+
const effectsRerunWithoutRenderRef = require_useEffectsRerunWithoutRender.useEffectsRerunWithoutRenderRef();
|
|
41
43
|
(0, react.useEffect)(() => {
|
|
42
44
|
lastCommittedParentCache.current = parentCache;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
rerender({});
|
|
45
|
+
if (!effectsRerunWithoutRenderRef.current) {
|
|
46
|
+
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(disposeOfTemporaryRetain);
|
|
47
|
+
if (undisposedPair != null) {
|
|
48
|
+
onCommit(undisposedPair);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
50
51
|
}
|
|
52
|
+
effectsRerunWithoutRenderRef.current = false;
|
|
53
|
+
const existingCacheItemCleanupPair = parentCache.getAndPermanentRetainIfPresent();
|
|
54
|
+
if (existingCacheItemCleanupPair != null) onCommit(existingCacheItemCleanupPair);
|
|
55
|
+
else onCommit(parentCache.factory());
|
|
56
|
+
rerender({});
|
|
51
57
|
}, [parentCache]);
|
|
52
58
|
if (lastCommittedParentCache.current === parentCache) return null;
|
|
53
59
|
const [cacheItem, item, disposeOfTemporaryRetain] = parentCache.getOrPopulateAndTemporaryRetain();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCachedResponsivePrecommitValue.d.cts","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"useCachedResponsivePrecommitValue.d.cts","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iCAAA,GAAA,CACd,WAAA,EAAa,WAAA,CAAY,CAAA,GACzB,QAAA,GAAW,IAAA,EAAM,eAAA,CAAgB,CAAA;EAC9B,KAAA,EAAO,CAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCachedResponsivePrecommitValue.d.mts","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"useCachedResponsivePrecommitValue.d.mts","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iCAAA,GAAA,CACd,WAAA,EAAa,WAAA,CAAY,CAAA,GACzB,QAAA,GAAW,IAAA,EAAM,eAAA,CAAgB,CAAA;EAC9B,KAAA,EAAO,CAAA;AAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.mjs";
|
|
1
2
|
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
|
|
3
4
|
//#region src/useCachedResponsivePrecommitValue.ts
|
|
@@ -36,16 +37,21 @@ import { useEffect, useRef, useState } from "react";
|
|
|
36
37
|
function useCachedResponsivePrecommitValue(parentCache, onCommit) {
|
|
37
38
|
const [, rerender] = useState(null);
|
|
38
39
|
const lastCommittedParentCache = useRef(null);
|
|
40
|
+
const effectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef();
|
|
39
41
|
useEffect(() => {
|
|
40
42
|
lastCommittedParentCache.current = parentCache;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
rerender({});
|
|
43
|
+
if (!effectsRerunWithoutRenderRef.current) {
|
|
44
|
+
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(disposeOfTemporaryRetain);
|
|
45
|
+
if (undisposedPair != null) {
|
|
46
|
+
onCommit(undisposedPair);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
48
49
|
}
|
|
50
|
+
effectsRerunWithoutRenderRef.current = false;
|
|
51
|
+
const existingCacheItemCleanupPair = parentCache.getAndPermanentRetainIfPresent();
|
|
52
|
+
if (existingCacheItemCleanupPair != null) onCommit(existingCacheItemCleanupPair);
|
|
53
|
+
else onCommit(parentCache.factory());
|
|
54
|
+
rerender({});
|
|
49
55
|
}, [parentCache]);
|
|
50
56
|
if (lastCommittedParentCache.current === parentCache) return null;
|
|
51
57
|
const [cacheItem, item, disposeOfTemporaryRetain] = parentCache.getOrPopulateAndTemporaryRetain();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCachedResponsivePrecommitValue.mjs","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"sourcesContent":["'use strict';\n\nimport type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef, useState } from 'react';\nimport type { ParentCache } from './ParentCache';\n\n/**\n * useCachedResponsivePrecommitValue<T>\n * - Takes a mutable parent cache, a factory function, and an onCommit callback.\n * - Returns T before commit after every parent cache change, and null afterward.\n * - Calls onCommit with the ItemCleanupPair during commit after every parent cache change.\n * - The T from the render phase is only temporarily retained. It may have been\n * disposed by the time of the commit. If so, this hook checks the parent cache\n * for another T or creates one, and passes this T to onCommit.\n * - If the T returned during the last render is not the same as the one that\n * is passed to onCommit, during the commit phase, it will schedule another render.\n *\n * Invariant: the returned T has not been disposed during the tick of the render.\n * The T passed to the onCommit callback has not been disposed when the onCommit\n * callback is called.\n *\n * Passing a different parentCache:\n * - Pre-commit, passing a different parentCache has the effect of \"resetting\" this\n * hook's state to the new cache's state. For example, if you have a cache associated\n * with a set of variables (e.g. {name: \"Matthew\"}), and pass in another cache\n * (e.g. associated with {name: \"James\"}), which is empty, the hook will fill that\n * new cache with the factory function.\n * - Post-commit, passing a different parentCache will reset hook to the pre-commit\n * state. The cache will return T before commit, then fill the new cache with the\n * factory function and return null afterwards.\n *\n * Passing a different factory:\n * - Passing a different factory has no effect, except when factory is called,\n * which is when the parent cache is being filled, or during commit.\n *\n * Passing a different onCommit:\n * - Passing a different onCommit has no effect, except for during commit.\n */\nexport function useCachedResponsivePrecommitValue<T>(\n parentCache: ParentCache<T>,\n onCommit: (pair: ItemCleanupPair<T>) => void,\n): { state: T } | null {\n // TODO: there should be two APIs. One in which we always re-render if the\n // committed item was not returned during the last render, and one in which\n // we do not. The latter is useful for cases where every disposable item\n // behaves identically, but must be loaded.\n //\n // This hook is the former, i.e. re-renders if the committed item has changed.\n const [, rerender] = useState<{} | null>(null);\n const lastCommittedParentCache = useRef<ParentCache<T> | null>(null);\n\n useEffect(() => {\n lastCommittedParentCache.current = parentCache;\n // On commit, cacheItem may be disposed, because during the render phase,\n // we only temporarily retained the item, and the temporary retain could have\n // expired by the time of the commit.\n //\n // So, we can be in one of two states:\n // - the item is not disposed. In that case, permanently retain and use that item.\n // - the item is disposed. In that case, we can be in two states:\n // - the parent cache is not empty (due to another component rendering, or\n // another render of the same component.) In that case, permanently retain and\n // use the item from the parent cache. (Note: any item present in the parent\n // cache is not disposed.)\n // - the parent cache is empty. In that case, call factory, getting a new item\n // and a cleanup function.\n //\n // After the above, we have a non-disposed item and a cleanup function, which we\n // can pass to onCommit.\n const undisposedPair = cacheItem.permanentRetainIfNotDisposed(\n
|
|
1
|
+
{"version":3,"file":"useCachedResponsivePrecommitValue.mjs","names":[],"sources":["../src/useCachedResponsivePrecommitValue.ts"],"sourcesContent":["'use strict';\n\nimport type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef, useState } from 'react';\nimport type { ParentCache } from './ParentCache';\nimport { useEffectsRerunWithoutRenderRef } from './useEffectsRerunWithoutRender';\n\n/**\n * useCachedResponsivePrecommitValue<T>\n * - Takes a mutable parent cache, a factory function, and an onCommit callback.\n * - Returns T before commit after every parent cache change, and null afterward.\n * - Calls onCommit with the ItemCleanupPair during commit after every parent cache change.\n * - The T from the render phase is only temporarily retained. It may have been\n * disposed by the time of the commit. If so, this hook checks the parent cache\n * for another T or creates one, and passes this T to onCommit.\n * - If the T returned during the last render is not the same as the one that\n * is passed to onCommit, during the commit phase, it will schedule another render.\n *\n * Invariant: the returned T has not been disposed during the tick of the render.\n * The T passed to the onCommit callback has not been disposed when the onCommit\n * callback is called.\n *\n * Passing a different parentCache:\n * - Pre-commit, passing a different parentCache has the effect of \"resetting\" this\n * hook's state to the new cache's state. For example, if you have a cache associated\n * with a set of variables (e.g. {name: \"Matthew\"}), and pass in another cache\n * (e.g. associated with {name: \"James\"}), which is empty, the hook will fill that\n * new cache with the factory function.\n * - Post-commit, passing a different parentCache will reset hook to the pre-commit\n * state. The cache will return T before commit, then fill the new cache with the\n * factory function and return null afterwards.\n *\n * Passing a different factory:\n * - Passing a different factory has no effect, except when factory is called,\n * which is when the parent cache is being filled, or during commit.\n *\n * Passing a different onCommit:\n * - Passing a different onCommit has no effect, except for during commit.\n */\nexport function useCachedResponsivePrecommitValue<T>(\n parentCache: ParentCache<T>,\n onCommit: (pair: ItemCleanupPair<T>) => void,\n): { state: T } | null {\n // TODO: there should be two APIs. One in which we always re-render if the\n // committed item was not returned during the last render, and one in which\n // we do not. The latter is useful for cases where every disposable item\n // behaves identically, but must be loaded.\n //\n // This hook is the former, i.e. re-renders if the committed item has changed.\n const [, rerender] = useState<{} | null>(null);\n const lastCommittedParentCache = useRef<ParentCache<T> | null>(null);\n\n const effectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef();\n\n useEffect(() => {\n lastCommittedParentCache.current = parentCache;\n // On commit, cacheItem may be disposed, because during the render phase,\n // we only temporarily retained the item, and the temporary retain could have\n // expired by the time of the commit.\n //\n // So, we can be in one of two states:\n // - the item is not disposed. In that case, permanently retain and use that item.\n // - the item is disposed. In that case, we can be in two states:\n // - the parent cache is not empty (due to another component rendering, or\n // another render of the same component.) In that case, permanently retain and\n // use the item from the parent cache. (Note: any item present in the parent\n // cache is not disposed.)\n // - the parent cache is empty. In that case, call factory, getting a new item\n // and a cleanup function.\n //\n // After the above, we have a non-disposed item and a cleanup function, which we\n // can pass to onCommit.\n //\n // When this effect runs again without a render (StrictMode's simulated\n // remount, Fast Refresh, Activity), the render's temporary retain was\n // already converted by the first run and released by the cleanup, so the\n // item is re-acquired from the parent cache below instead.\n if (!effectsRerunWithoutRenderRef.current) {\n const undisposedPair = cacheItem.permanentRetainIfNotDisposed(\n disposeOfTemporaryRetain,\n );\n if (undisposedPair != null) {\n onCommit(undisposedPair);\n return;\n }\n }\n effectsRerunWithoutRenderRef.current = false;\n\n // The cache item we created during render has been disposed. Check if the parent\n // cache is populated.\n const existingCacheItemCleanupPair =\n parentCache.getAndPermanentRetainIfPresent();\n if (existingCacheItemCleanupPair != null) {\n onCommit(existingCacheItemCleanupPair);\n } else {\n // We did not find an item in the parent cache, create a new one.\n onCommit(parentCache.factory());\n }\n // TODO: Consider whether we always want to rerender if the committed item\n // was not returned during the last render, or whether some callers will\n // prefer opting out of this behavior (e.g. if every disposable item behaves\n // identically, but must be loaded.)\n rerender({});\n }, [parentCache]);\n\n if (lastCommittedParentCache.current === parentCache) {\n return null;\n }\n\n // Safety: item is only safe to use (i.e. guaranteed not to have been disposed)\n // during this tick.\n const [cacheItem, item, disposeOfTemporaryRetain] =\n parentCache.getOrPopulateAndTemporaryRetain();\n\n return { state: item };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kCACd,aACA,UACqB;CAOrB,MAAM,GAAG,YAAY,SAAoB,KAAK;CAC9C,MAAM,2BAA2B,OAA8B,KAAK;CAEpE,MAAM,+BAA+B,iCAAiC;AAEtE,iBAAgB;AACd,2BAAyB,UAAU;AAsBnC,MAAI,CAAC,6BAA6B,SAAS;GACzC,MAAM,iBAAiB,UAAU,6BAC/B,yBACD;AACD,OAAI,kBAAkB,MAAM;AAC1B,aAAS,eAAe;AACxB;;;AAGJ,+BAA6B,UAAU;EAIvC,MAAM,+BACJ,YAAY,gCAAgC;AAC9C,MAAI,gCAAgC,KAClC,UAAS,6BAA6B;MAGtC,UAAS,YAAY,SAAS,CAAC;AAMjC,WAAS,EAAE,CAAC;IACX,CAAC,YAAY,CAAC;AAEjB,KAAI,yBAAyB,YAAY,YACvC,QAAO;CAKT,MAAM,CAAC,WAAW,MAAM,4BACtB,YAAY,iCAAiC;AAE/C,QAAO,EAAE,OAAO,MAAM"}
|
|
@@ -6,15 +6,16 @@ let react = require("react");
|
|
|
6
6
|
function useDisposableState(parentCache) {
|
|
7
7
|
const itemCleanupPairRef = (0, react.useRef)(null);
|
|
8
8
|
const preCommitItem = require_useCachedResponsivePrecommitValue.useCachedResponsivePrecommitValue(parentCache, (pair) => {
|
|
9
|
-
itemCleanupPairRef.current?.[1]();
|
|
10
9
|
itemCleanupPairRef.current = pair;
|
|
11
10
|
});
|
|
12
11
|
const { state: stateFromDisposableStateHook, setState } = require_useUpdatableDisposableState.useUpdatableDisposableState();
|
|
13
12
|
(0, react.useEffect)(function cleanupItemCleanupPairRefAfterSetState() {
|
|
14
|
-
if (stateFromDisposableStateHook !== require_useUpdatableDisposableState.UNASSIGNED_STATE)
|
|
15
|
-
itemCleanupPairRef.current
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
if (stateFromDisposableStateHook !== require_useUpdatableDisposableState.UNASSIGNED_STATE) {
|
|
14
|
+
if (itemCleanupPairRef.current != null) {
|
|
15
|
+
itemCleanupPairRef.current[1]();
|
|
16
|
+
itemCleanupPairRef.current = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
18
19
|
}, [stateFromDisposableStateHook]);
|
|
19
20
|
(0, react.useEffect)(function cleanupItemCleanupPairRefIfSetStateNotCalled() {
|
|
20
21
|
return () => {
|
|
@@ -23,7 +24,7 @@ function useDisposableState(parentCache) {
|
|
|
23
24
|
itemCleanupPairRef.current = null;
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
|
-
}, []);
|
|
27
|
+
}, [parentCache]);
|
|
27
28
|
const state = (stateFromDisposableStateHook !== require_useUpdatableDisposableState.UNASSIGNED_STATE ? stateFromDisposableStateHook : null) ?? preCommitItem?.state ?? itemCleanupPairRef.current?.[0];
|
|
28
29
|
if (state != null) return {
|
|
29
30
|
state,
|
|
@@ -6,15 +6,16 @@ import { useEffect, useRef } from "react";
|
|
|
6
6
|
function useDisposableState(parentCache) {
|
|
7
7
|
const itemCleanupPairRef = useRef(null);
|
|
8
8
|
const preCommitItem = useCachedResponsivePrecommitValue(parentCache, (pair) => {
|
|
9
|
-
itemCleanupPairRef.current?.[1]();
|
|
10
9
|
itemCleanupPairRef.current = pair;
|
|
11
10
|
});
|
|
12
11
|
const { state: stateFromDisposableStateHook, setState } = useUpdatableDisposableState();
|
|
13
12
|
useEffect(function cleanupItemCleanupPairRefAfterSetState() {
|
|
14
|
-
if (stateFromDisposableStateHook !== UNASSIGNED_STATE)
|
|
15
|
-
itemCleanupPairRef.current
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
if (stateFromDisposableStateHook !== UNASSIGNED_STATE) {
|
|
14
|
+
if (itemCleanupPairRef.current != null) {
|
|
15
|
+
itemCleanupPairRef.current[1]();
|
|
16
|
+
itemCleanupPairRef.current = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
18
19
|
}, [stateFromDisposableStateHook]);
|
|
19
20
|
useEffect(function cleanupItemCleanupPairRefIfSetStateNotCalled() {
|
|
20
21
|
return () => {
|
|
@@ -23,7 +24,7 @@ function useDisposableState(parentCache) {
|
|
|
23
24
|
itemCleanupPairRef.current = null;
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
|
-
}, []);
|
|
27
|
+
}, [parentCache]);
|
|
27
28
|
const state = (stateFromDisposableStateHook !== UNASSIGNED_STATE ? stateFromDisposableStateHook : null) ?? preCommitItem?.state ?? itemCleanupPairRef.current?.[0];
|
|
28
29
|
if (state != null) return {
|
|
29
30
|
state,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDisposableState.mjs","names":[],"sources":["../src/useDisposableState.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef } from 'react';\nimport type { ParentCache } from './ParentCache';\nimport { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';\nimport {\n UNASSIGNED_STATE,\n type UnassignedState,\n useUpdatableDisposableState,\n} from './useUpdatableDisposableState';\n\ntype UseUpdatableDisposableStateReturnValue<T> = {\n state: T;\n setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;\n};\n\nexport function useDisposableState<T = never>(\n parentCache: ParentCache<T>,\n): UseUpdatableDisposableStateReturnValue<T> {\n const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);\n\n const preCommitItem = useCachedResponsivePrecommitValue(\n parentCache,\n (pair) => {\n itemCleanupPairRef.current
|
|
1
|
+
{"version":3,"file":"useDisposableState.mjs","names":[],"sources":["../src/useDisposableState.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef } from 'react';\nimport type { ParentCache } from './ParentCache';\nimport { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';\nimport {\n UNASSIGNED_STATE,\n type UnassignedState,\n useUpdatableDisposableState,\n} from './useUpdatableDisposableState';\n\ntype UseUpdatableDisposableStateReturnValue<T> = {\n state: T;\n setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;\n};\n\nexport function useDisposableState<T = never>(\n parentCache: ParentCache<T>,\n): UseUpdatableDisposableStateReturnValue<T> {\n const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);\n\n const preCommitItem = useCachedResponsivePrecommitValue(\n parentCache,\n (pair) => {\n itemCleanupPairRef.current = pair;\n },\n );\n\n const { state: stateFromDisposableStateHook, setState } =\n useUpdatableDisposableState<T>();\n\n useEffect(\n function cleanupItemCleanupPairRefAfterSetState() {\n if (stateFromDisposableStateHook !== UNASSIGNED_STATE) {\n if (itemCleanupPairRef.current != null) {\n itemCleanupPairRef.current[1]();\n itemCleanupPairRef.current = null;\n }\n }\n },\n [stateFromDisposableStateHook],\n );\n\n useEffect(\n function cleanupItemCleanupPairRefIfSetStateNotCalled() {\n return () => {\n if (itemCleanupPairRef.current != null) {\n itemCleanupPairRef.current[1]();\n // Forgotten so that nothing can release this pair a second time\n // before an effect re-run (StrictMode, Fast Refresh) stores a new one.\n itemCleanupPairRef.current = null;\n }\n };\n },\n [parentCache],\n );\n const state: T | undefined =\n (stateFromDisposableStateHook !== UNASSIGNED_STATE\n ? stateFromDisposableStateHook\n : null) ??\n preCommitItem?.state ??\n itemCleanupPairRef.current?.[0];\n\n if (state != null) {\n return {\n state: state,\n setState,\n };\n }\n // Safety: we can be in one of three states. Pre-commit, in which case\n // preCommitItem is assigned, post-commit but before setState has been\n // called, in which case itemCleanupPairRef.current is assigned, or\n // after setState has been called, in which case\n // stateFromDisposableStateHook is assigned.\n //\n // Therefore, the type of state is T, not T | undefined. But the fact\n // that we are in one of the three states is not reflected in the types.\n // So we have to cast to T.\n //\n // Note that in the post-commit post-setState state, itemCleanupPairRef\n // can still be assigned, during the render before the\n // cleanupItemCleanupPairRefAfterSetState effect is called.\n throw new Error(\n 'state was unexpectedly null. This indicates a bug in react-disposable-state.',\n );\n}\n\n// @ts-ignore\nfunction tsTests() {\n let x: any;\n const a = useDisposableState(x);\n // This should be a compiler error, because the generic is inferred to be of\n // type never. TODO determine why this doesn't break the build!\n // @ts-expect-error\n a.setState(['asdf', () => {}]);\n // @ts-expect-error\n a.setState([UNASSIGNED_STATE, () => {}]);\n const b = useDisposableState<string | UnassignedState>(x);\n // @ts-expect-error\n b.setState([UNASSIGNED_STATE, () => {}]);\n b.setState(['asdf', () => {}]);\n}\n"],"mappings":";;;;;AAeA,SAAgB,mBACd,aAC2C;CAC3C,MAAM,qBAAqB,OAAkC,KAAK;CAElE,MAAM,gBAAgB,kCACpB,cACC,SAAS;AACR,qBAAmB,UAAU;GAEhC;CAED,MAAM,EAAE,OAAO,8BAA8B,aAC3C,6BAAgC;AAElC,WACE,SAAS,yCAAyC;AAChD,MAAI,iCAAiC,kBACnC;OAAI,mBAAmB,WAAW,MAAM;AACtC,uBAAmB,QAAQ,IAAI;AAC/B,uBAAmB,UAAU;;;IAInC,CAAC,6BAA6B,CAC/B;AAED,WACE,SAAS,+CAA+C;AACtD,eAAa;AACX,OAAI,mBAAmB,WAAW,MAAM;AACtC,uBAAmB,QAAQ,IAAI;AAG/B,uBAAmB,UAAU;;;IAInC,CAAC,YAAY,CACd;CACD,MAAM,SACH,iCAAiC,mBAC9B,+BACA,SACJ,eAAe,SACf,mBAAmB,UAAU;AAE/B,KAAI,SAAS,KACX,QAAO;EACE;EACP;EACD;AAeH,OAAM,IAAI,MACR,+EACD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
let react = require("react");
|
|
2
|
+
|
|
3
|
+
//#region src/useEffectsRerunWithoutRender.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects that React is running this component's effects again without a
|
|
6
|
+
* render in between. That happens on StrictMode's simulated unmount and
|
|
7
|
+
* remount on the first commit, on Fast Refresh, and when an `Activity`
|
|
8
|
+
* boundary hides and then shows the component. In each case React runs every
|
|
9
|
+
* effect's cleanup and then its mount, so a cleanup that sets a flag is
|
|
10
|
+
* observable from the mounts that follow.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref whose `current` is `true` from the moment the cleanup ran until
|
|
13
|
+
* the caller resets it. The effect that must tell a re-run apart from a first
|
|
14
|
+
* mount reads it and sets it back to `false`; nothing else clears it.
|
|
15
|
+
*/
|
|
16
|
+
function useEffectsRerunWithoutRenderRef() {
|
|
17
|
+
const effectsRerunWithoutRenderRef = (0, react.useRef)(false);
|
|
18
|
+
(0, react.useEffect)(() => {
|
|
19
|
+
return () => {
|
|
20
|
+
effectsRerunWithoutRenderRef.current = true;
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
return effectsRerunWithoutRenderRef;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
exports.useEffectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MutableRefObject } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useEffectsRerunWithoutRender.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects that React is running this component's effects again without a
|
|
6
|
+
* render in between. That happens on StrictMode's simulated unmount and
|
|
7
|
+
* remount on the first commit, on Fast Refresh, and when an `Activity`
|
|
8
|
+
* boundary hides and then shows the component. In each case React runs every
|
|
9
|
+
* effect's cleanup and then its mount, so a cleanup that sets a flag is
|
|
10
|
+
* observable from the mounts that follow.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref whose `current` is `true` from the moment the cleanup ran until
|
|
13
|
+
* the caller resets it. The effect that must tell a re-run apart from a first
|
|
14
|
+
* mount reads it and sets it back to `false`; nothing else clears it.
|
|
15
|
+
*/
|
|
16
|
+
declare function useEffectsRerunWithoutRenderRef(): MutableRefObject<boolean>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { useEffectsRerunWithoutRenderRef };
|
|
19
|
+
//# sourceMappingURL=useEffectsRerunWithoutRender.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectsRerunWithoutRender.d.cts","names":[],"sources":["../src/useEffectsRerunWithoutRender.ts"],"mappings":";;;;;AAcA;;;;;;;;;;iBAAgB,+BAAA,CAAA,GAAmC,gBAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MutableRefObject } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useEffectsRerunWithoutRender.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects that React is running this component's effects again without a
|
|
6
|
+
* render in between. That happens on StrictMode's simulated unmount and
|
|
7
|
+
* remount on the first commit, on Fast Refresh, and when an `Activity`
|
|
8
|
+
* boundary hides and then shows the component. In each case React runs every
|
|
9
|
+
* effect's cleanup and then its mount, so a cleanup that sets a flag is
|
|
10
|
+
* observable from the mounts that follow.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref whose `current` is `true` from the moment the cleanup ran until
|
|
13
|
+
* the caller resets it. The effect that must tell a re-run apart from a first
|
|
14
|
+
* mount reads it and sets it back to `false`; nothing else clears it.
|
|
15
|
+
*/
|
|
16
|
+
declare function useEffectsRerunWithoutRenderRef(): MutableRefObject<boolean>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { useEffectsRerunWithoutRenderRef };
|
|
19
|
+
//# sourceMappingURL=useEffectsRerunWithoutRender.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectsRerunWithoutRender.d.mts","names":[],"sources":["../src/useEffectsRerunWithoutRender.ts"],"mappings":";;;;;AAcA;;;;;;;;;;iBAAgB,+BAAA,CAAA,GAAmC,gBAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useEffectsRerunWithoutRender.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects that React is running this component's effects again without a
|
|
6
|
+
* render in between. That happens on StrictMode's simulated unmount and
|
|
7
|
+
* remount on the first commit, on Fast Refresh, and when an `Activity`
|
|
8
|
+
* boundary hides and then shows the component. In each case React runs every
|
|
9
|
+
* effect's cleanup and then its mount, so a cleanup that sets a flag is
|
|
10
|
+
* observable from the mounts that follow.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref whose `current` is `true` from the moment the cleanup ran until
|
|
13
|
+
* the caller resets it. The effect that must tell a re-run apart from a first
|
|
14
|
+
* mount reads it and sets it back to `false`; nothing else clears it.
|
|
15
|
+
*/
|
|
16
|
+
function useEffectsRerunWithoutRenderRef() {
|
|
17
|
+
const effectsRerunWithoutRenderRef = useRef(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
return () => {
|
|
20
|
+
effectsRerunWithoutRenderRef.current = true;
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
return effectsRerunWithoutRenderRef;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { useEffectsRerunWithoutRenderRef };
|
|
28
|
+
//# sourceMappingURL=useEffectsRerunWithoutRender.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectsRerunWithoutRender.mjs","names":[],"sources":["../src/useEffectsRerunWithoutRender.ts"],"sourcesContent":["import { type MutableRefObject, useEffect, useRef } from 'react';\n\n/**\n * Detects that React is running this component's effects again without a\n * render in between. That happens on StrictMode's simulated unmount and\n * remount on the first commit, on Fast Refresh, and when an `Activity`\n * boundary hides and then shows the component. In each case React runs every\n * effect's cleanup and then its mount, so a cleanup that sets a flag is\n * observable from the mounts that follow.\n *\n * Returns a ref whose `current` is `true` from the moment the cleanup ran until\n * the caller resets it. The effect that must tell a re-run apart from a first\n * mount reads it and sets it back to `false`; nothing else clears it.\n */\nexport function useEffectsRerunWithoutRenderRef(): MutableRefObject<boolean> {\n const effectsRerunWithoutRenderRef = useRef(false);\n useEffect(() => {\n return () => {\n effectsRerunWithoutRenderRef.current = true;\n };\n }, []);\n return effectsRerunWithoutRenderRef;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,SAAgB,kCAA6D;CAC3E,MAAM,+BAA+B,OAAO,MAAM;AAClD,iBAAgB;AACd,eAAa;AACX,gCAA6B,UAAU;;IAExC,EAAE,CAAC;AACN,QAAO"}
|
|
@@ -16,16 +16,16 @@ let react = require("react");
|
|
|
16
16
|
function useLazyDisposableState(parentCache) {
|
|
17
17
|
const itemCleanupPairRef = (0, react.useRef)(null);
|
|
18
18
|
const preCommitItem = require_useCachedResponsivePrecommitValue.useCachedResponsivePrecommitValue(parentCache, (pair) => {
|
|
19
|
-
itemCleanupPairRef.current?.[1]();
|
|
20
19
|
itemCleanupPairRef.current = pair;
|
|
21
20
|
});
|
|
22
21
|
(0, react.useEffect)(() => {
|
|
23
22
|
return () => {
|
|
24
23
|
const cleanupFn = itemCleanupPairRef.current?.[1];
|
|
25
24
|
if (cleanupFn == null) throw new Error("cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.");
|
|
26
|
-
|
|
25
|
+
cleanupFn();
|
|
26
|
+
itemCleanupPairRef.current = null;
|
|
27
27
|
};
|
|
28
|
-
}, []);
|
|
28
|
+
}, [parentCache]);
|
|
29
29
|
const returnedItem = preCommitItem?.state ?? itemCleanupPairRef.current?.[0];
|
|
30
30
|
if (returnedItem != null) return { state: returnedItem };
|
|
31
31
|
throw new Error("returnedItem was unexpectedly null. This indicates a bug in react-disposable-state.");
|
|
@@ -14,16 +14,16 @@ import { useEffect, useRef } from "react";
|
|
|
14
14
|
function useLazyDisposableState(parentCache) {
|
|
15
15
|
const itemCleanupPairRef = useRef(null);
|
|
16
16
|
const preCommitItem = useCachedResponsivePrecommitValue(parentCache, (pair) => {
|
|
17
|
-
itemCleanupPairRef.current?.[1]();
|
|
18
17
|
itemCleanupPairRef.current = pair;
|
|
19
18
|
});
|
|
20
19
|
useEffect(() => {
|
|
21
20
|
return () => {
|
|
22
21
|
const cleanupFn = itemCleanupPairRef.current?.[1];
|
|
23
22
|
if (cleanupFn == null) throw new Error("cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.");
|
|
24
|
-
|
|
23
|
+
cleanupFn();
|
|
24
|
+
itemCleanupPairRef.current = null;
|
|
25
25
|
};
|
|
26
|
-
}, []);
|
|
26
|
+
}, [parentCache]);
|
|
27
27
|
const returnedItem = preCommitItem?.state ?? itemCleanupPairRef.current?.[0];
|
|
28
28
|
if (returnedItem != null) return { state: returnedItem };
|
|
29
29
|
throw new Error("returnedItem was unexpectedly null. This indicates a bug in react-disposable-state.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLazyDisposableState.mjs","names":[],"sources":["../src/useLazyDisposableState.ts"],"sourcesContent":["'use strict';\n\nimport type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef } from 'react';\nimport type { ParentCache } from './ParentCache';\nimport { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';\nimport { type UnassignedState } from './useUpdatableDisposableState';\n\n/**\n * useLazyDisposableState<T>\n * - Takes a mutable parent cache and a factory function\n * - Returns { state: T }\n *\n * This lazily loads the disposable item using useCachedResponsivePrecommitValue, then\n * (on commit) sets it in state. The item continues to be returned after\n * commit and is disposed when the hook unmounts.\n */\nexport function useLazyDisposableState<T>(\n parentCache: ParentCache<Exclude<T, UnassignedState>>,\n): {\n state: T;\n} {\n const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);\n
|
|
1
|
+
{"version":3,"file":"useLazyDisposableState.mjs","names":[],"sources":["../src/useLazyDisposableState.ts"],"sourcesContent":["'use strict';\n\nimport type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useEffect, useRef } from 'react';\nimport type { ParentCache } from './ParentCache';\nimport { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';\nimport { type UnassignedState } from './useUpdatableDisposableState';\n\n/**\n * useLazyDisposableState<T>\n * - Takes a mutable parent cache and a factory function\n * - Returns { state: T }\n *\n * This lazily loads the disposable item using useCachedResponsivePrecommitValue, then\n * (on commit) sets it in state. The item continues to be returned after\n * commit and is disposed when the hook unmounts.\n */\nexport function useLazyDisposableState<T>(\n parentCache: ParentCache<Exclude<T, UnassignedState>>,\n): {\n state: T;\n} {\n const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);\n const preCommitItem = useCachedResponsivePrecommitValue(\n parentCache,\n (pair) => {\n itemCleanupPairRef.current = pair;\n },\n );\n\n useEffect(() => {\n return () => {\n const cleanupFn = itemCleanupPairRef.current?.[1];\n // TODO confirm useEffect is called in order.\n if (cleanupFn == null) {\n throw new Error(\n 'cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.',\n );\n }\n cleanupFn();\n itemCleanupPairRef.current = null;\n };\n }, [parentCache]);\n\n const returnedItem = preCommitItem?.state ?? itemCleanupPairRef.current?.[0];\n\n if (returnedItem != null) {\n return { state: returnedItem };\n }\n\n // Safety: This can't happen. For renders before the initial commit, preCommitItem\n // is non-null. During the initial commit, we assign itemCleanupPairRef.current,\n // so during subsequent renders, itemCleanupPairRef.current is non-null.\n throw new Error(\n 'returnedItem was unexpectedly null. This indicates a bug in react-disposable-state.',\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,uBACd,aAGA;CACA,MAAM,qBAAqB,OAAkC,KAAK;CAClE,MAAM,gBAAgB,kCACpB,cACC,SAAS;AACR,qBAAmB,UAAU;GAEhC;AAED,iBAAgB;AACd,eAAa;GACX,MAAM,YAAY,mBAAmB,UAAU;AAE/C,OAAI,aAAa,KACf,OAAM,IAAI,MACR,+EACD;AAEH,cAAW;AACX,sBAAmB,UAAU;;IAE9B,CAAC,YAAY,CAAC;CAEjB,MAAM,eAAe,eAAe,SAAS,mBAAmB,UAAU;AAE1E,KAAI,gBAAgB,KAClB,QAAO,EAAE,OAAO,cAAc;AAMhC,OAAM,IAAI,MACR,sFACD"}
|
|
@@ -60,7 +60,11 @@ function useUpdatableDisposableState() {
|
|
|
60
60
|
});
|
|
61
61
|
(0, react.useEffect)(() => {
|
|
62
62
|
return function disposeAllRemainingItems() {
|
|
63
|
-
for (const undisposedICI of undisposedICIs.current)
|
|
63
|
+
for (const undisposedICI of undisposedICIs.current) {
|
|
64
|
+
undisposedICIs.current.delete(undisposedICI);
|
|
65
|
+
undisposedICI.cleanup();
|
|
66
|
+
}
|
|
67
|
+
setStateICI(UNASSIGNED_STATE);
|
|
64
68
|
};
|
|
65
69
|
}, []);
|
|
66
70
|
return {
|
|
@@ -60,7 +60,11 @@ function useUpdatableDisposableState() {
|
|
|
60
60
|
});
|
|
61
61
|
useEffect(() => {
|
|
62
62
|
return function disposeAllRemainingItems() {
|
|
63
|
-
for (const undisposedICI of undisposedICIs.current)
|
|
63
|
+
for (const undisposedICI of undisposedICIs.current) {
|
|
64
|
+
undisposedICIs.current.delete(undisposedICI);
|
|
65
|
+
undisposedICI.cleanup();
|
|
66
|
+
}
|
|
67
|
+
setStateICI(UNASSIGNED_STATE);
|
|
64
68
|
};
|
|
65
69
|
}, []);
|
|
66
70
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUpdatableDisposableState.mjs","names":[],"sources":["../src/useUpdatableDisposableState.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useHasCommittedRef } from './useHasCommittedRef';\n\nexport const UNASSIGNED_STATE: unique symbol = Symbol();\nexport type UnassignedState = typeof UNASSIGNED_STATE;\n\ntype UseUpdatableDisposableStateReturnValue<T> = {\n state: T | UnassignedState;\n setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;\n};\n\n/**\n * ICI stands for ItemCleanupIndex. Use a short name because it shows up a lot.\n *\n * Like an ItemCleanupPair<T>, but with an additional index and as a struct\n * for readability.\n *\n * Interally, we must keep track of the index of the state change. The disposable\n * items that are set in state are not guaranteed to be distinct according to ===,\n * as in `setState([1, dispose1]); setState([1, dispose2])`. Following this, we\n * would expect dispose1 to be called on commit. If state items were compared for\n * ===, then dispose1 would not be called in such a situation.\n *\n * Note that this comes at a runtime cost, and we may want to expose APIs that do\n * not incur this runtime cost, for example in cases where every disposable item is\n * distinguishable via ===.\n */\ntype ICI<T> = { item: T; cleanup: () => void; index: number };\n\n/**\n * useUpdatableDisposableState\n * - Returns a { state, setItem } object.\n * - setItem accepts an ItemCleanupPair<T>, and throws if called before commit.\n * - setItem sets the T in state and adds it to a set.\n * - React's behavior is that when the hook commits, whatever item is currently\n * returned from the useState hook is the oldest item which will ever be returned\n * from that useState hook. (More newly created ones can later be returned with\n * concurrent mode.)\n * - When this hook commits, all items up to, but not including, the item currently\n * returned from the useState hook are disposed and removed from the set.\n *\n * Calling setState before the hook commits:\n * - Calling setState before the hook commits is disallowed because until the hook\n * commits, React will not schedule any unmount callbacks, meaning that if this\n * hook never commits, any disposable items passed to setState will never be\n * disposed.\n * - We also cannot store them in some cache, because multiple components can share\n * the same cache location (for example, if they are loading the same query from\n * multiple components), so updating the cache with the disposable item will cause\n * both components to show the updated data, which is almost certainly a bug.\n * - Note that calling setState before commit is probably an anti-pattern! Consider\n * not doing it.\n * - If you must, the workaround is to lazily load the disposable item with\n * useDisposableState or useLazyDisposableState, and update the cache location\n * instead of calling setState before commit. One can update the cache location\n * by calling setState on some parent component that has already mounted, and\n * therefore passing in different props.\n * - This may only work in concurrent mode, though.\n */\nexport function useUpdatableDisposableState<\n T = never,\n>(): UseUpdatableDisposableStateReturnValue<T> {\n const hasCommittedRef = useHasCommittedRef();\n\n const undisposedICIs = useRef(new Set<ICI<T>>());\n const setStateCountRef = useRef(0);\n\n const [stateICI, setStateICI] = useState<ICI<T> | UnassignedState>(\n UNASSIGNED_STATE,\n );\n\n const setStateAfterCommit = useCallback(\n (itemCleanupPair: ItemCleanupPair<T>) => {\n if (!hasCommittedRef.current) {\n throw new Error(\n 'Calling setState before the component has committed is unsafe and disallowed.',\n );\n }\n\n const ici: ICI<T> = {\n item: itemCleanupPair[0],\n cleanup: itemCleanupPair[1],\n index: setStateCountRef.current,\n };\n setStateCountRef.current++;\n undisposedICIs.current.add(ici);\n setStateICI(ici);\n },\n [setStateICI],\n );\n\n useEffect(function cleanupUnreachableItems() {\n const indexInState = stateICI !== UNASSIGNED_STATE ? stateICI.index : 0;\n\n if (indexInState === 0) {\n return;\n }\n\n for (const undisposedICI of undisposedICIs.current) {\n if (undisposedICI.index === indexInState) {\n break;\n }\n undisposedICIs.current.delete(undisposedICI);\n undisposedICI.cleanup();\n }\n });\n\n useEffect(() => {\n return function disposeAllRemainingItems() {\n for (const undisposedICI of undisposedICIs.current) {\n undisposedICI.cleanup();\n }\n };\n }, []);\n\n return {\n setState: setStateAfterCommit,\n state: stateICI !== UNASSIGNED_STATE ? stateICI.item : UNASSIGNED_STATE,\n };\n}\n\n// @ts-ignore\nfunction tsTests() {\n const a = useUpdatableDisposableState();\n // @ts-expect-error\n a.setState([UNASSIGNED_STATE, () => {}]);\n // @ts-expect-error\n a.setState(['asdf', () => {}]);\n const b = useUpdatableDisposableState<string | UnassignedState>();\n // @ts-expect-error\n b.setState([UNASSIGNED_STATE, () => {}]);\n b.setState(['asdf', () => {}]);\n}\n"],"mappings":";;;;AAIA,MAAa,mBAAkC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDvD,SAAgB,8BAE+B;CAC7C,MAAM,kBAAkB,oBAAoB;CAE5C,MAAM,iBAAiB,uBAAO,IAAI,KAAa,CAAC;CAChD,MAAM,mBAAmB,OAAO,EAAE;CAElC,MAAM,CAAC,UAAU,eAAe,SAC9B,iBACD;CAED,MAAM,sBAAsB,aACzB,oBAAwC;AACvC,MAAI,CAAC,gBAAgB,QACnB,OAAM,IAAI,MACR,gFACD;EAGH,MAAM,MAAc;GAClB,MAAM,gBAAgB;GACtB,SAAS,gBAAgB;GACzB,OAAO,iBAAiB;GACzB;AACD,mBAAiB;AACjB,iBAAe,QAAQ,IAAI,IAAI;AAC/B,cAAY,IAAI;IAElB,CAAC,YAAY,CACd;AAED,WAAU,SAAS,0BAA0B;EAC3C,MAAM,eAAe,aAAa,mBAAmB,SAAS,QAAQ;AAEtE,MAAI,iBAAiB,EACnB;AAGF,OAAK,MAAM,iBAAiB,eAAe,SAAS;AAClD,OAAI,cAAc,UAAU,aAC1B;AAEF,kBAAe,QAAQ,OAAO,cAAc;AAC5C,iBAAc,SAAS;;GAEzB;AAEF,iBAAgB;AACd,SAAO,SAAS,2BAA2B;AACzC,QAAK,MAAM,iBAAiB,eAAe,
|
|
1
|
+
{"version":3,"file":"useUpdatableDisposableState.mjs","names":[],"sources":["../src/useUpdatableDisposableState.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useHasCommittedRef } from './useHasCommittedRef';\n\nexport const UNASSIGNED_STATE: unique symbol = Symbol();\nexport type UnassignedState = typeof UNASSIGNED_STATE;\n\ntype UseUpdatableDisposableStateReturnValue<T> = {\n state: T | UnassignedState;\n setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;\n};\n\n/**\n * ICI stands for ItemCleanupIndex. Use a short name because it shows up a lot.\n *\n * Like an ItemCleanupPair<T>, but with an additional index and as a struct\n * for readability.\n *\n * Interally, we must keep track of the index of the state change. The disposable\n * items that are set in state are not guaranteed to be distinct according to ===,\n * as in `setState([1, dispose1]); setState([1, dispose2])`. Following this, we\n * would expect dispose1 to be called on commit. If state items were compared for\n * ===, then dispose1 would not be called in such a situation.\n *\n * Note that this comes at a runtime cost, and we may want to expose APIs that do\n * not incur this runtime cost, for example in cases where every disposable item is\n * distinguishable via ===.\n */\ntype ICI<T> = { item: T; cleanup: () => void; index: number };\n\n/**\n * useUpdatableDisposableState\n * - Returns a { state, setItem } object.\n * - setItem accepts an ItemCleanupPair<T>, and throws if called before commit.\n * - setItem sets the T in state and adds it to a set.\n * - React's behavior is that when the hook commits, whatever item is currently\n * returned from the useState hook is the oldest item which will ever be returned\n * from that useState hook. (More newly created ones can later be returned with\n * concurrent mode.)\n * - When this hook commits, all items up to, but not including, the item currently\n * returned from the useState hook are disposed and removed from the set.\n *\n * Calling setState before the hook commits:\n * - Calling setState before the hook commits is disallowed because until the hook\n * commits, React will not schedule any unmount callbacks, meaning that if this\n * hook never commits, any disposable items passed to setState will never be\n * disposed.\n * - We also cannot store them in some cache, because multiple components can share\n * the same cache location (for example, if they are loading the same query from\n * multiple components), so updating the cache with the disposable item will cause\n * both components to show the updated data, which is almost certainly a bug.\n * - Note that calling setState before commit is probably an anti-pattern! Consider\n * not doing it.\n * - If you must, the workaround is to lazily load the disposable item with\n * useDisposableState or useLazyDisposableState, and update the cache location\n * instead of calling setState before commit. One can update the cache location\n * by calling setState on some parent component that has already mounted, and\n * therefore passing in different props.\n * - This may only work in concurrent mode, though.\n */\nexport function useUpdatableDisposableState<\n T = never,\n>(): UseUpdatableDisposableStateReturnValue<T> {\n const hasCommittedRef = useHasCommittedRef();\n\n const undisposedICIs = useRef(new Set<ICI<T>>());\n const setStateCountRef = useRef(0);\n\n const [stateICI, setStateICI] = useState<ICI<T> | UnassignedState>(\n UNASSIGNED_STATE,\n );\n\n const setStateAfterCommit = useCallback(\n (itemCleanupPair: ItemCleanupPair<T>) => {\n if (!hasCommittedRef.current) {\n throw new Error(\n 'Calling setState before the component has committed is unsafe and disallowed.',\n );\n }\n\n const ici: ICI<T> = {\n item: itemCleanupPair[0],\n cleanup: itemCleanupPair[1],\n index: setStateCountRef.current,\n };\n setStateCountRef.current++;\n undisposedICIs.current.add(ici);\n setStateICI(ici);\n },\n [setStateICI],\n );\n\n useEffect(function cleanupUnreachableItems() {\n const indexInState = stateICI !== UNASSIGNED_STATE ? stateICI.index : 0;\n\n if (indexInState === 0) {\n return;\n }\n\n for (const undisposedICI of undisposedICIs.current) {\n if (undisposedICI.index === indexInState) {\n break;\n }\n undisposedICIs.current.delete(undisposedICI);\n undisposedICI.cleanup();\n }\n });\n\n useEffect(() => {\n return function disposeAllRemainingItems() {\n for (const undisposedICI of undisposedICIs.current) {\n undisposedICIs.current.delete(undisposedICI);\n undisposedICI.cleanup();\n }\n setStateICI(UNASSIGNED_STATE);\n };\n }, []);\n\n return {\n setState: setStateAfterCommit,\n state: stateICI !== UNASSIGNED_STATE ? stateICI.item : UNASSIGNED_STATE,\n };\n}\n\n// @ts-ignore\nfunction tsTests() {\n const a = useUpdatableDisposableState();\n // @ts-expect-error\n a.setState([UNASSIGNED_STATE, () => {}]);\n // @ts-expect-error\n a.setState(['asdf', () => {}]);\n const b = useUpdatableDisposableState<string | UnassignedState>();\n // @ts-expect-error\n b.setState([UNASSIGNED_STATE, () => {}]);\n b.setState(['asdf', () => {}]);\n}\n"],"mappings":";;;;AAIA,MAAa,mBAAkC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDvD,SAAgB,8BAE+B;CAC7C,MAAM,kBAAkB,oBAAoB;CAE5C,MAAM,iBAAiB,uBAAO,IAAI,KAAa,CAAC;CAChD,MAAM,mBAAmB,OAAO,EAAE;CAElC,MAAM,CAAC,UAAU,eAAe,SAC9B,iBACD;CAED,MAAM,sBAAsB,aACzB,oBAAwC;AACvC,MAAI,CAAC,gBAAgB,QACnB,OAAM,IAAI,MACR,gFACD;EAGH,MAAM,MAAc;GAClB,MAAM,gBAAgB;GACtB,SAAS,gBAAgB;GACzB,OAAO,iBAAiB;GACzB;AACD,mBAAiB;AACjB,iBAAe,QAAQ,IAAI,IAAI;AAC/B,cAAY,IAAI;IAElB,CAAC,YAAY,CACd;AAED,WAAU,SAAS,0BAA0B;EAC3C,MAAM,eAAe,aAAa,mBAAmB,SAAS,QAAQ;AAEtE,MAAI,iBAAiB,EACnB;AAGF,OAAK,MAAM,iBAAiB,eAAe,SAAS;AAClD,OAAI,cAAc,UAAU,aAC1B;AAEF,kBAAe,QAAQ,OAAO,cAAc;AAC5C,iBAAc,SAAS;;GAEzB;AAEF,iBAAgB;AACd,SAAO,SAAS,2BAA2B;AACzC,QAAK,MAAM,iBAAiB,eAAe,SAAS;AAClD,mBAAe,QAAQ,OAAO,cAAc;AAC5C,kBAAc,SAAS;;AAEzB,eAAY,iBAAiB;;IAE9B,EAAE,CAAC;AAEN,QAAO;EACL,UAAU;EACV,OAAO,aAAa,mBAAmB,SAAS,OAAO;EACxD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isograph/react-disposable-state",
|
|
3
|
-
"version": "0.0.0-main-
|
|
3
|
+
"version": "0.0.0-main-1ba744d7",
|
|
4
4
|
"description": "Primitives for managing disposable state in React",
|
|
5
5
|
"homepage": "https://isograph.dev",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
"author": "Isograph Labs",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@isograph/disposable-types": "0.0.0-main-
|
|
11
|
+
"@isograph/disposable-types": "0.0.0-main-1ba744d7"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"react": "^18.0.0 || ^19.0.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
+
"@testing-library/react": "16.3.2",
|
|
17
18
|
"@types/react": "18.3.1",
|
|
19
|
+
"@types/react-test-renderer": "^18.3.0",
|
|
20
|
+
"happy-dom": "20.4.0",
|
|
18
21
|
"react-test-renderer": "^18.2.0",
|
|
19
22
|
"typescript": "5.6.3"
|
|
20
23
|
},
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './CacheItem';
|
|
|
4
4
|
export * from './ParentCache';
|
|
5
5
|
export * from './useCachedResponsivePrecommitValue';
|
|
6
6
|
export * from './useDisposableState';
|
|
7
|
+
export * from './useEffectsRerunWithoutRender';
|
|
7
8
|
export * from './useHasCommittedRef';
|
|
8
9
|
export * from './useLazyDisposableState';
|
|
9
10
|
export * from './useUpdatableDisposableState';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
StrictMode,
|
|
4
|
+
type MutableRefObject,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
} from 'react';
|
|
3
7
|
import { create } from 'react-test-renderer';
|
|
4
8
|
import { assert, describe, expect, test, vi } from 'vitest';
|
|
5
9
|
import type { CacheItem, CacheItemState } from './CacheItem';
|
|
@@ -52,9 +56,10 @@ function promiseAndResolver() {
|
|
|
52
56
|
|
|
53
57
|
// The fact that sometimes we need to render in concurrent mode and sometimes
|
|
54
58
|
// not is a bit worrisome.
|
|
55
|
-
async function awaitableCreate(Component, isConcurrent: boolean) {
|
|
59
|
+
async function awaitableCreate(Component: ReactElement, isConcurrent: boolean) {
|
|
56
60
|
const element = create(
|
|
57
|
-
Component
|
|
61
|
+
<StrictMode>{Component}</StrictMode>,
|
|
62
|
+
// @ts-expect-error
|
|
58
63
|
isConcurrent ? { unstable_isConcurrent: true } : undefined,
|
|
59
64
|
);
|
|
60
65
|
await shortPromise();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
|
4
4
|
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import type { ParentCache } from './ParentCache';
|
|
6
|
+
import { useEffectsRerunWithoutRenderRef } from './useEffectsRerunWithoutRender';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* useCachedResponsivePrecommitValue<T>
|
|
@@ -49,6 +50,8 @@ export function useCachedResponsivePrecommitValue<T>(
|
|
|
49
50
|
const [, rerender] = useState<{} | null>(null);
|
|
50
51
|
const lastCommittedParentCache = useRef<ParentCache<T> | null>(null);
|
|
51
52
|
|
|
53
|
+
const effectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef();
|
|
54
|
+
|
|
52
55
|
useEffect(() => {
|
|
53
56
|
lastCommittedParentCache.current = parentCache;
|
|
54
57
|
// On commit, cacheItem may be disposed, because during the render phase,
|
|
@@ -67,28 +70,37 @@ export function useCachedResponsivePrecommitValue<T>(
|
|
|
67
70
|
//
|
|
68
71
|
// After the above, we have a non-disposed item and a cleanup function, which we
|
|
69
72
|
// can pass to onCommit.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} else {
|
|
83
|
-
// We did not find an item in the parent cache, create a new one.
|
|
84
|
-
onCommit(parentCache.factory());
|
|
73
|
+
//
|
|
74
|
+
// When this effect runs again without a render (StrictMode's simulated
|
|
75
|
+
// remount, Fast Refresh, Activity), the render's temporary retain was
|
|
76
|
+
// already converted by the first run and released by the cleanup, so the
|
|
77
|
+
// item is re-acquired from the parent cache below instead.
|
|
78
|
+
if (!effectsRerunWithoutRenderRef.current) {
|
|
79
|
+
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(
|
|
80
|
+
disposeOfTemporaryRetain,
|
|
81
|
+
);
|
|
82
|
+
if (undisposedPair != null) {
|
|
83
|
+
onCommit(undisposedPair);
|
|
84
|
+
return;
|
|
85
85
|
}
|
|
86
|
-
// TODO: Consider whether we always want to rerender if the committed item
|
|
87
|
-
// was not returned during the last render, or whether some callers will
|
|
88
|
-
// prefer opting out of this behavior (e.g. if every disposable item behaves
|
|
89
|
-
// identically, but must be loaded.)
|
|
90
|
-
rerender({});
|
|
91
86
|
}
|
|
87
|
+
effectsRerunWithoutRenderRef.current = false;
|
|
88
|
+
|
|
89
|
+
// The cache item we created during render has been disposed. Check if the parent
|
|
90
|
+
// cache is populated.
|
|
91
|
+
const existingCacheItemCleanupPair =
|
|
92
|
+
parentCache.getAndPermanentRetainIfPresent();
|
|
93
|
+
if (existingCacheItemCleanupPair != null) {
|
|
94
|
+
onCommit(existingCacheItemCleanupPair);
|
|
95
|
+
} else {
|
|
96
|
+
// We did not find an item in the parent cache, create a new one.
|
|
97
|
+
onCommit(parentCache.factory());
|
|
98
|
+
}
|
|
99
|
+
// TODO: Consider whether we always want to rerender if the committed item
|
|
100
|
+
// was not returned during the last render, or whether some callers will
|
|
101
|
+
// prefer opting out of this behavior (e.g. if every disposable item behaves
|
|
102
|
+
// identically, but must be loaded.)
|
|
103
|
+
rerender({});
|
|
92
104
|
}, [parentCache]);
|
|
93
105
|
|
|
94
106
|
if (lastCommittedParentCache.current === parentCache) {
|
|
@@ -21,7 +21,6 @@ export function useDisposableState<T = never>(
|
|
|
21
21
|
const preCommitItem = useCachedResponsivePrecommitValue(
|
|
22
22
|
parentCache,
|
|
23
23
|
(pair) => {
|
|
24
|
-
itemCleanupPairRef.current?.[1]();
|
|
25
24
|
itemCleanupPairRef.current = pair;
|
|
26
25
|
},
|
|
27
26
|
);
|
|
@@ -35,25 +34,25 @@ export function useDisposableState<T = never>(
|
|
|
35
34
|
if (itemCleanupPairRef.current != null) {
|
|
36
35
|
itemCleanupPairRef.current[1]();
|
|
37
36
|
itemCleanupPairRef.current = null;
|
|
38
|
-
} else {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'itemCleanupPairRef.current is unexpectedly null. ' +
|
|
41
|
-
'This indicates a bug in react-disposable-state.',
|
|
42
|
-
);
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
39
|
},
|
|
46
40
|
[stateFromDisposableStateHook],
|
|
47
41
|
);
|
|
48
42
|
|
|
49
|
-
useEffect(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
itemCleanupPairRef.current
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
useEffect(
|
|
44
|
+
function cleanupItemCleanupPairRefIfSetStateNotCalled() {
|
|
45
|
+
return () => {
|
|
46
|
+
if (itemCleanupPairRef.current != null) {
|
|
47
|
+
itemCleanupPairRef.current[1]();
|
|
48
|
+
// Forgotten so that nothing can release this pair a second time
|
|
49
|
+
// before an effect re-run (StrictMode, Fast Refresh) stores a new one.
|
|
50
|
+
itemCleanupPairRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
[parentCache],
|
|
55
|
+
);
|
|
57
56
|
const state: T | undefined =
|
|
58
57
|
(stateFromDisposableStateHook !== UNASSIGNED_STATE
|
|
59
58
|
? stateFromDisposableStateHook
|
|
@@ -67,7 +66,6 @@ export function useDisposableState<T = never>(
|
|
|
67
66
|
setState,
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
|
-
|
|
71
69
|
// Safety: we can be in one of three states. Pre-commit, in which case
|
|
72
70
|
// preCommitItem is assigned, post-commit but before setState has been
|
|
73
71
|
// called, in which case itemCleanupPairRef.current is assigned, or
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
import { useEffectsRerunWithoutRenderRef } from './useEffectsRerunWithoutRender';
|
|
5
|
+
|
|
6
|
+
function Probe({ dep, reports }: { dep: number; reports: boolean[] }) {
|
|
7
|
+
const effectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
reports.push(effectsRerunWithoutRenderRef.current);
|
|
10
|
+
effectsRerunWithoutRenderRef.current = false;
|
|
11
|
+
}, [dep, effectsRerunWithoutRenderRef]);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('useEffectsRerunWithoutRenderRef', () => {
|
|
16
|
+
test('under StrictMode, the first mount reads false and the simulated remount reads true', () => {
|
|
17
|
+
const reports: boolean[] = [];
|
|
18
|
+
render(<Probe dep={0} reports={reports} />, { reactStrictMode: true });
|
|
19
|
+
|
|
20
|
+
expect(reports).toEqual([false, true]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('without StrictMode, the mount reads false', () => {
|
|
24
|
+
const reports: boolean[] = [];
|
|
25
|
+
render(<Probe dep={0} reports={reports} />, { reactStrictMode: false });
|
|
26
|
+
|
|
27
|
+
expect(reports).toEqual([false]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('an effect run caused by a render reads false, once the remount has reset the ref', () => {
|
|
31
|
+
const reports: boolean[] = [];
|
|
32
|
+
const { rerender } = render(<Probe dep={0} reports={reports} />, {
|
|
33
|
+
reactStrictMode: true,
|
|
34
|
+
});
|
|
35
|
+
rerender(<Probe dep={1} reports={reports} />);
|
|
36
|
+
|
|
37
|
+
expect(reports).toEqual([false, true, false]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the ref stays true until the caller resets it', () => {
|
|
41
|
+
const reads: boolean[] = [];
|
|
42
|
+
function NeverResets() {
|
|
43
|
+
const effectsRerunWithoutRenderRef = useEffectsRerunWithoutRenderRef();
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
reads.push(effectsRerunWithoutRenderRef.current);
|
|
46
|
+
});
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const { rerender } = render(<NeverResets />, { reactStrictMode: true });
|
|
50
|
+
rerender(<NeverResets />);
|
|
51
|
+
|
|
52
|
+
expect(reads).toEqual([false, true, true]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type MutableRefObject, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects that React is running this component's effects again without a
|
|
5
|
+
* render in between. That happens on StrictMode's simulated unmount and
|
|
6
|
+
* remount on the first commit, on Fast Refresh, and when an `Activity`
|
|
7
|
+
* boundary hides and then shows the component. In each case React runs every
|
|
8
|
+
* effect's cleanup and then its mount, so a cleanup that sets a flag is
|
|
9
|
+
* observable from the mounts that follow.
|
|
10
|
+
*
|
|
11
|
+
* Returns a ref whose `current` is `true` from the moment the cleanup ran until
|
|
12
|
+
* the caller resets it. The effect that must tell a re-run apart from a first
|
|
13
|
+
* mount reads it and sets it back to `false`; nothing else clears it.
|
|
14
|
+
*/
|
|
15
|
+
export function useEffectsRerunWithoutRenderRef(): MutableRefObject<boolean> {
|
|
16
|
+
const effectsRerunWithoutRenderRef = useRef(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return () => {
|
|
19
|
+
effectsRerunWithoutRenderRef.current = true;
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
return effectsRerunWithoutRenderRef;
|
|
23
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { configure, renderHook } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
5
4
|
import { ParentCache } from './ParentCache';
|
|
6
5
|
import { useLazyDisposableState } from './useLazyDisposableState';
|
|
6
|
+
configure({ reactStrictMode: true });
|
|
7
7
|
|
|
8
8
|
function createCache<T>(value: T) {
|
|
9
9
|
const disposeItem = vi.fn();
|
|
@@ -26,46 +26,85 @@ function promiseWithResolvers() {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe('useLazyDisposableState', async () => {
|
|
29
|
-
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('on render it should read the cache', async () => {
|
|
30
37
|
const cache1 = createCache(1);
|
|
31
|
-
const cache2 = createCache(2);
|
|
32
|
-
const renders = vi.fn();
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const { result } = renderHook(
|
|
40
|
+
(props) => useLazyDisposableState(props.parentCache),
|
|
41
|
+
{
|
|
42
|
+
initialProps: {
|
|
43
|
+
parentCache: cache1.cache,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
);
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
expect(result.current.state).toEqual(1);
|
|
49
|
+
expect(cache1.cache.factory).toHaveBeenCalledOnce();
|
|
50
|
+
});
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
test('on unmount it should dispose the cache', async () => {
|
|
53
|
+
const cache1 = createCache(1);
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
const { unmount } = renderHook(
|
|
56
|
+
(props) => useLazyDisposableState(props.parentCache),
|
|
57
|
+
{
|
|
58
|
+
initialProps: {
|
|
59
|
+
parentCache: cache1.cache,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
unmount();
|
|
65
|
+
vi.runAllTimers();
|
|
66
|
+
expect(cache1.disposeItem).toHaveBeenCalledOnce();
|
|
67
|
+
});
|
|
53
68
|
|
|
54
|
-
|
|
69
|
+
test('on cache change, it should read new cache', async () => {
|
|
70
|
+
const cache1 = createCache(1);
|
|
71
|
+
const cache2 = createCache(2);
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
const { result, rerender, unmount } = renderHook(
|
|
74
|
+
(props) => useLazyDisposableState(props.parentCache),
|
|
75
|
+
{
|
|
76
|
+
initialProps: {
|
|
77
|
+
parentCache: cache1.cache,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
);
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await unmounted.promise;
|
|
65
|
-
expect(cache2.disposeItem).toHaveBeenCalled();
|
|
82
|
+
rerender({
|
|
83
|
+
parentCache: cache2.cache,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.current.state).toEqual(2);
|
|
66
87
|
expect(cache2.cache.factory).toHaveBeenCalledOnce();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('on cache change, it should dispose previous cache', async () => {
|
|
91
|
+
const cache1 = createCache(1);
|
|
92
|
+
const cache2 = createCache(2);
|
|
93
|
+
|
|
94
|
+
const { rerender } = renderHook(
|
|
95
|
+
(props) => useLazyDisposableState(props.parentCache),
|
|
96
|
+
{
|
|
97
|
+
initialProps: {
|
|
98
|
+
parentCache: cache1.cache,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
rerender({
|
|
104
|
+
parentCache: cache2.cache,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
vi.runAllTimers();
|
|
108
|
+
expect(cache1.disposeItem).toHaveBeenCalledOnce();
|
|
70
109
|
});
|
|
71
110
|
});
|
|
@@ -21,11 +21,9 @@ export function useLazyDisposableState<T>(
|
|
|
21
21
|
state: T;
|
|
22
22
|
} {
|
|
23
23
|
const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);
|
|
24
|
-
|
|
25
24
|
const preCommitItem = useCachedResponsivePrecommitValue(
|
|
26
25
|
parentCache,
|
|
27
26
|
(pair) => {
|
|
28
|
-
itemCleanupPairRef.current?.[1]();
|
|
29
27
|
itemCleanupPairRef.current = pair;
|
|
30
28
|
},
|
|
31
29
|
);
|
|
@@ -39,9 +37,10 @@ export function useLazyDisposableState<T>(
|
|
|
39
37
|
'cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.',
|
|
40
38
|
);
|
|
41
39
|
}
|
|
42
|
-
|
|
40
|
+
cleanupFn();
|
|
41
|
+
itemCleanupPairRef.current = null;
|
|
43
42
|
};
|
|
44
|
-
}, []);
|
|
43
|
+
}, [parentCache]);
|
|
45
44
|
|
|
46
45
|
const returnedItem = preCommitItem?.state ?? itemCleanupPairRef.current?.[0];
|
|
47
46
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { type MutableRefObject } from 'react';
|
|
1
|
+
import React, { type MutableRefObject, StrictMode } from 'react';
|
|
2
2
|
import { create } from 'react-test-renderer';
|
|
3
3
|
import { describe, expect, test, vi } from 'vitest';
|
|
4
4
|
import {
|
|
@@ -51,7 +51,8 @@ function promiseAndResolver() {
|
|
|
51
51
|
// not is a bit worrisome.
|
|
52
52
|
async function awaitableCreate(Component, isConcurrent: boolean) {
|
|
53
53
|
const element = create(
|
|
54
|
-
Component
|
|
54
|
+
<StrictMode>{Component}</StrictMode>,
|
|
55
|
+
|
|
55
56
|
isConcurrent ? { unstable_isConcurrent: true } : undefined,
|
|
56
57
|
);
|
|
57
58
|
await shortPromise();
|
|
@@ -109,8 +109,10 @@ export function useUpdatableDisposableState<
|
|
|
109
109
|
useEffect(() => {
|
|
110
110
|
return function disposeAllRemainingItems() {
|
|
111
111
|
for (const undisposedICI of undisposedICIs.current) {
|
|
112
|
+
undisposedICIs.current.delete(undisposedICI);
|
|
112
113
|
undisposedICI.cleanup();
|
|
113
114
|
}
|
|
115
|
+
setStateICI(UNASSIGNED_STATE);
|
|
114
116
|
};
|
|
115
117
|
}, []);
|
|
116
118
|
|