You can simplify by defining your data$ observable to be derived from your "data input" and use the scan operator to emit the updated state based on the previous state and the new input. This can all be done without subscribing.
private dataInput$ = new ReplaySubject<DataInput>(1); // like BehaviorSubject, but with no initial emission
public data$: Observable<Data[]> = this.dataInput$.pipe(
scan((previous, input) => {
// return a value to be emitted based on the
// previous value and the current input.
return previous.concat(input);
}, [])
);
public addData(newData) {
this.dataInput.next(newData);
}
Here data$ will emit the updated state whenever dataInput$ emits a new value. If the modifications you need to make are synchronous, the above code will work fine. However, if they come from other observable sources, you will need to use mergeMap first:
public data$: Observable<Data[]> = this.dataInput$.pipe(
mergeMap(input => this.getSomeOtherData(input)),
scan((previous, someOtherData) => {
return // return something meaningful
}, INITIAL_DATA),
// startWith(INITIAL_DATA) // use startWith if you want to emit an initial value (in case inputData$ hasn't emitted yet
);
Here mergeMap will subscribe to the getSomeOtherData observable and emit it's emisison(s) into the scan operator.
Notice there are less moving parts here, its simply a subject, and a single derived observable. This approach avoids the circular issue you were having but, it also has the benefit of being completely lazy: if there no subscribers to data$ then the "fetch" logic will not be executed.