Skip to content

Store.ifLet(then:else:) migration: observe re-fires on child state mutations #3871

@renatorodrigues

Description

@renatorodrigues

Description

Store.ifLet(then:else:) was deprecated in TCA 1.24.0 in favor of Observation-based tooling. The two APIs have fundamentally different semantics that make a direct migration non-trivial, even when both parent and child states adopt @ObservableState.

ifLet(then:else:) fires only when the optional transitions between nil and non-nil. Internal mutations to the child state do not re-fire the then closure.

The observe { if let store = store.scope(...) } replacement fires whenever the optional's value changes, including internal child state mutations.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

After the nil → non-nil transition, subsequent child state mutations should not re-enter the observe block. The behavior should match ifLet(then:else:).

Actual behavior

The observe block re-fires on every child state mutation while the optional remains non-nil. In a UIKit context, this means view creation code executes multiple times, resulting in duplicate views or view controllers being added to the hierarchy.

Reproducing project

// First tap: nil → non-nil
observe: called ✅

// Second tap: child state mutation (title updated, feedback still non-nil)
observe: called ❌ → duplicate view added to stack

Preview

if-let-store.mp4.mp4

Xcode Project

https://github.com/renatorodrigues/if-let-store

The Composable Architecture version information

1.24.1

Destination operating system

iOS 26.2 / iOS 18.2

Xcode version information

26.2.0

Swift Compiler version information

swift-driver version: 1.127.14.1 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx26.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working due to a bug in the library.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions