'use client'; import React, { useEffect, useRef } from 'react'; import { usePlaygroundContext } from '../../context/PlaygroundContext'; import { useEndpointDraft } from '../../hooks/useEndpointDraft'; interface EndpointDraftSyncProps { /** Active schema id (so drafts don't bleed between different APIs). * When null — drafts are disabled (hook returns empty draft, writes * are no-ops). Pass the current schema's ``id`` from useOpenApiSchema. */ schemaId: string | null; } /** * Headless component: keeps ``RequestPanel`` state mirrored with a * per-endpoint draft in localStorage. * * Flow: * 1. On endpoint change ⇒ read draft ⇒ push into context state. * 2. On context state change (user edits params/body) ⇒ write draft. * * Step 1 is gated by a ref so "just-loaded" drafts don't immediately * trigger step 2. Step 2 additionally compares against a serialised * snapshot of the last persisted value so a re-render without a real * value change doesn't touch storage. * * Had an infinite-render-loop bug here earlier: the persist callbacks * were driven by ``useLocalStorage`` whose setter identity changed on * every internal state update, so depending on them from an effect * created a cycle. Fixed by making ``useEndpointDraft``'s writers * stable (ref-based), and by gating writes with value comparison below. */ export function EndpointDraftSync({ schemaId }: EndpointDraftSyncProps) { const { state, setParameters, setRequestBody, setActiveSchemaId } = usePlaygroundContext(); const ep = state.selectedEndpoint; // Mirror schemaId into context so other components (e.g. the Reset // button in RequestPanel) can read it without receiving props from // a parent they don't own. useEffect(() => { setActiveSchemaId(schemaId); }, [schemaId, setActiveSchemaId]); const { draft, setParameters: persistParams, setRequestBody: persistBody } = useEndpointDraft(schemaId, ep); const lastLoadedKeyRef = useRef(null); const lastPersistedParamsRef = useRef(''); const lastPersistedBodyRef = useRef(''); const currentKey = ep ? `${ep.method}|${ep.path}` : null; // Step 1 — hydrate context from draft on endpoint switch. // // IMPORTANT: only apply fields that actually exist in the saved draft. // SELECT_ENDPOINT in the reducer pre-fills ``requestBody`` with an // auto-generated schema example, and ``parameters`` with ``{}``. // If we blindly overwrite those with an empty draft we wipe the // example before the user ever sees it. Tracked via a bug: opening // POST /pet showed ``{"key":"value"}`` instead of the Pet example. useEffect(() => { if (!ep || !currentKey) { lastLoadedKeyRef.current = null; return; } if (lastLoadedKeyRef.current === currentKey) return; lastLoadedKeyRef.current = currentKey; const hasStoredParams = draft.parameters && Object.keys(draft.parameters).length > 0; const hasStoredBody = typeof draft.requestBody === 'string' && draft.requestBody !== ''; if (hasStoredParams) { setParameters(draft.parameters); lastPersistedParamsRef.current = JSON.stringify(draft.parameters); } else { // Keep whatever the reducer put there; mirror it into the // "last persisted" ref so step-2 treats it as a baseline. lastPersistedParamsRef.current = JSON.stringify(state.parameters); } if (hasStoredBody) { setRequestBody(draft.requestBody); lastPersistedBodyRef.current = draft.requestBody; } else { lastPersistedBodyRef.current = state.requestBody; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentKey]); // Step 2 — persist user edits. useEffect(() => { if (!ep || lastLoadedKeyRef.current !== currentKey) return; const serialised = JSON.stringify(state.parameters); if (serialised === lastPersistedParamsRef.current) return; lastPersistedParamsRef.current = serialised; persistParams(state.parameters); }, [state.parameters, ep, currentKey, persistParams]); useEffect(() => { if (!ep || lastLoadedKeyRef.current !== currentKey) return; if (state.requestBody === lastPersistedBodyRef.current) return; lastPersistedBodyRef.current = state.requestBody; persistBody(state.requestBody); }, [state.requestBody, ep, currentKey, persistBody]); return null; }