import type { ItemCleanupPair } from '@isograph/disposable-types'; import { useEffect, useRef } from 'react'; import type { ParentCache } from './ParentCache'; import { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue'; import { UNASSIGNED_STATE, type UnassignedState, useUpdatableDisposableState, } from './useUpdatableDisposableState'; type UseUpdatableDisposableStateReturnValue = { state: T; setState: (pair: ItemCleanupPair>) => void; }; export function useDisposableState( parentCache: ParentCache, ): UseUpdatableDisposableStateReturnValue { const itemCleanupPairRef = useRef | null>(null); const preCommitItem = useCachedResponsivePrecommitValue( parentCache, (pair) => { itemCleanupPairRef.current?.[1](); itemCleanupPairRef.current = pair; }, ); const { state: stateFromDisposableStateHook, setState } = useUpdatableDisposableState(); useEffect( function cleanupItemCleanupPairRefAfterSetState() { if (stateFromDisposableStateHook !== UNASSIGNED_STATE) { 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; } }; }, []); const state: T | undefined = (stateFromDisposableStateHook !== UNASSIGNED_STATE ? stateFromDisposableStateHook : null) ?? preCommitItem?.state ?? itemCleanupPairRef.current?.[0]; if (state != null) { return { state: state, setState, }; } // Safety: we can be in one of three states. Pre-commit, in which case // preCommitItem is assigned, post-commit but before setState has been // called, in which case itemCleanupPairRef.current is assigned, or // after setState has been called, in which case // stateFromDisposableStateHook is assigned. // // Therefore, the type of state is T, not T | undefined. But the fact // that we are in one of the three states is not reflected in the types. // So we have to cast to T. // // Note that in the post-commit post-setState state, itemCleanupPairRef // can still be assigned, during the render before the // cleanupItemCleanupPairRefAfterSetState effect is called. throw new Error( 'state was unexpectedly null. This indicates a bug in react-disposable-state.', ); } // @ts-ignore function tsTests() { let x: any; const a = useDisposableState(x); // This should be a compiler error, because the generic is inferred to be of // type never. TODO determine why this doesn't break the build! // @ts-expect-error a.setState(['asdf', () => {}]); // @ts-expect-error a.setState([UNASSIGNED_STATE, () => {}]); const b = useDisposableState(x); // @ts-expect-error b.setState([UNASSIGNED_STATE, () => {}]); b.setState(['asdf', () => {}]); }