134

I am wondering what is the use of asObservable:

As per docs:

An observable sequence that hides the identity of the source sequence.

But why would you need to hide the sequence?

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
born2net
  • 24,129
  • 22
  • 65
  • 104

4 Answers4

246

When to use Subject.prototype.asObservable()

The purpose of this is to prevent leaking the "observer side" of the Subject out of an API. Basically to prevent a leaky abstraction when you don't want people to be able to "next" into the resulting observable.

Example

(NOTE: This really isn't how you should make a data source like this into an Observable, instead you should use the new Observable constructor, See below).

const myAPI = {
  getData: () => {
    const subject = new Subject();
    const source = new SomeWeirdDataSource();
    source.onMessage = (data) => subject.next({ type: 'message', data });
    source.onOtherMessage = (data) => subject.next({ type: 'othermessage', data });
    return subject.asObservable();
  }
};

Now when someone gets the observable result from myAPI.getData() they can't next values in to the result:

const result = myAPI.getData();
result.next('LOL hax!'); // throws an error because `next` doesn't exist

You should usually be using new Observable(), though

In the example above, we're probably creating something we didn't mean to. For one, getData() isn't lazy like most observables, it's going to create the underlying data source SomeWeirdDataSource (and presumably some side effects) immediately. This also means if you retry or repeat the resulting observable, it's not going to work like you think it will.

It's better to encapsulate the creation of your data source within your observable like so:

const myAPI = {
  getData: () => return new Observable(subscriber => {
    const source = new SomeWeirdDataSource();
    source.onMessage = (data) => subscriber.next({ type: 'message', data });
    source.onOtherMessage = (data) => subscriber.next({ type: 'othermessage', data });
    return () => {
      // Even better, now we can tear down the data source for cancellation!
      source.destroy();
    };
  });
}

With the code above, any behavior, including making it "not lazy" can be composed on top of the observable using RxJS's existing operators.

Darpan
  • 234
  • 3
  • 15
Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 6
    TX Ben... been following your stuff... tx for all the great support on RX – born2net May 03 '16 at 20:22
  • then how to get value after var result = myAPI.getData(); – Shardul Jan 23 '17 at 21:53
  • 3
    @Shardul... you'd subscribe to result: `result.subscribe(value => doSomething(value))` – Ben Lesh Jan 26 '17 at 03:00
  • Does anyone know if this creates a new reference? i.e. if returning form a service would it create a new observable from the subject each time? – Ben Taliadoros Apr 12 '18 at 12:09
  • 2
    @BenTaliadoros Yes, each time you `return subject.asObservable();` it would be a new observable. You'd have a single Subject member variable, and the onMessage/onOtherMessage would be declared in a condition, or at initialization (not every call). I've used that approach, with a `pipe( filter() )` based on a parameter supplied to the `getData()` function. The – Drenai Jul 04 '18 at 12:00
  • @BenLesh what if I create the subject outside getData() and just return observable everytime getData() is called from the same subject instance? is it multicasting rather the current impl where each new subject is created and only once observable will be returned for that subject for each call to getData() – Shailesh Vaishampayan Feb 06 '19 at 14:57
  • 5
    @BenLesh, in your second code sample is `subject` supposed to be `subscriber` ? – Florin D Dec 09 '19 at 23:16
  • 1
    I would also like verification here : should the `subject.next` lines be `subscriber`. Also, "if you retry or repeat the resulting observable, it's not going to work like you think it will." Can you be more specific? Do you just mean `new SomeWeirdDataSource()` is going to happen every time `getData` is called and that by wrapping it in `new Observable` you make that instantiation wait until a subscription. I guess I don't see when you would call `getData` without a `.subscribe` so I'm missing the value there. Finally, what do you foresee happening to "tear down the data source"? Thanks. – 1252748 Jan 22 '20 at 22:55
21

A Subject can act both as an observer and an observable.

An Obervable has 2 methods.

  • subscribe
  • unsubscribe

Whenever you subscribe to an observable, you get an observer which has next, error and complete methods on it.

You'd need to hide the sequence because you don't want the stream source to be publicly available in every component. You can refer to @BenLesh's example, for the same.

P.S. : When I first-time came through Reactive Javascript, I was not able to understand asObservable. Because I had to make sure I understand the basics clearly and then go for asObservable. :)

HV Sharma
  • 4,891
  • 3
  • 15
  • 30
  • this might be a silly question but why don't we create an Observable in the first case instead of creating a Subject? – Waleed Ahmad Mar 21 '23 at 07:50
7

(Typescript Only) Use Types Instead of asObservable()

I like what Alex Vayda is saying about using types instead, so I'm going to add some additional information to clarify.


If you use asObservable(), then you are running the following code.

/**
 * Creates a new Observable with this Subject as the source. You can do this
 * to create customize Observer-side logic of the Subject and conceal it from
 * code that uses the Observable.
 * @return {Observable} Observable that the Subject casts to
 */
asObservable(): Observable<T> {
  const observable = new Observable<T>();
  (<any>observable).source = this;
  return observable;
}

This is useful for Javascript, but not needed in Typescript. I'll explain why below.


Example

export class ExampleViewModel {

   // I don't want the outside codeworld to be able to set this INPUT
   // so I'm going to make it private. This means it's scoped to this class
   // and only this class can set it.
   private _exampleData = new BehaviorSubject<ExampleData>(null);

   // I do however want the outside codeworld to be able to listen to
   // this data source as an OUTPUT. Therefore, I make it public so that
   // any code that has reference to this class can listen to this data
   // source, but can't write to it because of a type cast.
   // So I write this
   public exampleData$ = this._exampleData as Observable<ExampleData>;
   // and not this
   // public exampleData$ = this._exampleData.asObservable();
}

Both do the samething, but one doesn't add additional code calls or memory allocation to your program.

this._exampleData.asObservable();
Requires additional memory allocation and computation at runtime.

this._exampleData as Observable<ExampleData>;
Handled by the type system and will NOT add additional code or memory allocation at runtime.


Conclusion

If your colleagues try this, referenceToExampleViewModel.exampleData$.next(new ExampleData());, then it will be caught at compile time and the type system won't let them, because exampleData$ has been casted to Observable<ExampleData> and is no longer of type BehaviorSubject<ExampleData>, but they will be able to listen (.subscribe()) to that source of data or extend it (.pipe()).

This is useful when you only want a particular service or class to be setting the source of information. It also helps with separating the input from the output, which makes debugging easier.

Kevin Baker
  • 569
  • 5
  • 8
  • Can you elaborate a little bit on the _"Requires additional memory allocation and computation at runtime."_ topic? Is it because you create a new instance of an observable? – Franz Mar 20 '23 at 15:25
  • Calling .asObservable() runs the code block mentioned above. When a function is called it requires computation cycles on the CPU. Inside that code block is: new Observable(). When the "new" keyword is used, you are allocating memory to store this new object. If you don't use .asObservable, then you save some cpu computation and memory allocation – Kevin Baker Mar 20 '23 at 17:02
  • You can use .asObservable() in a small program and it won't matter. The amount of computation time and memory allocated is negligible, but if you are working on an enterprise system, and you need all the resources you can get, then casting your BehaviorSubject's to observables using the type system is the way to go – Kevin Baker Mar 20 '23 at 17:05
  • I feel like I could easily cast it back and use next if that was my intention. Wether that's a problem or not depends on context I suppose. – Arheisel Apr 13 '23 at 18:25
5

In addition to this answer I would mention that in my opinion it depends on the language in use.

For untyped (or weakly typed) languages like JavaScript it might make sense to conceal the source object from the caller by creating a delegate object like asObservable() method does. Although if you think about it it won't prevent a caller from doing observable.source.next(...). So this technique doesn't prevent the Subject API from leaking, but it indeed makes it more hidden form the caller.

On the other hand, for strongly typed languages like TypeScript the method asObservable() doesn't seem to make much sense (if any). Statically typed languages solve the API leakage problem by simply utilizing the type system (e.g. interfaces). For example, if your getData() method is defined as returning Observable<T> then you can safely return the original Subject, and the caller will get a compilation error if attempting to call getData().next() on it.

Think about this modified example:

let myAPI: { getData: () => Observable<any> }

myAPI = {
    getData: () => {
        const subject = new Subject()
        // ... stuff ...
        return subject
    }
}

myAPI.getData().next() // <--- error TS2339: Property 'next' does not exist on type 'Observable<any>'

Of course, since it all compiles to JavaScript in the end of the day there might still be cases when you want to create a delegate. But my point is that the room for those cases is much smaller then when using vanilla JavaScript , and probably in majority of cases you don't need that method.

Alex Vayda
  • 6,154
  • 5
  • 34
  • 50