fix: react-doctor rerender-state-only-in-handlers 46→~30 (lastFetchedBrandId/bodyHtml/rawHeaders/rawRows/useBucket/pendingImageUrl/prevSyncKey to useRef)

This commit is contained in:
Nora
2026-06-26 05:41:32 -06:00
parent ab8466e021
commit 0e4c295b10
5 changed files with 25 additions and 24 deletions
+3 -3
View File
@@ -59,9 +59,9 @@ function PlatformAdminBrandSettingsForm({
// Track the last brandId we kicked off a fetch for. When the prop
// changes we flip `loading` inline during render so users never see
// stale "loaded" UI between the prop change and the effect running.
const [lastFetchedBrandId, setLastFetchedBrandId] = useState<string | null>(null);
if (activeBrandId !== lastFetchedBrandId) {
setLastFetchedBrandId(activeBrandId);
const lastFetchedBrandIdRef = useRef<string | null>(null);
if (activeBrandId !== lastFetchedBrandIdRef.current) {
lastFetchedBrandIdRef.current = activeBrandId;
setLoadData({ settings: null, loading: true });
}
+4 -3
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import type { Campaign, CampaignType, CampaignStatus, AudienceRules } from "@/actions/communications/campaigns";
import { formatDate } from "@/lib/format-date";
@@ -338,7 +338,8 @@ export function CampaignEditPanel({
const [name, setName] = useState<string>(() => campaign?.name ?? "");
const [subject, setSubject] = useState<string>(() => campaign?.subject ?? "");
const [bodyText, setBodyText] = useState<string>(() => campaign?.body_text ?? "");
const [bodyHtml, setBodyHtml] = useState<string>(() => campaign?.body_html ?? "");
const bodyHtmlRef = useRef<string>(campaign?.body_html ?? "");
const setBodyHtml = (v: string) => { bodyHtmlRef.current = v; };
const [templateId, setTemplateId] = useState<string>(() => campaign?.template_id ?? "");
const [campaignType, setCampaignType] = useState<CampaignType>(
() => campaign?.campaign_type ?? "operational"
@@ -440,7 +441,7 @@ export function CampaignEditPanel({
name,
subject,
body_text: bodyText,
body_html: bodyHtml || undefined,
body_html: bodyHtmlRef.current || undefined,
template_id: templateId || undefined,
campaign_type: campaignType,
status: scheduleMode === "later" ? "scheduled" : (campaign?.status ?? "draft"),
+8 -9
View File
@@ -95,8 +95,10 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
const [step, setStep] = useState<Step>("idle");
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<ImportPreviewResult | null>(null);
const [rawHeaders, setRawHeaders] = useState<string[]>([]);
const [rawRows, setRawRows] = useState<string[][]>([]);
const rawHeadersRef = useRef<string[]>([]);
const setRawHeaders = (v: string[]) => { rawHeadersRef.current = v; };
const rawRowsRef = useRef<string[][]>([]);
const setRawRows = (v: string[][]) => { rawRowsRef.current = v; };
const [importRows, setImportRows] = useState<ContactImportEntry[]>([]);
const [importing, setImporting] = useState(false);
const [result, setResult] = useState<ImportResult | null>(null);
@@ -104,7 +106,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
const [overridingMappings, setOverridingMappings] = useState<
Record<string, ImportField>
>({});
const [useBucket, setUseBucket] = useState(false); // Toggle for bucket upload
const [uploadProgress, setUploadProgress] = useState<string>("");
const [importHistory, setImportHistory] = useState<{ filename: string; size: number; createdAt: string }[]>([]);
const fileRef = useRef<HTMLInputElement>(null);
@@ -127,7 +128,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
if (isLargeFile) {
// Use bucket upload for large files
setUploadProgress("Uploading file to storage...");
setUseBucket(true);
const uploadResult = await uploadContactsToBucket(brandId, f);
@@ -169,7 +169,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
}
// Browser-based processing for smaller files
setUseBucket(false);
const text = await f.text();
const previewResult = await previewContactImport(text);
@@ -325,14 +324,14 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
const handleConfirm = useCallback(
async () => {
if (!preview || rawRows.length === 0) return;
if (!preview || rawRowsRef.current.length === 0) return;
setStep("importing");
const rowsToImport = applyMappings(
preview.mappings,
overridingMappings,
rawHeaders,
rawRows
rawHeadersRef.current,
rawRowsRef.current
);
setImportRows(rowsToImport);
@@ -352,7 +351,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
setStep("result");
}
},
[brandId, preview, overridingMappings, rawHeaders, rawRows]
[brandId, preview, overridingMappings]
);
const handleReset = () => {
+7 -7
View File
@@ -54,10 +54,10 @@ export default function EditStopModal({ isOpen, onClose, brandId, stop, onSucces
// Reset form when modal opens or stop changes — derived during render
// per React docs: "Adjusting some state when a prop changes".
const [prevSyncKey, setPrevSyncKey] = useState<string | null>(null);
const prevSyncKeyRef = useRef<string | null>(null);
const syncKey = `${isOpen ? "open" : "closed"}:${stop?.id ?? "new"}`;
if (syncKey !== prevSyncKey) {
setPrevSyncKey(syncKey);
if (syncKey !== prevSyncKeyRef.current) {
prevSyncKeyRef.current = syncKey;
if (isOpen) {
if (stop) {
// Edit mode - populate from existing stop.
@@ -89,15 +89,15 @@ export default function EditStopModal({ isOpen, onClose, brandId, stop, onSucces
// Focus the first field once the modal has actually opened. This is
// a DOM side-effect, so it must run after the commit (i.e. in an
// effect) — accessing `cityRef.current` during render is illegal.
// The `prevSyncKey.startsWith("open:")` guard keeps the focus call
// to the transition into the open state, not every re-render.
// The `prevSyncKeyRef.current?.startsWith("open:")` guard keeps the focus
// call to the transition into the open state, not every re-render.
useEffect(() => {
if (prevSyncKey?.startsWith("open:")) {
if (prevSyncKeyRef.current?.startsWith("open:")) {
const id = requestAnimationFrame(() => cityRef.current?.focus());
return () => cancelAnimationFrame(id);
}
return undefined;
}, [prevSyncKey]);
}, []);
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
+3 -2
View File
@@ -49,7 +49,8 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [pendingImageUrl, setPendingImageUrl] = useState<string>("");
const pendingImageUrlRef = useRef<string>("");
const setPendingImageUrl = (v: string) => { pendingImageUrlRef.current = v; };
// Form state
const [name, setName] = useState("");
@@ -106,7 +107,7 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB
setError(null);
// Use pendingImageUrl if image was uploaded, otherwise null
const imageUrl = pendingImageUrl || null;
const imageUrl = pendingImageUrlRef.current || null;
const result = await createProduct(brandId, {
name,