-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Cancellable issue in root store #3658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
To workaround the issue I use this propertyWrapper to wrap the reducer inside an /// A property wrapper that mimics `@StateObject` for root-level TCA features.
///
/// Useful when your feature has no parent (e.g. in your `App`, modal entry point, or top-level view).
/// Ensures the store is only initialized once.
///
/// ```swift
/// @RootStore(MyFeature(), state: MyFeature.State()) var store
/// ```
@propertyWrapper
public struct RootStore<StoreState, StoreAction>: DynamicProperty {
@StateObject private var store: Store<StoreState?, StoreAction>
public var wrappedValue: Store<StoreState, StoreAction> { store.scope(state: \.self!, action: \.self) }
public var projectedValue: Perception.Bindable<Store<StoreState, StoreAction>> {
.init(wrappedValue)
}
/// Creates the wrapped store using `.ifLet(\.self, action: \.self)` around the reducer,
/// to isolate each root-level feature instance and ensure effect cancellation works correctly.
///
/// This workaround is necessary when multiple instances of the same feature exist at the root level,
/// to avoid shared cancellation IDs clashing across unrelated store trees.
///
/// - seealso: https://github.com/pointfreeco/swift-composable-architecture/issues/3658
public init<R: Reducer>(
_ reducer: @escaping @autoclosure () -> R,
state initialState: @escaping @autoclosure () -> StoreState,
withDependencies prepareDependencies: ((inout DependencyValues) -> Void)? = nil
) where R.State == StoreState, R.Action == StoreAction {
self._store = .init(wrappedValue: {
.init(
initialState: initialState(),
reducer: {
EmptyReducer()
.ifLet(\.self, action: \.self, then: reducer)
},
withDependencies: prepareDependencies
)
}())
}
} |
Hi @arnauddorgans, thanks for reporting this. Can you give this PR #3660 a spin and let us know if it works for you? |
@mbrandonw thanks, it fixes the issue, but it could be cool to add the same logic to Scope by adding the keyPath in the store id maybe? @Reducer
struct SuperFeature {
@ObservableState
struct State {
var child1: Feature.State = .init(id: 1)
var child2: Feature.State = .init(id: 2)
var child3: Feature.State = .init(id: 3)
var child4: Feature.State = .init(id: 4)
var child5: Feature.State = .init(id: 5)
}
enum Action {
case child1(Feature.Action)
case child2(Feature.Action)
case child3(Feature.Action)
case child4(Feature.Action)
case child5(Feature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \.child1, action: \.child1) {
Feature()
}
Scope(state: \.child2, action: \.child2) {
Feature()
}
Scope(state: \.child3, action: \.child3) {
Feature()
}
Scope(state: \.child4, action: \.child4) {
Feature()
}
Scope(state: \.child5, action: \.child5) {
Feature()
}
}
} VStack {
WithPerceptionTracking {
FeatureView(store: store.scope(state: \.child1, action: \.child1))
FeatureView(store: store.scope(state: \.child2, action: \.child2))
FeatureView(store: store.scope(state: \.child3, action: \.child3))
FeatureView(store: store.scope(state: \.child4, action: \.child4))
FeatureView(store: store.scope(state: \.child5, action: \.child5))
}
} |
Hi @arnauddorgans, for that we are going to wait until 2.0. Introducing a navigation ID path for each scoping would be a bit too much with how things are modeled now, but will come for free with the changes we have planned for 2.0. |
@mbrandonw okay thanks! but it is weird because Scope and ifLet don't act the same, I don't have the issue with EmptyReducer()
.ifLet(\.child1, action: \.child1) {
Feature()
}
.ifLet(\.child2, action: \.child2) {
Feature()
}
.ifLet(\.child3, action: \.child3) {
Feature()
}
.ifLet(\.child4, action: \.child4) {
Feature()
}
.ifLet(\.child5, action: \.child5) {
Feature()
} |
Description
If you have multiple root stores of the same feature and return a
.cancel(id:)
effect in one of those, it will cancel the effect in all other root stores.Checklist
main
branch of this package.Expected behavior
It cancels only the effect of the current store
Simulator.Screen.Recording.-.iPhone.SE.3rd.generation.-.2025-04-16.at.18.10.01.mp4
Actual behavior
Cancels all effects of all stores
Simulator.Screen.Recording.-.iPhone.SE.3rd.generation.-.2025-04-16.at.18.09.30.mp4
Reproducing project
The Composable Architecture version information
1.19.0
Destination operating system
iOS 16 - 18
Xcode version information
Xcode 16.3
Swift Compiler version information
The text was updated successfully, but these errors were encountered: