fix(widgets): prevent TabsContainer state reset on data refetch

react-jsx-parser auto-generates random keys on every render, causing
full remounts that reset uncontrolled Tabs to defaultValue. Fix by:
- Adding disableKeyGeneration to preserve React reconciliation
- Memoizing bindings to avoid unnecessary re-parses
- Converting TabsContainer to controlled Tabs with useState
- Validating defaultTab against actual panel values
- Clearing stale parseErrors when template/data changes
This commit is contained in:
Thomas Camlong committed 2026-06-16 17:55:37 +02:00
1 parent 39900090b3
commit 065b78fa3e
2 files changed
+12 -3

No files matched your search

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import JsxParser from "react-jsx-parser";
import { Alert, Stack, Text } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
@@ -19,6 +19,11 @@ export default function CustomJsxDisplay({ data }: { data: Record<string, unknow
const template = String(data.template ?? "");
const apiData = data.data;
const [parseErrors, setParseErrors] = useState<string[]>([]);
const bindings = useMemo(() => SAFE_BINDINGS(apiData), [apiData]);
useEffect(() => {
setParseErrors([]);
}, [template, bindings]);
const handleError = useCallback((error: Error) => {
setParseErrors((prev) => appendParseError(prev, error.message));
@@ -39,7 +44,8 @@ export default function CustomJsxDisplay({ data }: { data: Record<string, unknow
<JsxParser
jsx={template}
components={WHITELISTED_COMPONENTS as never}
bindings={SAFE_BINDINGS(apiData)}
bindings={bindings}
disableKeyGeneration
componentsOnly
allowUnknownElements={false}
blacklistedAttrs={[/^on.+/i, /^dangerously/i]}
@@ -69,9 +69,12 @@ function isTabPanel(child: ReactNode): child is React.ReactElement<TabPanelProps
export function TabsContainer({ children, defaultTab }: TabsContainerProps) {
const panels = Children.toArray(children).filter(isTabPanel);
const firstPanel = panels[0];
const panelValues = new Set(panels.map((p) => p.props.value));
const initialTab = (defaultTab && panelValues.has(defaultTab) ? defaultTab : firstPanel?.props.value) ?? null;
const [activeTab, setActiveTab] = useState<string | null>(initialTab);
return (
<Tabs defaultValue={defaultTab ?? firstPanel?.props.value}>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List>
{panels.map((panel) => (
<Tabs.Tab key={panel.props.value} value={panel.props.value}>