I'm currently using Composable Architecture 0.57.0. I'm gradually going to migrate to 1.0.0. I started working on some warnings. One that is causing a different behavior than the one I expect is replacing 'fireAndForget' with 'Effect.run, when I use them inside concatenate.
If I have a concatenate with many action calls and some fireAndForget, the functions in the fireAndForget would be executed before the ones that were indirectly called by the actions. When I replaced the code for .run I noticed that the order changed. In fireAndForget the functions ran by default on the main thread, which is not the case within .run, I tried running everything using the MainActor but the order is still different.
I could move that code out from concatenate, and just run it directly in the case action block , but I don't want to have effects executing without control, since it would be harder to test, although if fixes my order problem.
Here is an example. I need to keep the same order I get with fireAndForget but using .run.
import Testing
import ComposableArchitecture
var numbersFire: [Int] = []
var numbersRun: [Int] = []
struct OrderTestingTGATests {
@Test func exampleFire() async throws {
struct OrderFeature: Reducer {
struct State: Equatable {}
enum Action: String, CaseIterable {
case concat, add3, add4, add5, add6
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .add3:
return .fireAndForget { numbersFire.append(3) }
case .add4:
return .fireAndForget { numbersFire.append(4) }
case .add5:
return .fireAndForget { numbersFire.append(5) }
case .add6:
return .fireAndForget { numbersFire.append(6) }
case .concat:
return .concatenate(
.send(.add3),
.send(.add4),
.fireAndForget { numbersFire.append(1) },
.fireAndForget { numbersFire.append(2) },
.send(.add5),
.send(.add6)
)
}
}
}
}
let store = Store(initialState: OrderFeature.State(), reducer: OrderFeature())
store.send(.concat)
// Test passes
#expect(numbersFire == [1, 2, 3, 4, 5, 6])
}
@Test func exampleRun() async throws {
struct OrderFeature: Reducer {
struct State: Equatable {}
enum Action: String, CaseIterable {
case concat, add3, add4, add5, add6
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .add3:
return .run { _ in await MainActor.run { numbersRun.append(3) } }
case .add4:
return .run { _ in await MainActor.run { numbersRun.append(4) } }
case .add5:
return .run { _ in await MainActor.run { numbersRun.append(5) } }
case .add6:
return .run { _ in await MainActor.run { numbersRun.append(6) } }
case .concat:
return .concatenate(
.send(.add3),
.send(.add4),
.run { _ in await MainActor.run { numbersRun.append(1) } },
.run { _ in await MainActor.run { numbersRun.append(2) } },
.send(.add5),
.send(.add6)
)
}
}
}
}
let store = Store(initialState: OrderFeature.State(), reducer: OrderFeature())
store.send(.concat)
try await Task.sleep(for: .milliseconds(100))
// Expectation failed: (numbersRun → [1, 3, 4, 2, 5, 6]) == [1, 2, 3, 4, 5, 6]
#expect(numbersRun == [1, 2, 3, 4, 5, 6])
}
}