Summary
Both useAskableVisibility and useAskableScrollView create an internal context via useState when no ctx is provided. The cleanup effect is supposed to destroy that internal context on unmount — but the cleanup condition checks the options parameter ctx, not the state variable, causing it to always run (or never run) incorrectly if the parent later provides a different context.
Root cause
packages/react-native/src/useAskableVisibility.ts:
const [visibilityCtx] = useState<AskableContext>(
() => ctx ?? createAskableContext(options)
);
useEffect(() => {
return () => {
if (!ctx) { // ← checks the CURRENT options.ctx prop
visibilityCtx.destroy(); // ← but visibilityCtx was created based on ctx at mount time
}
};
}, [ctx, visibilityCtx]);
If a parent passes ctx={undefined} at mount (so an internal context is created) and later passes ctx={someCtx}, the cleanup will NOT destroy the original internal context because at cleanup time ctx is now truthy.
The identical pattern exists in useAskableScrollView.ts.
Expected behavior
Whether to destroy the internal context should be decided at creation time (was a context created internally?) and stored alongside it, not re-evaluated at cleanup time from the current prop value.
Fix
const [{ visibilityCtx, owned }] = useState(() => {
const provided = ctx;
return provided
? { visibilityCtx: provided, owned: false }
: { visibilityCtx: createAskableContext(options), owned: true };
});
useEffect(() => {
return () => {
if (owned) visibilityCtx.destroy();
};
}, [visibilityCtx, owned]);
Same fix applies to useAskableScrollView.
Summary
Both
useAskableVisibilityanduseAskableScrollViewcreate an internal context viauseStatewhen noctxis provided. The cleanup effect is supposed to destroy that internal context on unmount — but the cleanup condition checks the options parameterctx, not the state variable, causing it to always run (or never run) incorrectly if the parent later provides a different context.Root cause
packages/react-native/src/useAskableVisibility.ts:If a parent passes
ctx={undefined}at mount (so an internal context is created) and later passesctx={someCtx}, the cleanup will NOT destroy the original internal context because at cleanup timectxis now truthy.The identical pattern exists in
useAskableScrollView.ts.Expected behavior
Whether to destroy the internal context should be decided at creation time (was a context created internally?) and stored alongside it, not re-evaluated at cleanup time from the current prop value.
Fix
Same fix applies to
useAskableScrollView.