From e51666ee142a8f21c3d841b04383c269ec1e6da0 Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 29 Apr 2026 11:48:20 -0400 Subject: [PATCH] fix(ui): stop ServiceConfigForm from re-fetching schema every render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schema useEffect depended on currentConfig, which the parent passes as a fresh `{}` literal on every render — referentially new each time, so the effect re-ran and the GET /services/.../schema hammered the server. Schema fetch now only depends on serviceSlug; form seeding from currentConfig moved to a separate effect keyed on JSON-stringified config so a real change reseeds but referential churn doesn't. --- .../src/components/ServiceConfigForm.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/decnet_web/src/components/ServiceConfigForm.tsx b/decnet_web/src/components/ServiceConfigForm.tsx index 9c8105f9..a29986a9 100644 --- a/decnet_web/src/components/ServiceConfigForm.tsx +++ b/decnet_web/src/components/ServiceConfigForm.tsx @@ -70,6 +70,9 @@ const ServiceConfigForm: React.FC = ({ const [busy, setBusy] = useState<'save' | 'apply' | null>(null); const [revealed, setRevealed] = useState>({}); + // Fetch schema only when the slug changes. currentConfig is a fresh + // object literal from the parent on every render — depending on it + // here would re-fetch the schema on every render. useEffect(() => { let cancelled = false; setSchema(null); @@ -78,16 +81,25 @@ const ServiceConfigForm: React.FC = ({ .then(({ data }) => { if (cancelled) return; setSchema(data); - const init = buildInitial(data.fields, currentConfig ?? {}); - setForm(init); - setInitial(init); }) .catch((err) => { if (cancelled) return; setLoadErr(fmtError(err, 'Schema load failed.')); }); return () => { cancelled = true; }; - }, [serviceSlug, currentConfig]); + }, [serviceSlug]); + + // Seed form values from currentConfig once the schema is in hand. + // Keyed on JSON of the current cfg so a real change reseeds, but a + // referentially-new-but-equal object doesn't. + const seedKey = useMemo(() => JSON.stringify(currentConfig ?? {}), [currentConfig]); + useEffect(() => { + if (!schema) return; + const init = buildInitial(schema.fields, currentConfig ?? {}); + setForm(init); + setInitial(init); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [schema, seedKey]); const dirty = useMemo(() => { const keys = new Set([...Object.keys(form), ...Object.keys(initial)]);