ReactiveCocoa and the flattenMap Operator

ReactiveCocoa has a number of built-in operators that enable a developer to map, filter, reduce, combine, merge, and flatten steams or signals. It’s a powerful and extensive toolbox, but sometimes it’s not clear under what circumstances these operators should be used.

One of my favorite operators is -flattenMap #mapping-and-flattening, and there are a two common cases in which I leverage it.

What is Flatten Map?

The documentation defines the -flattenMap: operator as follows:

-flattenMap: is used to transform each of a stream’s values into a new stream. Then, all of the streams returned will be flattened down into a single stream. In other words, it’s -map: followed by -flatten.

Another way to to define is that a new stream will be mapped from the value of its source stream, where the subscriber gets the value of the last stream to finish executing.

Use Cases

Incremental Loading

A number of mobile applications leverage incrementally loading and rendering data returned from an API. It improves the overall experience of the application because the user can start to engage with the application quickly, instead of having to wait for all relevant data to be returned.

For example, let’s assume there is an API that has endpoints that will return basic information about a person and known family members.

- (void) load {
  RACSignal *signal = [[api fetchPerson: personId] flattenMap: ^(id person) {
    return [[[api fetchFamilyMembers: person.id] map: ^(id familyMembers) {
      return [self buildViewModel: person familyMembers: familyMembers];     
    }] startWith: [self buildViewModel: person familyMembers: @[]];
  }];
}

The above code snippets does the following:

  1. Executes a signal that will eventually return a person from the API.
  2. When the person is retrieved from the API, it maps to a new signal from the API that will return the person’s family members.
  3. Both results are reduced to a view model that represents a person and their family members:
    • -startsWith: ensures that the signal returns immediately with the person and an empty collection of family members.
    • When the family members are fetched it will build a new view model with the person and collection of family members.

Mapping Bad Values to Errors

There are instances where a signal may return a value that is either an object that represents an error (NSError, for example) or the value is invalid and cannot be used.

The naive approach would be to do the following:

  - (void) load {
    RACSignal* signal = [api fetchPerson: personId];
    [signal subscribeNext: ^(id person) {
      if (!person) {
        [view renderError];
      } else {
        [view renderPerson: person];
      }
    }];
  }

In this case, it isn’t okay when a person is nil, and each subscriber would need to explicitly detect and know the error condition and handle it. Instead, the -fetchPerson: implementation could be written to map the signal to an error in the event a person is nil.

- (instancetype)fetchPerson:(NSInteger)personId {
  [[self fetchAtEndPoint: [NSString stringWithFormat: @"/person/%d", personId], mapTo: [Person class]] flattenMap: ^(id person) {
    if(!person) {
      return [RACSignal error: nil];
    } else {
      return [RACSignal return: person];
    }
  }];
}

- (instancetype)fetchAtEndPoint: (NSString *)relativePath mapTo: (Class)theClass {
 // Implementation details...
}

Now, in the event that a person is nil subscribers will not be notified and errors can be explicitly handled via the -subscribeError: operator.

- (void) load {
  RACSignal* signal = [api fetchPerson: personId];
  [signal subscribeNext: ^(id person) {
      [view renderPerson: person];
  }];
  
  [signal subscribeError: ^(NSError *error) {
    [view renderError];
  }];
}