Wait for several dependencies onAppear - Effect.merge

I have three service calls that must be made before a page can be shown. At the moment, I have done this by merging three effects:

let profileReducer = Reducer.combine(
Reducer<ProfileState, ProfileAction, ProfileEnvironment> { state, action, environment in
    switch action {
    case .onAppear:

        return Effect.merge([
            environment.orgClient.listPermissions()
                .receive(on: environment.mainQueue)
                .catchToEffect()
                .cancellable(id: OrgId())
                .map(ProfileAction.permissionsResponse),
            
            environment.locationClient.listLocations()
                .receive(on: environment.mainQueue)
                .catchToEffect()
                .cancellable(id: LocId())
                .map(ProfileAction.locationsResponse),
            
            environment.teamClient.listTeams()
                .receive(on: environment.mainQueue)
                .catchToEffect()
                .cancellable(id: TeamsId())
                .map(ProfileAction.teamsResponse)
            ]).cancellable(id: CombId())

...but that is probably not the smartest way to do it. I want to do these three simultaneously and wait for all three. Then I am sure that I have loaded everything I need to continue.

I do think there is another issue here; if one of the three calls fails with an error: 401 access token expired, I will remove the page entirely and I think that is the cause of a well described crash I am seeing:

Fatal error: "MainViewAction.profile(.permissionsResponse(.failure(.invalidToken)))" was received by an optional reducer when its state was "nil". This can happen for a few reasons:

* The optional reducer was combined with or run from another reducer that set "MainViewState" to "nil" before the optional reducer ran. Combine or run optional reducers before reducers that can set their state to "nil". This ensures that optional reducers can handle their actions while their state is still non-"nil".

* An active effect emitted this action while state was "nil". Make sure that effects for this optional reducer are canceled when optional state is set to "nil".

* This action was sent to the store while state was "nil". Make sure that actions for this reducer can only be sent to a view store when state is non-"nil". In SwiftUI applications, use "IfLetStore".:

This error has a beautiful description, and explain things very well, but I am struggling to find where to fix it. This led med to the merge of the three service calls.

Update: Possible solution

I think I found an ok solution - on receive error from one of the three calls, I could cancel the others by adding:

        case let .permissionsResponse(.failure(error)),
             let .teamsResponse(.failure(error)),
             let .locationsResponse(.failure(error)):
        return Effect.concatenate([ .cancel(id: OrgId()), 
                                    .cancel(id: LocId()), 
                                    .cancel(id: TeamsId()), 
                                    .init(value: .handleError(error))])

Then I also get the opportunity to handle the errors I want.

You might also be able to leverage Combine's operators to simplify. If you want to combine all 3 payloads into a single case where any failure short-circuits things, Zip3 could be perfect:

case .onAppear:
  return Zip3(
    environment.orgClient.listPermissions()
    environment.locationClient.listLocations()
    environment.teamClient.listTeams()
  )
  .receive(on: environment.mainQueue)
  .catchToEffect()
  .map(ProfileAction.combinedResponse)
  .cancellable(id: CombinedRequestId())
Terms of Service

Privacy Policy

Cookie Policy