Skip to content

Fix tabs not updating stateful child components on tab switch#6458

Open
jholwill wants to merge 1 commit intomainfrom
fix/tabs-stale-carousel-state
Open

Fix tabs not updating stateful child components on tab switch#6458
jholwill wants to merge 1 commit intomainfrom
fix/tabs-stale-carousel-state

Conversation

@jholwill
Copy link
Copy Markdown

@jholwill jholwill commented Mar 13, 2026

Summary

Fixes an issue where switching tabs did not update content for stateful child components (e.g. Carousel showing stale pages/scroll position from the previously selected tab).

Root Cause

LoadedTabsComponentView renders a single LoadedTabComponentView at a fixed structural position in the SwiftUI view hierarchy. When the selected tab changes, SwiftUI sees the same view type at the same position and reuses it — preserving all @State from the previous tab's child components.

This is compounded by ComponentsView using ForEach(..., id: \.offset), which keys children by array index rather than component identity. A Carousel at index 1 in Tab A is treated as the same view as a Carousel at index 1 in Tab B, so its @State (data, index, carouselHeight, autoTimer) survives the tab switch. Since setupData() only runs in .onAppear — which doesn't re-fire for a structurally reused view — the carousel renders stale page data.

Fix

Add .id(selectedTabId) to LoadedTabComponentView. This forces SwiftUI to destroy and recreate the entire tab content subtree when the selected tab changes, ensuring all child @State is freshly initialized.

Alternatives Considered

  1. Fix ComponentsView ForEach identity — Use stable component IDs (every component has an id field from the data model) instead of id: \.offset. This would be a more surgical fix but has broader blast radius since ComponentsView/FlexVStack/FlexHStack are used everywhere, and changing ForEach identity can affect animation behavior and performance across all component rendering.

  2. React to tab changes inside CarouselView — Add .onChange(of: pages) or similar to re-run setupData() when the carousel's input data changes. This only fixes the Carousel specifically and would need to be repeated for every stateful component that could appear inside tabs.

  3. Default carousel size to width: .fill — Change CarouselComponentViewModel to default nil size to (width: .fill, height: .fit) instead of (width: .fit, height: .fit). This was observed to mask the issue (likely by triggering enough layout invalidation to cause re-rendering) but doesn't address the underlying view identity problem.

🤖 Generated with Claude Code


Note

Low Risk
Low risk, single-view identity change in SwiftUI; main impact is resetting child @State (and potentially animations/scroll position) when switching tabs.

Overview
Fixes stale state when switching tabs by forcing SwiftUI to rebuild the active tab subtree.

LoadedTabComponentView is now keyed with .id(selectedTabId), so stateful child components are recreated on tab changes instead of reusing prior tab state.

Written by Cursor Bugbot for commit a80274f. This will update automatically on new commits. Configure here.

When switching tabs, SwiftUI reuses LoadedTabComponentView at the same
structural position, preserving @State from the previous tab's children
(e.g. Carousel page data, scroll index). Adding .id(selectedTabId)
forces a full view recreation on each tab switch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jholwill jholwill marked this pull request as ready for review March 13, 2026 12:13
@jholwill jholwill requested a review from a team as a code owner March 13, 2026 12:13
@jholwill
Copy link
Copy Markdown
Author

What might explain the Kaitlin being able to edit the fill component to fix this is:

Carousel defaults to width: .fit when size is nil — CarouselComponentViewModel.swift:182 defaults to (width: .fit, height: .fit). Combined with GeometryReader (which needs proper size proposals from parents), this can cause layout issues. .fill would be a more sensible default for a carousel.

and figma importer:

Carousel converter doesn't emit size — carousel_converter.py never includes size in its output, so every Figma-imported carousel gets size: null, hitting the .fit default on iOS

Copy link
Copy Markdown
Member

@MonikaMateska MonikaMateska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks good!
Might be worth a follow-up at some point: if we key ComponentsView’s ForEach by a stable component id (instead of index) and have stateful bits like the Carousel re-set when their input changes, we’d fix this at the source and not rely on the parent nuking the subtree, just a thought for later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants