From 408c1f599a564f2c37f24f7d1cce9323a9b16b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 13 Aug 2026 12:46:55 +0200 Subject: [PATCH] Resolve the settle verdict in render, not in an effect Same guarded transition, expressed as the render-phase state adjustment the hooks lint rule asks for: a resolved settle clears the state in the same committed render that carries the outcome, so the falling-edge toast still sees both flips at once and the block runs at most once per settle. --- .../insights/use-insights-advisor.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/insights/use-insights-advisor.ts b/src/components/insights/use-insights-advisor.ts index 4ad523632..0823c247f 100644 --- a/src/components/insights/use-insights-advisor.ts +++ b/src/components/insights/use-insights-advisor.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { InsightResult } from "@/lib/ai/types"; // Type-only — the runtime schemas load lazily inside `fetchAdvisor` so @@ -476,25 +476,24 @@ export function useInsightsAdvisorQuery( // Apply the settle resolution the pure seam proves. Each poll result lands // in `query.data` (the poll's own cache write IS the fresh payload — no - // second setQueryData needed); the seam decides fresh / settle-failed / - // keep-waiting, and the strip toasts on the falling edge of the combined - // busy state. The effect keys on `dataUpdatedAt`, not the payload object: - // structural sharing keeps an unchanged payload reference-identical across - // polls, and the attempt cap must still advance on every poll. - const settlePayload = query.data?.payload; - const settleDataUpdatedAt = query.dataUpdatedAt; - useEffect(() => { - if (!settling) return; + // second setQueryData needed) and re-renders this hook; the seam decides + // fresh / settle-failed / keep-waiting. Render-phase state adjustment (the + // React-documented alternative to a setState-in-effect): guarded so it + // fires at most once per settle — resolution non-null clears `settling`, + // and the next render skips the whole block. Both flips land in the SAME + // committed render, so the strip's falling-edge toast sees the proven + // outcome exactly when the busy state drops. + if (settling) { const resolution = resolveAdvisorSettle( settling, - settlePayload, + query.data?.payload, readDataUpdateCount(), ); if (resolution) { setSettling(null); setSettleOutcome(resolution); } - }, [settling, settlePayload, settleDataUpdatedAt, readDataUpdateCount]); + } const mutation = useMutation({ mutationFn: () => fetchAdvisor({ force: true }),