Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix error occurring in useEffect on hmr #308

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
441b1df
fix error with hot module replacement
PatrykWalach Nov 12, 2024
10ce0e2
update tests
PatrykWalach Nov 12, 2024
61b16b9
fix cleanup
PatrykWalach Nov 14, 2024
4fbfb36
don't use stale itemCleanupPairRef
PatrykWalach Nov 14, 2024
e9d1845
add comment for guards added in `useEffects`
PatrykWalach Jan 3, 2025
ff7cd3b
Merge branch 'main' into fix-use-effect-error-on-hmr-rebase-2
PatrykWalach Jan 10, 2025
52bef46
fix throwing in `useDisposableState`
PatrykWalach Jan 10, 2025
820a8f7
fix double cleanup in `useUpdatableDisposableState`
PatrykWalach Jan 10, 2025
23fb36a
Merge branch 'fix-use-effect-error-on-hmr-rebase-2' of github.com:Pat…
PatrykWalach Jan 10, 2025
20c773e
detect hmr
PatrykWalach Jan 13, 2025
78c6037
fix errors
PatrykWalach Jan 13, 2025
2e24a7f
set `itemCleanupPairRef` to null on cleanup
PatrykWalach Jan 13, 2025
3f441e0
`revert last`
PatrykWalach Jan 13, 2025
d3de5d4
use ref of type bool to detect hmr
PatrykWalach Jan 13, 2025
ef925c6
fix error with hot module replacement
PatrykWalach Nov 12, 2024
a6af861
update tests
PatrykWalach Nov 12, 2024
b377add
fix cleanup
PatrykWalach Nov 14, 2024
faf696a
don't use stale itemCleanupPairRef
PatrykWalach Nov 14, 2024
8352c72
add comment for guards added in `useEffects`
PatrykWalach Jan 3, 2025
c0083e3
fix throwing in `useDisposableState`
PatrykWalach Jan 10, 2025
bef4639
fix double cleanup in `useUpdatableDisposableState`
PatrykWalach Jan 10, 2025
652be84
detect hmr
PatrykWalach Jan 13, 2025
8b0fdab
fix errors
PatrykWalach Jan 13, 2025
e329219
set `itemCleanupPairRef` to null on cleanup
PatrykWalach Jan 13, 2025
ad22a27
`revert last`
PatrykWalach Jan 13, 2025
50f95ea
use ref of type bool to detect hmr
PatrykWalach Jan 13, 2025
402ac00
Merge branches 'fix-use-effect-error-on-hmr-rebase-2' and 'fix-use-ef…
PatrykWalach Jan 13, 2025
420d291
Merge branch 'main' into fix-use-effect-error-on-hmr-rebase-2
PatrykWalach Jan 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/isograph-react-disposable-state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"devDependencies": {
"@types/react": "18.3.1",
"@types/react-test-renderer": "^18.3.0",
"react-test-renderer": "^18.2.0",
"typescript": "5.6.3"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ItemCleanupPair } from '@isograph/disposable-types';
import React from 'react';
import React, { StrictMode, type ReactElement } from 'react';
import { create } from 'react-test-renderer';
import { assert, describe, expect, test, vi } from 'vitest';
import { CacheItem, CacheItemState } from './CacheItem';
Expand Down Expand Up @@ -51,9 +51,10 @@ function promiseAndResolver() {

// The fact that sometimes we need to render in concurrent mode and sometimes
// not is a bit worrisome.
async function awaitableCreate(Component, isConcurrent) {
async function awaitableCreate(Component: ReactElement, isConcurrent) {
const element = create(
Component,
<StrictMode>{Component}</StrictMode>,
// @ts-expect-error
isConcurrent ? { unstable_isConcurrent: true } : undefined,
);
await shortPromise();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ export function useCachedResponsivePrecommitValue<T>(
const [, rerender] = useState<{} | null>(null);
const lastCommittedParentCache = useRef<ParentCache<T> | null>(null);

const maybeHiddenOrFastRefresh = useRef(false);
useEffect(() => {
return () => {
// Attempt to detect if the component was
// hidden (by Offscreen API), or fast refresh occured;
// Only in these situations would the effect cleanup
// for "unmounting" run multiple times, so if
// we are ever able to read this ref with a value
// of true, it means that one of these cases
// has happened.
maybeHiddenOrFastRefresh.current = true;
};
}, []);

useEffect(() => {
lastCommittedParentCache.current = parentCache;
// On commit, cacheItem may be disposed, because during the render phase,
Expand All @@ -68,28 +82,32 @@ export function useCachedResponsivePrecommitValue<T>(
//
// After the above, we have a non-disposed item and a cleanup function, which we
// can pass to onCommit.
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(
disposeOfTemporaryRetain,
);
if (undisposedPair !== null) {
onCommit(undisposedPair);
} else {
// The cache item we created during render has been disposed. Check if the parent
// cache is populated.
const existingCacheItemCleanupPair =
parentCache.getAndPermanentRetainIfPresent();
if (existingCacheItemCleanupPair !== null) {
onCommit(existingCacheItemCleanupPair);
} else {
// We did not find an item in the parent cache, create a new one.
onCommit(parentCache.factory());
if (maybeHiddenOrFastRefresh.current === false) {
const undisposedPair = cacheItem.permanentRetainIfNotDisposed(
disposeOfTemporaryRetain,
);
if (undisposedPair !== null) {
onCommit(undisposedPair);
return;
}
// TODO: Consider whether we always want to rerender if the committed item
// was not returned during the last render, or whether some callers will
// prefer opting out of this behavior (e.g. if every disposable item behaves
// identically, but must be loaded.)
rerender({});
}
maybeHiddenOrFastRefresh.current = false;

// The cache item we created during render has been disposed. Check if the parent
// cache is populated.
const existingCacheItemCleanupPair =
parentCache.getAndPermanentRetainIfPresent();
if (existingCacheItemCleanupPair !== null) {
onCommit(existingCacheItemCleanupPair);
} else {
// We did not find an item in the parent cache, create a new one.
onCommit(parentCache.factory());
}
// TODO: Consider whether we always want to rerender if the committed item
// was not returned during the last render, or whether some callers will
// prefer opting out of this behavior (e.g. if every disposable item behaves
// identically, but must be loaded.)
rerender({});
}, [parentCache]);

if (lastCommittedParentCache.current === parentCache) {
Expand Down
24 changes: 10 additions & 14 deletions libs/isograph-react-disposable-state/src/useDisposableState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export function useDisposableState<T = never>(
const preCommitItem = useCachedResponsivePrecommitValue(
parentCache,
(pair) => {
itemCleanupPairRef.current?.[1]();
itemCleanupPairRef.current = pair;
},
);
Expand All @@ -35,25 +34,22 @@ export function useDisposableState<T = never>(
if (itemCleanupPairRef.current !== null) {
itemCleanupPairRef.current[1]();
itemCleanupPairRef.current = null;
} else {
throw new Error(
'itemCleanupPairRef.current is unexpectedly null. ' +
'This indicates a bug in react-disposable-state.',
);
}
}
},
[stateFromDisposableStateHook],
);

useEffect(function cleanupItemCleanupPairRefIfSetStateNotCalled() {
return () => {
if (itemCleanupPairRef.current !== null) {
itemCleanupPairRef.current[1]();
itemCleanupPairRef.current = null;
}
};
}, []);
useEffect(
function cleanupItemCleanupPairRefIfSetStateNotCalled() {
return () => {
if (itemCleanupPairRef.current !== null) {
itemCleanupPairRef.current[1]();
}
};
},
[parentCache],
);

// Safety: we can be in one of three states. Pre-commit, in which case
// preCommitItem is assigned, post-commit but before setState has been
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ItemCleanupPair } from '@isograph/disposable-types';
import React, { useEffect, useState } from 'react';
import React, { StrictMode, useEffect, useState } from 'react';
import { create } from 'react-test-renderer';
import { describe, expect, test, vi } from 'vitest';
import { ParentCache } from './ParentCache';
Expand Down Expand Up @@ -56,7 +56,12 @@ describe('useLazyDisposableState', async () => {
return null;
}

const root = create(<TestComponent />, { unstable_isConcurrent: true });
const root = create(
<StrictMode>
<TestComponent />
</StrictMode>,
{ unstable_isConcurrent: true },
);
await committed.promise;
expect(cache1.disposeItem).toHaveBeenCalled();
expect(cache1.cache.factory).toHaveBeenCalledOnce();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ export function useLazyDisposableState<T>(
state: T;
} {
const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);

const preCommitItem = useCachedResponsivePrecommitValue(
parentCache,
(pair) => {
itemCleanupPairRef.current?.[1]();
itemCleanupPairRef.current = pair;
},
);
Expand All @@ -39,9 +37,9 @@ export function useLazyDisposableState<T>(
'cleanupFn unexpectedly null. This indicates a bug in react-disposable-state.',
);
}
return cleanupFn();
cleanupFn();
};
}, []);
}, [parentCache]);

const returnedItem = preCommitItem?.state ?? itemCleanupPairRef.current?.[0];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { StrictMode } from 'react';
import { create } from 'react-test-renderer';
import { describe, expect, test, vi } from 'vitest';
import {
Expand Down Expand Up @@ -45,7 +45,8 @@ function promiseAndResolver() {
// not is a bit worrisome.
async function awaitableCreate(Component, isConcurrent) {
const element = create(
Component,
<StrictMode>{Component}</StrictMode>,

isConcurrent ? { unstable_isConcurrent: true } : undefined,
);
await shortPromise();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function useUpdatableDisposableState<
useEffect(() => {
return function disposeAllRemainingItems() {
for (const undisposedICI of undisposedICIs.current) {
undisposedICIs.current.delete(undisposedICI);
undisposedICI.cleanup();
}
};
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading