import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; export type KeyedRead = | { status: 'idle' } | { status: 'pending' } | { status: 'ready'; value: T } | { status: 'failed'; error: unknown }; type Settled = { key: string; attempt: number; generation: number } & ({ ok: true; value: T } | { ok: false; error: unknown }); /** * A read for whatever `key` names. An answer stamped with another key reads as pending, never as this * one's, and an answer or error arriving after the key moved on is dropped. Bump `attempt` to read again. * Each read is its own generation: returning to a key reads it afresh, and its old answer never counts again. */ export function useKeyedRead( key: string | undefined, read: (key: string) => Promise, attempt = 0, ): KeyedRead { const [settled, setSettled] = useState | undefined>(undefined); // Bumped by every change of key or attempt, going idle included, so only the newest read's answer is current. const generationRef = useRef(0); // The latest callback, so a caller's unstable function cannot re-fire the read on every render. const readRef = useRef(read); useLayoutEffect(() => { readRef.current = read; }); useEffect(() => { generationRef.current += 1; const generation = generationRef.current; if (!key) return undefined; let live = true; readRef.current(key).then( (value) => { if (live) setSettled({ key, attempt, generation, ok: true, value }); }, (error: unknown) => { if (live) setSettled({ key, attempt, generation, ok: false, error }); }, ); return () => { live = false; }; }, [key, attempt]); return useMemo>(() => { if (!key) return { status: 'idle' }; if (!settled || settled.key !== key || settled.attempt !== attempt || settled.generation !== generationRef.current) { return { status: 'pending' }; } return settled.ok ? { status: 'ready', value: settled.value } : { status: 'failed', error: settled.error }; }, [key, attempt, settled]); }