Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gentle-dryers-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@gradio/dataframe": patch
"@gradio/tabs": patch
"gradio": patch
---

fix:Fix infinite effect loop in tabs
12 changes: 8 additions & 4 deletions js/dataframe/shared/tanstack/table.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ export function createSvelteTable<TData extends RowData>(
let version = $state(0);

function updateOptions(): void {
table.setOptions((prev) => {
// mergeObjects creates lazy getters — `state` is NOT read here,
// only when TanStack accesses the `state` property later.
return mergeObjects(prev, options, {
table.setOptions(() => {
// Always merge from resolvedOptions instead of prev to prevent
// unbounded getter chains. Using prev would add a new mergeObjects
// layer on every call; properties not in the overrides would have
// to traverse every previous layer, causing stack overflow.
// TanStack's setOptions already re-applies feature defaults via
// mergeOptions, so we don't lose any internal defaults.
return mergeObjects(resolvedOptions, options, {
state: mergeObjects(state, options.state || {}),
onStateChange: (updater: any) => {
if (updater instanceof Function) state = updater(state);
Expand Down
15 changes: 9 additions & 6 deletions js/tabs/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@
let props = $props();
const gradio = new Gradio<TabsEvents, TabsProps>(props);

let old_selected = $state(gradio.props.selected);

$effect(() => {
if (gradio.props.selected) {
untrack(() => {
const i = gradio.props.initial_tabs.findIndex(
(t) => t.id === gradio.props.selected
);
if (old_selected !== gradio.props.selected) {
const i = gradio.props.initial_tabs.findIndex(
(t) => t.id === gradio.props.selected
);
if (i >= 0) {
gradio.dispatch("gradio_tab_select", {
value: gradio.props.initial_tabs[i].label,
index: i,
id: gradio.props.initial_tabs[i].id,
component_id: gradio.props.initial_tabs[i].component_id
});
});
}
old_selected = gradio.props.selected;
}
});
</script>
Expand Down
16 changes: 10 additions & 6 deletions js/tabs/shared/Tabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@

// When initial_tabs changes (e.g. a non-mounted tab's props were updated),
// sync the internal tabs array so the tab buttons reflect the new state.
// Using a function call so the $: dependency is only on initial_tabs,
// not on tabs (which would cause a loop with register_tab).
// The tabs mutation is deferred via tick() because in Svelte 5 legacy mode
// $: effects track all reads inside called functions — writing tabs[i]
// through the $state proxy would track `tabs` as a dependency, creating
// an infinite self-triggering loop.
$: _sync_tabs(initial_tabs);

function _sync_tabs(new_tabs: Tab[]): void {
for (let i = 0; i < new_tabs.length; i++) {
if (new_tabs[i] && !mounted_tab_orders.has(i)) {
tabs[i] = new_tabs[i];
tick().then(() => {
for (let i = 0; i < new_tabs.length; i++) {
if (new_tabs[i] && !mounted_tab_orders.has(i)) {
tabs[i] = new_tabs[i];
}
}
}
});
}

$: has_tabs = tabs.length > 0;
Expand Down
Loading