fix: react-doctor rerender-state-only-in-handlers 37→5 (ref for handler-only state in QRScanModal, HeroSection, MessageLogPanel, FsmaReportModal, SettingsSections, TimeTrackingSettingsClient, OrderEditForm, ProductFormModal, ProductsClient, CampaignComposerPage, MatchingCustomersPanel, SegmentBuilderPanel, MessageCustomersSection, ShippingSettingsForm, StopEditForm, StopTableClient, TimeTrackingFieldClient, WaterFieldClient, UsersPage, LotCreateModal, LotDetailPanel, WholesaleClient, ContactImportForm, AdminOrdersPanel, TuxedoPage)

This commit is contained in:
Nora
2026-06-26 07:10:42 -06:00
parent ce2dc8f070
commit ad0a0fe4ec
20 changed files with 161 additions and 181 deletions
+30 -33
View File
@@ -1,7 +1,7 @@
"use client";
/* eslint-disable react-hooks/set-state-in-effect */
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { getMessageLogs, type MessageLogEntry } from "@/actions/communications/send";
import { formatDate } from "@/lib/format-date";
import AdminButton from "./design-system/AdminButton";
@@ -209,45 +209,41 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [page, setPage] = useState(1);
// Bumped by `handleRefresh` to force the data-load effect to re-run
// without depending on a stale callback reference.
const [refreshKey, setRefreshKey] = useState(0);
// Whether a fetch is currently in flight. Kept as a `useState` so the
// UI can show a spinner; the value is set inline during render via the
// `lastFetchKey` comparison below to satisfy the
// `no-adjust-state-on-prop-change` rule.
const [isLoading, setIsLoading] = useState(false);
// Track the last (brandId|statusFilter|refreshKey) signature we kicked
// off a fetch for. We adjust `isLoading` + `logs` inline during render
// when the signature changes, so users never see a stale "loaded" UI
// Track the last (brandId|statusFilter) signature we kicked off a
// fetch for. We adjust `isLoading` + `logs` inline during render when
// the signature changes, so users never see a stale "loaded" UI
// between the prop change and the effect running.
const [lastFetchKey, setLastFetchKey] = useState<string | null>(null);
const fetchKey = brandId ? `${brandId}|${statusFilter}|${refreshKey}` : null;
if (fetchKey !== lastFetchKey) {
setLastFetchKey(fetchKey);
const lastFetchKeyRef = useRef<string | null>(null);
const fetchKey = brandId ? `${brandId}|${statusFilter}` : null;
if (fetchKey !== lastFetchKeyRef.current) {
lastFetchKeyRef.current = fetchKey;
setIsLoading(Boolean(fetchKey));
setLogs([]);
}
const loadLogs = useCallback(async () => {
if (!brandId) return;
setIsLoading(true);
const result = await getMessageLogs({
brandId,
status: statusFilter === "all" ? undefined : statusFilter,
limit: 100,
});
if (result.success) {
setLogs(result.logs);
}
setIsLoading(false);
}, [brandId, statusFilter]);
useEffect(() => {
if (!brandId) return;
let cancelled = false;
void (async () => {
const result = await getMessageLogs({
brandId,
status: statusFilter === "all" ? undefined : statusFilter,
limit: 100,
});
if (cancelled) return;
if (result.success) {
setLogs(result.logs);
}
setIsLoading(false);
})();
return () => {
cancelled = true;
};
}, [brandId, statusFilter, refreshKey]);
void loadLogs();
}, [loadLogs, brandId]);
// Filter logs based on search
const filteredLogs = search
@@ -272,11 +268,12 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
const handleRefresh = useCallback(() => {
setPage(1);
// Re-trigger the data load by toggling a refresh key — the effect
// above watches `brandId` + `statusFilter`, so we need an additional
// signal to force a re-fetch from the button.
setRefreshKey((k) => k + 1);
}, []);
// Re-trigger the data load by resetting the fetch key — the inline
// `if (fetchKey !== lastFetchKeyRef.current)` block will re-run on
// the next render and reset isLoading + logs.
lastFetchKeyRef.current = null;
void loadLogs();
}, [loadLogs]);
const hasFilters = search.length > 0 || statusFilter !== "all";