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())

Is there an equivalent to do this with an array of effects? I'm trying to send an x amount of similar requests to an API and want to gather the results of each query into a single action.
So ProfileAction.combinedResponse would take an array of some object. Just not sure how to create something equivalent to the Zip3 part. I've tried using .merge() but can't seem to make it work.

Do you have some code we can look at? Effects are Combine publishers, so you can still feed them to .zip and then afterwards you can .map to your domain's action type before finally calling .eraseToEffect().

1 Like

Sure, I don't have it on hand unfortunately but I'll try to give a similar example.
In my environment I have a client object on which I can make the following call:
public var getObservations: (ObservationRequest) -> Effect<Observation, ClientError>
A single call returns only a single observation, here ObservationRequest contains some parameters for the api.
Now in my app I have some effect that once finished sends an action to the store with an array of queries (so it returns [ObservationRequest]). For each item in that list I want to make a call to the api by using the client. By mapping over them, I obtain a list of effects, one for each query.

let effects = queries.map { query in environment.client.getObservations(query) }

Now I'd like to run all these effects and once all have completed and returned their Observation I'd like to gather these and send an action that expects an array of observations SomeAction.allRequestsCompleted(observations). This is the part where I'm not sure how to do that :).
Is that enough context? Otherwise let me know and I'll make sure I can get access to the code again a bit faster :sweat_smile:

The complexity for me is in not knowing up front how many requests have to be made and therefore I can't use any of the Zip functions that are defined on Publishers it seems. Are there other methods/solutions I could be missing?

This doesn’t help with the error case but you can run your parallel effects using merge and concatenate a terminal effect which can just be an Effect(value: action) to the merged effect, which will run after the merged effect has completed.

For the errors you’d want to do as you’ve suggested and explicitly cancel the effects that are still in flight when one of them fails.