fix: react-doctor rerender-state-only-in-handlers 46→~30 (lastFetchedBrandId/bodyHtml/rawHeaders/rawRows/useBucket/pendingImageUrl/prevSyncKey to useRef)
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user