Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RxJS support #2549

Closed
Rich-Harris opened this issue Apr 24, 2019 · 33 comments
Closed

RxJS support #2549

Rich-Harris opened this issue Apr 24, 2019 · 33 comments

Comments

@Rich-Harris
Copy link
Member

Seeing unexpected excitement from the RxJS community — turns out you can almost use RxJS observables natively in Svelte. The place where it falls down is unsubscriptions: https://mobile.twitter.com/Rich_Harris/status/1121042764429438976.

If there was a way to make RxJS support dead simple, a lot of people would find it useful. Could mean either

  • having a (built-in?) way to wrap RxJS observables as Svelte stores
  • accommodating subscribe functions that either return an unsubscribe function (as currently) or an object with an unsubscribe method, when setting up subscriptions with the implicit $foo$ form (the trailing $ is an Rx convention). Maybe we don't want to play favourites though (but it would be an easy and small thing to add, I think)
@tivac
Copy link
Contributor

tivac commented Apr 24, 2019

Supporting { unsubscribe() } would be useful for those of us who built up infrastructure around that convention from svelte@2, so I'd be very in favor of that.

@pngwn
Copy link
Member

pngwn commented Apr 24, 2019

The cost seems minimal, I see no reason not to make this change considering the above benefits.

benlesh added a commit to benlesh/svelte that referenced this issue Apr 25, 2019
There is a bit of excitement in the RxJS community about Svelte.

- It seems like the rest of Svelte "just works™" with RxJS!
- **BUT** The danger is that unwary users will figure out how smooth this API is and accidentally create nasty memory leaks if the returned RxJS Subscriptions are not handled. Fortunately the required change is small.

NOTE: I am not entirely sure how to test this change. The goal here is to make sure that whenever you would normally teardown your store subscriptions, it is also tearing down these RxJS-shaped subscriptions. This is most commonly something you want in a component scenario. Say you have a timer component in your app that you show and remove with an `{#if}` block, when the `{#if}` block hides the component, you'd want to tear down the underlying Observable that is "ticking".

Related sveltejs#2549
@kylecordes
Copy link

Automatic RxJS support would be a great improvement would be to Svelte, to make RxJS usable in component templates with certainty of automatic unsubscription and avoiding needless duplicate subscriptions.

How about reaching a bit further though?

Let developers trivially use an observable in a template, or use it as the thing being looped over in “each” etc., and have it “just work”. No extra syntax. No "each". Automatically unwrap the observability and do the right thing. Exact same template syntax for variables of type string, and of type Observable.

I also argue that right now is a good time to consider such a bold level of support. Over in my other favorite framework (Angular) it is arguably too late to do this because it could break semantics or break user expectations of a huge developer ecosystem.

@Rich-Harris
Copy link
Member Author

@kylecordes I'm not sure I follow what you mean by 'Exact same template syntax for variables of type string, and of type Observable' over and above what #2556 gives us?

@kylecordes
Copy link

Sorry for the fuzziness. When I hurriedly wrote the above, I said "no each" but meant "no $". Here’s what works right now:

<script>
	import { interval } from 'rxjs';
	let count = interval(400);
</script>

<h1> The count is {$count}</h1>

Here’s what I’m wondering about:

<script>
	import { interval } from 'rxjs';
	let count = interval(400);
</script>

<h1> The count is {count}</h1>

In other words, have observable values "Just Work" rather than treat them as special, as Stores etc. Reactivity as default built-in always-on, rather than being reached for (albeit with only a single character).

The challenge here, until type information is comprehensive of the available it's not clear how to do this with full efficiency. Once the type system can support telling the template compiler which values are type Observable, then it becomes "free" at runtime for the compiler to infer the $, right?

@pngwn
Copy link
Member

pngwn commented Apr 26, 2019

I don't see why Svelte would want to make such a concession for a single state management tool, if we do it for RxJS why shouldn't we do it for other widely used state-management libraries. This issue was really just about making a minor change that would prevent memory leaks, making RxJS observables usable out of the box.

In this context, RxJS Observables aren't being treated like Stores, they are stores. They fulfill the store contract. And as such they need to use the required syntax in order to benefit from automatic subscription and unsubscription. I see no reason to make special allowances for RxJS here.

You say 'don't treat them as special' but the proposed change would be treating them as special, that behaviour would not be consistent with how the svelte API works in general.

@kylecordes
Copy link

@pngwn Indeed, the thing I'm talking about, would ideally not be RxJS-specific or -aware at all. Rather, how about if store/async/observable/etc. data, regardless of library, would "just work" in a Svelte template?

Or put a different way, how about if plain local component state variables were no longer privileged to be consumed in a special way (without the $ prefix), but rather, both local state variables and higher order state (be it RxJS, svelte/store, etc.) were both first-class?

@pngwn
Copy link
Member

pngwn commented Apr 26, 2019

Because, outside of technical concerns, the syntax allows us to easily differentiate between normal values and subscribables. The extra syntax is a tool for users of the framework that allows for greater clarity.

Svelte has a few simple rules around reactivity. Reactive labels ($) for local reactive values, assigments operators (=, etc) for 'manual' reactivity and store prefixes ($value) for subscribables. I think these rules are relatively straightforward. Forcing users to understand the implementation details of each value in order to reason about their component code will lead to a worse user experience, in my opinion. Especially when you consider that not all stores will be written directly by the user, they could be consuming them via a third party library.

@kylecordes
Copy link

Here's a far future prediction: a framework/language of the future, will treat observable/reactive data as first class, and treat local state as either equally first-class, or behind some extra syntax. When the time comes for this, it will be obviously right. There is a progression something:

  • Make a thing possible (consume reactive data, but it is tedious to do so)
  • Make a thing easy (consume reactive data, you have to know to reach for the syntax to do so etc.) <— We are here
  • Make a thing default (consume reactive data without any difference at all)

Maybe now is not the time yet - today too much of the user base (including third-party libraries as you describe) would see it as being forced to understand something they don't want to understand, versus seeing it as a higher abstraction. I'm certainly not going to beat a dead horse on it, though I am curious what @Rich-Harris thinks also.

@steve-taylor
Copy link

steve-taylor commented Apr 26, 2019

@kylecordes the problem with your suggestion is that omitting the $ in $value means that Svelte has to check, at runtime, whether value is a store or, to avoid false positives, would have to wrap everything in a store. I don't think either option is ideal.

@kylecordes
Copy link

@steve-taylor Yes, that's why I think this probably is not feasible in an efficient way until there is comprehensive TypeScript support. The types could inform the compiler how to handle a particular variable.

(More generally, type aware compilation can produce better/smaller output in other ways also.)

@steve-taylor
Copy link

TypeScript

Svelte v4, perhaps.

@Rich-Harris
Copy link
Member Author

I'm inclined to agree with @pngwn, largely because I've made the mistake of banking on a Sufficiently Smart Compiler too many times in the past. Even assuming perfect typing information (and thereby excluding non-TypeScript users, I think it puts us in a worse position, because of things like this:

<!-- this works today -->
<p>{$a} + {$b} = {$a + $b}</p>

<!-- what would this mean? -->
<p>{a} + {b} = {a + b}</p>

It's one thing to treat the expression inside a curly brace as potentially being an observable, but to treat things within an expression as observable introduces a lot of ambiguity. I much prefer the explicitness of 'here is a store, and here is the store value'.

@kylecordes
Copy link

Thank you for looking at my suggestion - the point about still supporting JavaScript users is especially key, I think will a long time before it is "OK" in the overall web community to not support untyped code. I shall slink back to typing “$” without comment :-)

@brucou
Copy link

brucou commented Apr 28, 2019

I see where @kylecordes is coming from. I understand a store to be an abstraction for a (continuous) stream of values. A store always has a value, which can be queried (sampled) at any time via the appropriate sampling operator. The DOM is also a continuous stream of values. I guess Kyles want to write the DOM as an equation between stream of values. To refer to the example: $DOM = <h1> The count is {$count}</h1>. You could then sample the DOM stream with rAF.

There are functional languages such as Lucid Synchrone or Zelus which have implemented reactivity in the way Kyles suggest. However, they had to be very strict regarding the treatment of time, and the types they allow. Typically in Lucid, everything is a stream, and time is discrete, and represented by a clock abstraction. While the end result is indeed elegant, and in many ways so simple, and made its way into industrial modelling tools for embedded systems (with a natural concept of clock) such as SCADE, it is not easy to directly extend to asynchronous contexts.

About API for stores, to stay from vendor-specific API, why not use the TC39 observable proposal?

interface Observable {
(...)
    // Subscribes to the sequence with an observer
    subscribe(observer : Observer) : Subscription;
(...)
    // Subscribes to the sequence with callbacks
    subscribe(onNext : Function,
              onError? : Function,
              onComplete? : Function) : Subscription;
(...)
}

interface Subscription {

    // Cancels the subscription
    unsubscribe() : void;
(...)
}

That is pretty much the rxjs way actually, but it is not the rxjs interface, it is the TC39 interface and that goes in the direction of Svelte of supporting standards..

@pngwn
Copy link
Member

pngwn commented Apr 28, 2019

@brucou The relationship between Svelte stores and the TC39 Observable Proposal was discussed in the Reactive Store RFC.

@brucou
Copy link

brucou commented Apr 29, 2019

I read that. Yeah observables can be a bit tricky to grok indeed, mainly because of the difference between the concept and its implementation. A rxjs observable is for instance not a stream, but encloses a stream factory or more commonly called a producer -- that is how they say observables are cold. Because it has a factory, it has to be started, i.e. the factory has to be executed -- that is when you .subscribe(...). And what you terminate (with .complete()) is not the stream, it is the producer. And that is just the beginning for the source of confusion. Now if implementations are confusing, the concept itself is pretty simple.

I reviewed svelte's store and conceptually stores seem to be a mix of lenses and what is called behaviours in FRP terminology. A behaviour is denotationally a continuous function of time, say the behaviour b is a function t -> b(t) and you can only observe (sample) its values at a given time. Say at t0 you will observe b(t0)=b0. A behaviour neither starts nor terminates, and can't be updated, it is just a function. Lenses is a functional tool which allows to enter a structure and get/update a part of it.

Sorry if I over-extended myself. Back to the subject matter, I see a lot of benefits to supporting rxjs/tc39 observer interface, given the small surface area of the change.

@Mapiac
Copy link

Mapiac commented May 3, 2019

I agree this shouldn't be exclusive to RxJS. A huge contingent of ppl us Most.JS Core and other observable patterns and it would be best not only to support RxJS but them too.

@Conduitry
Copy link
Member

Any third-party observable pattern can be used with Svelte by writing an adapter. This has always been the case.

Native support for RxJS in particular was opted for because:

  • it's so widespread
  • it almost already worked, and the changes required to support it were minimal
  • (perhaps most importantly) the part of it we're dealing with aligns with TC39 Observables, the closest thing to a standardized way for observables to work that we've got

We don't want Svelte core to be bogged down with support for a bunch of relatively obscure observable implementations. Whether there should be official adapters for them is a separate question, but native support for them should not be a goal just because RxJS was added.

@MVSICA-FICTA
Copy link

What is the current status of the integration between Svelte and RxJS? It would be also be useful to see something documented about best practices and where the boundaries between the two APIs are.

@prescientmoon
Copy link

So, can i curently use rxjs with svelte? Was the unsubscribe thing implemented?

@kylecordes
Copy link

@mateiadrielrafael You can use it right now. We are. But there is a big caveat. To meet the "store contract", your observable needs to emit its first value immediately. If you have an observable chain that may take a while to emit its first value, you'll typically need a "startWith" or "publishReplay(1)" or similar depending on the situation.

@alshdavid
Copy link

alshdavid commented Aug 23, 2019

Also this works fine:

<script>
	import { onDestroy } from "svelte";
	export let authStore;

	let user;
	
	const unsubscribe = authStore.user.subscribe(value => {
	  user = value;
	});

	onDestroy(() => unsubscribe());
</script>

<main>
	<h1>Protected</h1>
	<h2>Hi {user.username}</h2>
</main>

But this does not

<script>
	export let authStore;
</script>

<main>
	<h1>Protected</h1>
	<h2>Hi {$authStore.user.username}</h2>
</main>

I have also tried authStore.$user.username

@kylecordes
Copy link

@alshdavid If you create something like this in a form it runs/fails in the REPL, likely someone can easily point out a syntax variation that works.

@alshdavid
Copy link

alshdavid commented Aug 23, 2019

I see. I am using codesandbox so perhaps that is the issue.

Here it is. The code in question is /pages/protected.svelte
https://codesandbox.io/s/vigorous-satoshi-z3jyv

@alshdavid
Copy link

alshdavid commented Aug 28, 2019

As an alternative, you can wrap an rxjs observable in a hook-like function

// use-subscribe.js
import { onDestroy } from "svelte";

export const useSubscribe = (rx, cb = () => {}) => {
  const unsubscribe = rx.subscribe(cb);

  onDestroy(() => unsubscribe());
};
// component.svelte
<script>
	import { useSubscribe } from "../use-subscribe";
	export let authStore;

	let user;
	useSubscribe(authStore.user, v => (user = v));
</script>

<div>Hi {user.username}</div>

@prescientmoon
Copy link

Hmmm, so it doesnt unsubscribe by itself...

@kylecordes
Copy link

@mateiadrielrafael Yes, it does unsubscribe. Svelte treats an Rx observable like a store; you access it with the $ prefix and it mostly Just Works. I say mostly because it is unclear to me whether it is ever OK for a Svelte store to not emit its first value immediately, while it is OK for an Observable to do so. I've occasionally edited my code a bit to ensure some value is emitted upon subscribe.

@alshdavid What is the benefit of writing that code above, given that Svelte already treats an Rx observable as a store: subscribing, updating, unsubscribing for you?

@prescientmoon
Copy link

@kylecordes So i should always use BehaviorSubjects or the shareReplay operator?

@evdama
Copy link

evdama commented Sep 2, 2019

I'd also be very interested in using rxjs with svelte, same as MVSICA-FICTA my question is wheter or not there's some sort of best practices already and if that's documented somewhere?

@kylecordes
Copy link

This is a new area for Svelte. I'm using it this way, but I haven't been following discussion on Discord recently and don't know if anyone else is.

About @mateiadrielrafael 's question, that is a good starting point simplification. You can often get by with just a startWith, but sometimes a shareReplay, which behind the scenes is basically creating a BehaviorSubject, makes more sense. I don't have some kind of simple iron rule to follow though. I approach this from the point of view of having used RxJS extensively for years.

@evdama I don't think there is any significant documentation yet, there probably needs to emerge a community of practice out of which that could arise. For the moment though, just that key rule I mentioned (to follow the Svelte store contract, your observable ought emit immediately upon subscription) will hopefully get you through, if you're generally experienced with RxJS.

@Conduitry
Copy link
Member

I'm going to close this, as I believe for some time we've had the issues ironed out with consuming RxJS observables anywhere that we can autosubscribe to a store.

@Jopie64
Copy link

Jopie64 commented Dec 15, 2019

@kylecordes

The types could inform the compiler how to handle a particular variable.

Just for your info, maybe you understood in the mean time:
TypeScript only uses types for type checking. It doesn't emit different JavaScript when only the types differ. That's one way it stays compatible with JavaScript.
Other compilers, like Babel, can simply remove type info from typescript code to output valid JavaScript.
So what you said here is impossible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests