How to cancel an action inside another action in ComposableArchitecture

463 Views Asked by At

I am learning TCA using this link, I am having an issue which is when I tap the fact button, I need to stop the timer and trigger the toggleTimerButtonTapped action.

I tried to add .cancellable to .factButtonTapepd action but it doesn't work. I just want to know what is the right approach here?

    case .factButtonTapepd:
        state.isLoading = true
        state.isTimerRunning = false
        return .run { [count = state.count] send in
            Task {
                let fact = await fetchFact(forCount: count)
                await send(.factResponse(fact ?? "nil"))
            }
        }
        .cancellable(id: CancelID.timer)

here is the full code:

func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .decrementButtonTapped:
            state.count -= 1
            state.fact = nil
            return .none

        case .incrementButtonTapped:
            state.count += 1
            state.fact = nil
            return .none

        case .factButtonTapepd:
            state.isLoading = true
            state.isTimerRunning = false
            return .run { [count = state.count] send in
                Task {
                    let fact = await fetchFact(forCount: count)
                    await send(.factResponse(fact ?? "nil"))
                }
            }
            .cancellable(id: CancelID.timer)

        case let .factResponse(fact):
            state.fact = fact
            state.isLoading = false
            return .none

        case .toggleTimerButtonTapped:
            state.isTimerRunning.toggle()
            if state.isTimerRunning {
                return .run { send in
                    while true {
                        try await Task.sleep(for: .seconds(1))
                        await send(.timerTick)
                    }
                }
                .cancellable(id: CancelID.timer)
            } else {
                return .cancel(id: CancelID.timer)
            }

        case .timerTick:
            state.count += 1
            state.fact = nil
            return .none
        }
    }
}
1

There are 1 best solutions below

0
On

return a .merge(.cancel(id: CancelID.timer), .run { ... }), or add the parameter cancelInFlight: with a value of true to allow the cancellation of any running effects still in progress before starting the new effect.

.cancellable(id: any Hashable) allow for that effect to be cancelled and does not automatically cancel any effect "in flight". Providing the same value as you do in your code, I expect, would allow both long running effects to be canceled by any action that returns a .cancel(id:) effect with that same id.

In either case, you might consider using distinct cancellation identifiers for each of the timer and fetch fact effects.