fix(ui): prevent infinite re-render loop on agent configure page
MarkdownEditor's plugins useMemo depended on imageUploadHandler, which was a new arrow function on every parent render. This caused MDXEditor to reinitialize plugins on each render, firing onChange, which updated the overlay state in AgentConfigForm, triggering a parent re-render — creating an infinite "Maximum update depth exceeded" loop. Fix: use a stable ref for imageUploadHandler so plugins are only created once (keyed on whether a handler exists, not its identity). Also use a module-level empty object for env config fallback to avoid unnecessary EnvVarEditor re-renders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,9 @@ const emptyOverlay: Overlay = {
|
|||||||
runtime: {},
|
runtime: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Stable empty object used as fallback for missing env config to avoid new-object-per-render. */
|
||||||
|
const EMPTY_ENV: Record<string, EnvBinding> = {};
|
||||||
|
|
||||||
function isOverlayDirty(o: Overlay): boolean {
|
function isOverlayDirty(o: Overlay): boolean {
|
||||||
return (
|
return (
|
||||||
Object.keys(o.identity).length > 0 ||
|
Object.keys(o.identity).length > 0 ||
|
||||||
@@ -617,8 +620,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<EnvVarEditor
|
<EnvVarEditor
|
||||||
value={
|
value={
|
||||||
isCreate
|
isCreate
|
||||||
? ((val!.envBindings ?? {}) as Record<string, EnvBinding>)
|
? ((val!.envBindings ?? EMPTY_ENV) as Record<string, EnvBinding>)
|
||||||
: ((eff("adapterConfig", "env", config.env ?? {}) as Record<string, EnvBinding>)
|
: ((eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record<string, EnvBinding>))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
secrets={availableSecrets}
|
secrets={availableSecrets}
|
||||||
|
|||||||
@@ -208,6 +208,10 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const dragDepthRef = useRef(0);
|
const dragDepthRef = useRef(0);
|
||||||
|
|
||||||
|
// Stable ref for imageUploadHandler so plugins don't recreate on every render
|
||||||
|
const imageUploadHandlerRef = useRef(imageUploadHandler);
|
||||||
|
imageUploadHandlerRef.current = imageUploadHandler;
|
||||||
|
|
||||||
// Mention state (ref kept in sync so callbacks always see the latest value)
|
// Mention state (ref kept in sync so callbacks always see the latest value)
|
||||||
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
||||||
const mentionStateRef = useRef<MentionState | null>(null);
|
const mentionStateRef = useRef<MentionState | null>(null);
|
||||||
@@ -235,11 +239,17 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
},
|
},
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
|
// Whether the image plugin should be included (boolean is stable across renders
|
||||||
|
// as long as the handler presence doesn't toggle)
|
||||||
|
const hasImageUpload = Boolean(imageUploadHandler);
|
||||||
|
|
||||||
const plugins = useMemo<RealmPlugin[]>(() => {
|
const plugins = useMemo<RealmPlugin[]>(() => {
|
||||||
const imageHandler = imageUploadHandler
|
const imageHandler = hasImageUpload
|
||||||
? async (file: File) => {
|
? async (file: File) => {
|
||||||
|
const handler = imageUploadHandlerRef.current;
|
||||||
|
if (!handler) throw new Error("No image upload handler");
|
||||||
try {
|
try {
|
||||||
const src = await imageUploadHandler(file);
|
const src = await handler(file);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
return src;
|
return src;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -268,7 +278,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
all.push(imagePlugin({ imageUploadHandler: imageHandler }));
|
all.push(imagePlugin({ imageUploadHandler: imageHandler }));
|
||||||
}
|
}
|
||||||
return all;
|
return all;
|
||||||
}, [imageUploadHandler]);
|
}, [hasImageUpload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== latestValueRef.current) {
|
if (value !== latestValueRef.current) {
|
||||||
|
|||||||
Reference in New Issue
Block a user