import { useCallback, useEffect, useRef, useState } from "react"; import { useClient, type CombinedError, type TypedDocumentNode } from "urql"; import { fetchAllPages, type CursorConnection } from "@/lib/fetch-all-pages"; export interface AllConnectionNodesResult { /** * Every node once the load succeeds. `null` while loading AND on failure — * so it never masquerades as a real empty connection. Check `error` to tell a * failed load apart from a genuinely empty one. */ nodes: Node[] | null; error?: CombinedError; /** Retry the load (e.g. from an error fallback's "Try again"). */ reload: () => void; } /** * Eagerly load EVERY node of a nested cursor connection — paged to completion at * `pageSize` rows per request — for forms that genuinely need the whole line set * in memory (drag-reorder across all rows, or a submit diff computed against the * full document). `nodes` stays `null` until the load fully succeeds, so the * caller can gate rendering and then seed its field array ONCE from the complete * array (rather than mirroring server data into form state through an effect). * * For lazy, page-at-a-time reads use the DataTable cursor pattern instead; this * is specifically the "must have every row up front" case (#350). * * `variables` must NOT include `first` / `after` — those are injected per page. */ export function useAllConnectionNodes, Node>( query: TypedDocumentNode, variables: Omit, select: (data: Result) => CursorConnection | null | undefined, pageSize = 300, ): AllConnectionNodesResult { const client = useClient(); // Latest `select` held in a ref (updated in an effect, never during render) so // callers can pass an inline projection without re-triggering the load. const selectRef = useRef(select); useEffect(() => { selectRef.current = select; }); const varsKey = JSON.stringify(variables); const [reloadKey, setReloadKey] = useState(0); // Result is stamped with the vars + reload attempt it belongs to. When either // changes we're (re)loading, so the derived value below reports `null` without // a synchronous setState in the effect — no stale data, no phantom `[]`. const [state, setState] = useState<{ key: string; attempt: number; nodes: Node[] | null; error?: CombinedError; }>({ key: "", attempt: -1, nodes: null }); useEffect(() => { let cancelled = false; fetchAllPages( client, query, JSON.parse(varsKey) as Variables, (data: Result) => selectRef.current(data), pageSize, ) .then((nodes) => { if (!cancelled) setState({ key: varsKey, attempt: reloadKey, nodes }); }) .catch((error: CombinedError) => { if (!cancelled) setState({ key: varsKey, attempt: reloadKey, nodes: null, error }); }); return () => { cancelled = true; }; }, [client, query, varsKey, pageSize, reloadKey]); const reload = useCallback(() => setReloadKey((k) => k + 1), []); const fresh = state.key === varsKey && state.attempt === reloadKey; return { nodes: fresh ? state.nodes : null, error: fresh ? state.error : undefined, reload, }; }