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

Implement Sideways Data Loading #3398

Closed
sebmarkbage opened this issue Mar 13, 2015 · 136 comments
Closed

Implement Sideways Data Loading #3398

sebmarkbage opened this issue Mar 13, 2015 · 136 comments

Comments

@sebmarkbage
Copy link
Collaborator

This is a first-class API for sideways data loading of stateless (although potentially memoized) data from a global store/network/resource, potentially using props/state as input.

type RecordOfObservables = { [key:string]: Observable<mixed> };

class Foo {

  observe(): RecordOfObservables {
    return {
      myContent: xhr(this.props.url)
    };
  }

  render() {
    var myContent : ?string = this.data.myContent;
    return <div>{myContent}</div>;
  }

}

observe() executes after componentWillMount/componentWillUpdate but before render.

For each key/value in the record. Subscribe to the Observable in the value.

subscription = observable.subscribe({ onNext: handleNext });

We allow onNext to be synchronously invoked from subscribe. If it is, we set:

this.data[key] = nextValue;

Otherwise we leave it as undefined for the initial render. (Maybe we set it to null?)

Then render proceeds as usual.

Every time onNext gets invoked, we schedule a new "this.data[key]" which effectively triggers a forcedUpdate on this component. If this is the only change, then observe is not reexecuted (componentWillUpdate -> render -> componentDidUpdate).

If props / state changed (i.e. an update from recieveProps or setState), then observe() is reexecuted (during reconciliation).

At this point we loop over the new record, and subscribe to all the new Observables.

After that, unsubscribe to the previous Observables.

subscription.dispose();

This ordering is important since it allows the provider of data to do reference counting of their cache. I.e. I can cache data for as long as nobody listens to it. If I unsubscribed immediately, then the reference count would go down to zero before I subscribe to the same data again.

When a component is unmounted, we automatically unsubscribe from all the active subscriptions.

If the new subscription didn't immediately call onNext, then we will keep using the previous value.

So if my this.props.url from my example changes, and I'm subscribing to a new URL, myContent will keep showing the content of the previous url until the next url has fully loaded.

This has the same semantics as the <img /> tag. We've seen that, while this can be confusing and lead to inconsistencies it is a fairly sane default, and it is easier to make it show a spinner than it would be to have the opposite default.

Best practice might be to immediately send a "null" value if you don't have the data cached. Another alternative is for an Observable to provide both the URL (or ID) and the content in the result.

class Foo {

  observe() {
    return {
      user: loadUser(this.props.userID)
    };
  }

  render() {
    if (this.data.user.id !== this.props.userID) {
      // Ensure that we never show inconsistent userID / user.name combinations.
      return <Spinner />;
    }
    return <div>Hello, {this.data.user.name} [{this.props.userID}]!</div>;
  }

}

We should use the RxJS contract of Observable since that is more in common use and allows synchronous execution, but once @jhusain's proposal is in more common use, we'll switch to that contract instead.

var subscription = observable.subscribe({ onNext, onError, onCompleted });
subscription.dispose();

We can add more life-cycle hooks that respond to these events if necessary.

Note: This concept allows sideways data to behave like "behaviors" - just like props. This means that we don't have to overload the notion state for these things. It allows for optimizations such as throwing away the data only to resubscribe later. It is restorable.

@josephsavona
Copy link
Contributor

undefined is probably the safest value to assign to data until the observable provides its first value via onNext. For example in Relay we assign different meanings to null (data does not exist) and undefined (not yet fetched), so our ideal default data value would be undefined. The alternative is to provide a new method, eg getInitialData, but I suspect this is unnecessary/overkill.

@fdecampredon
Copy link

This is pretty interesting however from the point of view of static typing I'm not so happy of key/value system, their type is pretty much impossible to express.
Why not having observe return a single observable and set/merge the value resolved to this.data :

class Foo {

  observe() {
    return (
      loadUser(this.props.userID)
        .map(user => { user })
  }

  render() {
    if (this.data.user.id !== this.props.userID) {
      // Ensure that we never show inconsistent userID / user.name combinations.
      return <Spinner />;
    }
    return <div>Hello, {this.data.user.name} [{this.props.userID}]!</div>;
  }

}

And for the case of multiple fetch something like :

class Foo {

  observe() {
    return (
     combineLatest(
      loadUser(this.props.userID),
      loadSomethingElse(this.props.somethingElseId),
      (user, somethingElse) => ({ user, somethingElse})
     )
  }

  render() {
    ..
  }

}

This is perhaps a bit more verbose, but it allows to have nice static type :

interface Comp<T> {
  observe(): Observable<T>;
  data: T;
}

@fdecampredon
Copy link

Also instead of re executing observe when props/state change we can have access to 'props' 'state' as an observable :

class Foo {

  observe(propsStream) {
    return (
      propsStream
        .flatMap(({ userID }) => loadUser(userId))
        .map(user => { user })
    );
  }

  render() {
    if (this.data.user.id !== this.props.userID) {
      // Ensure that we never show inconsistent userID / user.name combinations.
      return <Spinner />;
    }
    return <div>Hello, {this.data.user.name} [{this.props.userID}]!</div>;
  }
}

@sebmarkbage
Copy link
Collaborator Author

The reason is because we don't want to require the use of combinators and understanding RxJS to be able to subscribe to (multiple) Observables. Combining two Observables in this way is quite confusing. In fact, at least for our data sources, we'll probably implement the subscription API but not even include the combinators on the Observables' prototype. That's not a requirement, but you're free to use combinators if you need to.

However, to subscribe to a simple Flux store, you shouldn't need to.

I think that Flow will probably be able to handle this static type using constraints, but I will check with those guys to make sure. I think that it'll be enough to type the data property and then the observe type can be implied.

class Foo extends React.Component {
  data : { user : User, content : string };
  observe() /* implied as { user : Observable<User>, content : Observable<string> } */ {
  }
}

This change is not about going all in on Observables as a way to describe application state. That can be implemented on top of this, like you've done before. This is explicitly not about application state since this method is idempotent. The framework is free to unsubscribe and resubscribe as needed.

@vjeux
Copy link
Contributor

vjeux commented Mar 13, 2015

cc @ericvicenti

@fdecampredon
Copy link

At least in the case of typescript, there would be no way to constraint the return type of observe based on the type of data, at least until something like microsoft/TypeScript#1295 is implemented.

@gaearon
Copy link
Collaborator

gaearon commented Mar 13, 2015

I'd love to use this for the next version of React DnD, but obviously this requires waiting for React 0.14.
I wonder if I can “polyfill” this for the time being with a higher-order component that sets this.data on a ref instance.. Might be too crazy though.

@RickWong
Copy link

Would it be possible to observe Promises? Then one could use a Promises tree to resolve data for the whole component tree before the first render! This would be very useful for server-side React.

@aaronshaf
Copy link

What are the benefits of making this a first-class API? It could essentially be accomplished using a "higher-order component."

@gaearon
Copy link
Collaborator

gaearon commented Mar 14, 2015

What are the benefits of making this a first-class API? It could essentially be accomplished using a "higher-order component."

Wrapping in 5 HOCs to get 5 subscriptions is a bit unwieldy and harder to understand for beginners. Understanding componentWillReceiveProps is also non-trivial. This fixes both.

I, for one, welcome our new observable overlords.

@gaearon
Copy link
Collaborator

gaearon commented Mar 14, 2015

I wonder if this can help bring https://github.com/chenglou/react-state-stream closer to React's vanilla API

@aaronshaf
Copy link

Wouldn't it just take one HOC? In the example in your Medium post, you iterate over stores and subscribe to each.

stores.forEach(store =>
  store.addChangeListener(this.handleStoresChanged)
);

@gaearon
Copy link
Collaborator

gaearon commented Mar 14, 2015

@aaronshaf Depends on use case, for sure. Sometimes it's different kinds of state sources, not just “several stores”. But I can't say on behalf of React team, let's hear what @sebmarkbage says.

@nmn
Copy link
Contributor

nmn commented Mar 14, 2015

Would love some sort of polyfill to play with this now. I didn't get the idea completely, yet. What is the mechanism involved with dealing with future updates. I'll spend some more time understanding it. I think it should be doable with a simple mixin.

@elierotenberg
Copy link

(@vjeux told me I should chime in! so here I am.)

I don't mean to promote my own work, but I think this hook is very similar to the getNexusBindings hook in React Nexus. You declare data deps at the component level via a lifecycle hook (which can depend on props).

The API looks like:

class UserDetails {
  getNexusBindings(props) {
    return {
      // binding to data in the datacenter
      posts: [this.getNexus().remote, `users/${this.props.userId}/posts`],
      // binding to data in the local flux
      mySession: [this.getNexus().local, `session`],
    }
  }
}

The binding is applied/updated during componentDidMount and componentWillReceiveProps. In the latter case, the next bindings are diffed with the previous bindings; removed bindings are unsubscribed, added bindings are subscribed. The underlying fetching/updated mechanism is described in the implementation of Nexus Flux. Basically with the same API you can either subscribe to local data (traditional local stores) or remote data (fetch using GET and receive patches via Websockets/polyfill). You could actually subscribe to data from another window (using postWindow) or a WebWorker/ServiceWorker but I still haven't found a truly useful use case for this.

Long story short, you synchronously describe data deps at the component level using a Flux abstraction and the hooks make sure your dependencies are automatically subscribed, injected on updates, and unsubscribed.

But it also comes with a nice feature: the exact same lifecycle functions are leveraged to perform data prefetching at server-side rendering time. Basically, starting from the root and recusrively from there, React Nexus pre-fetches the bindings, renders the component, and continues with the descendants until all the components are rendered.

@sebmarkbage
Copy link
Collaborator Author

@aaronshaf @gaearon The benefit of making it first class is:

  1. It doesn't eat away at the props namespace. E.g. the higher-order component doesn't need to claim a name like data from your props object that can't use for anything else. Chaining multiple higher order components keeps eating up more names and now you have to find a way to keep those names unique. What if you're composing something that might already be composed and now you have a name conflict?

Besides, I think that best-practice for higher-order components should be to avoid changing the contract of the wrapped component. I.e. conceptually it should be the same props in as out. Otherwise it is confusing to use and debug when the consumer supplies a completely different set of props than is received.

  1. We don't have to use state to store the last value. The data concept is similar to props in the sense that it is purely a memoization. We're free to throw it out at any point if we need to reclaim the memory. For example, in an infinite scroll we might automatically clean up invisible subtrees.

@sebmarkbage
Copy link
Collaborator Author

@RickWong Yes, it would be fairly trivial to support Promises since they're a subset of Observables. We should probably do that to be unopinionated. However, I would still probably recommend against using them. I find that they're inferior to Observables for the following reasons:

A) They can't be canceled automatically by the framework. The best we can do is ignore a late resolution. In the meantime, the Promise holds on to potentially expensive resources. It is easy to get into a thrashy situation of subscribe/cancel/subscribe/cancel... of long running timers/network requests and if you use Promises, they won't cancel at the root and therefore you have to just wait for the resources to complete or timeout. This can be detrimental to performance in large desktop pages (like facebook.com) or latency critical apps in memory constrained environments (like react-native).

B) You're locking yourself into only getting a single value. If that data changes over time, you can't invalidate your views and you end up in an inconsistent state. It is not reactive. For a single server-side render that might be fine, however, on the client you should ideally be designing it in a way that you can stream new data to the UI and automatically update to avoid stale data.

Therefore I find that Observable is the superior API to build from since it doesn't lock you in to fix these issues if you need to.

@sebmarkbage
Copy link
Collaborator Author

@elierotenberg Thanks for chiming in! It does seem very similar indeed. Same kind of benefits. Do you see any limitations with my proposal? I.e. there something missing, that React Nexus has, which you couldn't build on top of this? Would be nice if we didn't lock ourselves out of important use cases. :)

@mridgway
Copy link
Contributor

From the server-rending standpoint, it is important that we're able postpone the final renderToString until the Observable/Promise has been resolved with data that could be fetched asynchronously. Otherwise, we're still in the position of having to do all asynchronous data fetching outside of React without knowing which components will be on the page yet.

I believe react-nexus does allow asynchronous loading to happen within a component before continuing down the render tree.

@elierotenberg
Copy link

Yes, react-nexus explicitly separates:

  1. binding declaration as getNexusBindings (which is a synchronous, side-effect free lifecycle method, similar to render - actually it used to be name renderDependencies but I thought it was confusing),
  2. binding subscription/update as applyNexusBindings (which is synchronous and diffs the previous nexus bindings to determine which new bindings must be subscribed and which ones must be unsubscribed)
  3. binding prefetching as prefetchNexusBindings (which is asynchronous and resolves when the "initial" (whathever this means) value is ready)

ReactNexus.prefetchApp(ReactElement) returns a Promise(String html, Object serializableData). This hook mimicks the construction of the React tree (using instantiateReactComponent) and recursively constructs/prefetches/renders the components. When the whole component tree is 'ready', it finally calls React.renderToString, knowing that all the data is ready (modulo errors). Once resolved, the value of this Promise can be injected in the server response. On the client, the regular React.render() lifecycle works as usual.

@gaearon
Copy link
Collaborator

gaearon commented Mar 15, 2015

If anybody wants to play around with this kind of API, I made a really dumb polyfill for observe as a higher order component:

import React, { Component } from 'react';

export default function polyfillObserve(ComposedComponent, observe) {
  const Enhancer = class extends Component {
    constructor(props, context) {
      super(props, context);

      this.subscriptions = {};
      this.state = { data: {} };

      this.resubscribe(props, context);
    }

    componentWillReceiveProps(props, context) {
      this.resubscribe(props, context);
    }

    componentWillUnmount() {
      this.unsubscribe();
    }

    resubscribe(props, context) {
      const newObservables = observe(props, context);
      const newSubscriptions = {};

      for (let key in newObservables) {
        newSubscriptions[key] = newObservables[key].subscribe({
          onNext: (value) => {
            this.state.data[key] = value;
            this.setState({ data: this.state.data });
          },
          onError: () => {},
          onCompleted: () => {}
        });
      }

      this.unsubscribe();
      this.subscriptions = newSubscriptions;
    }

    unsubscribe() {
      for (let key in this.subscriptions) {
        if (this.subscriptions.hasOwnProperty(key)) {
          this.subscriptions[key].dispose();
        }
      }

      this.subscriptions = {};
    }

    render() {
      return <ComposedComponent {...this.props} data={this.state.data} />;
    }
  };

  Enhancer.propTypes = ComposedComponent.propTypes;
  Enhancer.contextTypes = ComposedComponent.contextTypes;

  return Enhancer;
}

Usage:

// can't put this on component but this is good enough for playing
function observe(props, context) {
  return {
    yourStuff: observeYourStuff(props)
  };
}

class YourComponent extends Component {
  render() {
    // Note: this.props.data, not this.data
    return <div>{this.props.data.yourStuff}</div>;
  }
}

export default polyfillObserve(YourComponent, observe);

@jquense
Copy link
Contributor

jquense commented Mar 15, 2015

Is Observable a concrete, agreed upon thing aside from library implementations? What is the contract, is it simple enough to implement without needing to use bacon or Rxjs? As nice as a first class api for sideloading data would be, it seems weird for React to add an api based on an unspeced/very-initial-specing primitive, given React's steady movement towards plain js. Would something like this ties us to a specific user land implementation?

as an aside why not Streams? I have no horse in the race, but I am honestly wondering; there is already work done on web streams, and of course there is node

@aaronshaf
Copy link

@sebmarkbage
Copy link
Collaborator Author

@jquense There is active work on a proposal for adding Observable to ECMAScript 7(+) so ideally this would become plain JS. https://github.com/jhusain/asyncgenerator (Currently out-of-date.)

We would not take on a dependency on RxJS. The API is trivial to implement yourself without using RxJS. RxJS is the closest to the active ECMAScript proposal.

most.js seems doable too.

Bacon.js' API seems difficult to consume without taking on a dependency on Bacon because of the use of the Bacon.Event types for separating values.

The Stream APIs are too high-level and far removed from this use case.

@RickWong
Copy link

Is there some kind of “await before render" option? I mean on the client it’s not necessary to wait for all Observables before rendering, but on the server you’d want to wait for them to resolve so that each component's render() is full, not partial.

[parent] await observe(). full render(). -> [foreach child] await observe(). full render().

In all of my explorations I found that this is the most important lifecycle hook missing in server-side React.

@elierotenberg
Copy link

Following up this discussion, I've tried to sum up what React Nexus does in the following post:

Ismorphic Apps done right with React Nexus

Heres' the diagram of the core prefetching routine:

React Nexus

@jquense
Copy link
Contributor

jquense commented Mar 15, 2015

We would not take on a dependency on RxJS. The API is trivial to implement yourself without using RxJS. RxJS is the closest to the active ECMAScript proposal.

👍 this is the big concern for me, thinking about say, promises where implementing your own is extremely fraught unless you know what you're doing. I think otherwise you end up with an implicit requirement on a specific lib in the ecosystem. Tangentially...one of the nice things from the promise world is the A+ test suite, so even across libraries there was at least an assurance of a common functionality of .then, which was helpful for promise interop before they were standardized.

@gaearon
Copy link
Collaborator

gaearon commented Mar 15, 2015

this is the big concern for me, thinking about say, promises where implementing your own is extremely fraught unless you know what you're doing. I think otherwise you end up with an implicit requirement on a specific lib in the ecosystem.

Completely agreed. Thankfully observables have a really simple contract, and don't even have built-in methods like then so in a way they're even simpler than promises.

@sebmarkbage
Copy link
Collaborator Author

They might become more complicated (and slower) if the committee insists that calling next schedules a micro-task like Promises.

@jimfb
Copy link
Contributor

jimfb commented Oct 13, 2015

@mitranim is correct, the semantics of #3920 are more "automatic" than Meteor (unsubscription really is automatic), and far simpler since there is literally zero API surface area in the common use case.

@mweststrate
Copy link

@ccorcos @mitranim For a ready to use Tracker / Vue.js inspired library you could try Mobservable, it observers all data accessed during render, and disposes all subscriptions on unmount (until then the subscriptions are kept alive). We applied it successfully to pretty large projects so far at Mendix. It is pretty unobtrusive to your data as well as it decorates existing objects instead of providing its own model objects.

@ccorcos
Copy link

ccorcos commented Oct 13, 2015

Last I checked, Tracker didn't have permanent subscriptions

@mitranim subscriptions can be permanent. Check this out.

sub = Meteor.subscribe('chatrooms')
# this subscription lasts until...
sub.stop()

Now there's some interesting stuff with Tracker. Check this out.

comp = Tracker.autorun ->
  Meteor.subscribe('chatrooms')
# this subscription lasts until...
comp.stop()

The last example isnt terribly useful though. Until we do something like this.

roomId = new ReactiveVar(1)
comp = Tracker.autorun ->
  Meteor.subscribe('messages', roomId.get())
# when I change the roomId, the autorun will re-run
roomId.set(2)
# the subscription to room 1 was stopped and now the subscription to room 2 has started
# the subscription is stopped when I call stop...
comp.stop()

each function that establishes a dependency on a reactive data source (by accessing it) is rerun once when it changes, and that's the end of the subscription.

The subscription lasts until the autorun is rerun (a reactive dependency changed) or the computation it lives in stops. Both of which call the computation.onInvalidate hook.

Here's a super contrived version that accomplished the exact same thing. Hopefully it will help you understand how Tracker works. And maybe you'll also see how messy it can get.

comp = Tracker.autorun ->
  Meteor.subscribe('chatrooms')

# is the same as

comp = Tracker.autorun (c) ->
  sub = null
  Tracker.nonreactive ->
    # dont let comp.stop() stop the subscription using Tracker.nonreactive
    sub = Meteor.subscribe('chatrooms')
  c.onInvalidate ->
    # stop the subscription when the computation is invalidated (re-run)
    sub.stop()
# invalidate and stop the computation
comp.stop()

@mitranim
Copy link

@ccorcos I see the source of confusion now; it was my vocabulary. When talking about subscriptions, I didn't mean Meteor subscriptions. They determine what data is pushed from the server to the client, but aren't relevant to the view layer updates, which is the subject of this discussion. When saying subscription, I was drawing a parallel between traditional event listeners and Tracker's ability to rerun a function that has a dependency on a reactive data source. In the case of React components, that would be a component method that fetches data and then calls setState or forceUpdate.

@mweststrate Thanks for the example, it looks interesting.

@ccorcos
Copy link

ccorcos commented Oct 14, 2015

Ah yes. So they have a clever way of doing it.

componentWillMount: function() {
  this.comp = Tracker.autorun(() => {
    let sub = Meteor.subscribe('messages')
    return {
      loading: !sub.ready(),
      messages: Messages.find().fetch()
    }
  })
componentWillUnmount: function() {
  this.comp.stop()
}

The subscription will just resubscribe each time with no issues. sub.ready and Messages.find.fetch are both "reactive" and will trigger the autorun to re-run whenever they change. Whats cool about Tracker is when you start to hide the autoruns and just have in the documentation that a certain function is within a "reactive context"

you could throw this in a mixin

componentWillMount: function() {
  this.comp = Tracker.autorun(() => {
    return this.getReactiveData()
  })
componentWillUnmount: function() {
  this.comp.stop()
}

And then you're left with this magically reactive function that just works!

getReactiveData: function() {
  let sub = Meteor.subscribe('messages')
  return {
    loading: !sub.ready(),
    messages: Messages.find().fetch()
  }
}

Tracker is pretty cool like this...

@mitranim
Copy link

@ccorcos Turns out I was somewhat wrong about automatic unsubs when using Tracker-style eventing. You still need to stop the listener in componentWillUnmount, otherwise it'll keep reaching for data sources and calling setState (unless you check with isMounted() which is now deprecated).

On a side note, you can create elegant decorators to make component methods reactive. Here's some examples: [1], [2]. Looks like this:

export class Chat extends React.Component {
  @reactive
  updateState () {
    this.setState({
      auth: auth.read(),
      messages: messages.read()
    })
  }

  /* ... */
}

@ccorcos
Copy link

ccorcos commented Oct 26, 2015

@mitranim pretty neat -- I prefer high-order functions though. ;)

@oztune
Copy link

oztune commented Nov 16, 2015

@sebmarkbage @jimfb I've been following this thread and the alt thread (#3858) for a few months now, and am curious if the core team has reached any consensus about this problem, or at least a general direction.

@jimfb
Copy link
Contributor

jimfb commented Nov 16, 2015

@oztune No updates; we've been focused on other priorities. We will post to one of the threads when there is an update on this topic.

@arunoda
Copy link

arunoda commented Jan 3, 2016

Guys, I have a created a universal API to compose containers. Check react-komposer. Which that, we could create containers with a higher order function.

import { compose } from `react-komposer`;

// Create a component to display Time
const Time = ({time}) => (<div>{time}</div>);

// Create the composer function and tell how to fetch data
const composerFunction = (props, onData) => {
    const handler = setInterval(() => {
    const time = new Date().toString();
    onData(null, {time});
  }, 1000);

  const cleanup = () => clearInterval(handler);
  return cleanup;
};

// Compose the container
const Clock = compose(composerFunction)(Time);

// Render the container
ReactDOM.render(<Clock />, document.getElementById('react-root'));

Here's the live version: https://jsfiddle.net/arunoda/jxse2yw8/

We've also has some easy ways to compose containers with Promises, Rx.js Observables and With Meteor's Tracker.

Also check my article on this: Let’s Compose Some React Containers

@oztune
Copy link

oztune commented Jan 3, 2016

@arunoda We wound up doing something very similar. One thing I'm wondering is how do you prevent composerFunction from being called on every prop change?

@arunoda
Copy link

arunoda commented Jan 3, 2016

@oztune Actually now it'll run again. We use this Lokka and Meteor. Both of those have local caches and does not hit the server even when we call the composerFunction multiple times.

But I think we could do something like this:

const options =  {propsToWatch: ["postId"]};
const Clock = compose(composerFunction, options)(Time);

Any ideas?

@oztune
Copy link

oztune commented Jan 3, 2016

@arunoda That's what we tried too, but it creates a bit of a disconnect between the action and its dependencies. We now do something similar to react-async, where, instead of immediately performing the given action, composerFunction would return a function and a key. If the key is different from the previous key returned by composerFunction, the new function will be performed. I'm not sure if this is a tangent from this github thread, so I'd be glad to continue on Twitter (same username).

@arunoda
Copy link

arunoda commented Jan 4, 2016

@oztune I created a new GH issue and let's continue our chat there. Much better than twitter I guess.

@vladap
Copy link

vladap commented Jan 17, 2016

Why not just let JSX to understand and render Observables directly so that I can pass Observable in props, e.g. this.props.todo$ and embed them in JSX. Then I wouldn't need any API, the rest is managed outside of React and HoC is used to fill in observables. It shouldn't matter if props contain plain data or an observable hence no special this.data is needed.

{this.props.todo$

Additionally React render could be able to render Oservable[JSX] which would allow design described in the links without additional library.

https://medium.com/@milankinen/containers-are-dead-long-live-observable-combinators-2cb0c1f06c96#.yxns1dqin

https://github.com/milankinen/react-combinators

@giltig
Copy link

giltig commented Mar 1, 2016

Hello,
As for now we could use stateless components with rxjs streams easily.
I dont understand the need for another API.
I wrote an example - a board that you can move mouse over it and when it gets to 26 it changes to restart.
I would love to hear what you think about this way.
Here is the code:
https://jsfiddle.net/a6ehwonv/74/

@vladap
Copy link

vladap commented Mar 2, 2016

@giltig: I'm learning this way lately as well and like it. Together with Cycle.js.

I would appreciate to be able to listen to event handlers defined on components somehow easily without having to pass in Subjects for bridging. If I understand correctly it is pretty much the other way around than what is suggested here. Alternatively, if we could observe React vdom for its synthetic events, maybe "refs" could be used for observing to avoid having CSS tags just to observe.

Further RxJs could support object in combineLatest so that we could use React functional components directly to create larger components.

const myFancyReactComponent = ({surface, number, gameover}) => (
        <div> 
          {gameover ? gameover : surface}
          {number}
        </div>
)

const LiveApp = Rx.Observable.combineLatest(
    LiveSurface, DynamicNumberView, DynamicGameOver,
    myFancyReactComponent
)

@giltig
Copy link

giltig commented Mar 2, 2016

Hi,
The reason we don't use Cycle or any other framework is that I prefer libraries over frameworks (more control to the developer) and also I want to enjoy the community power that React has.
So many render engines are now developed for React and it's a pity not to use it (ReactNative, ReactDom, ReactThree etc..). It's pretty simple to use just Rxjs and react as the example I've shown above.

It would have made thinks easier if react components could accept pojos as long as observables as props. As for now it's not possible so what I've shown above is the way we chose.

BTW what you did with MyFancyReactComponent is possible and we actually did that too in some cases though you can also write the jsx straightforward.

Regarding subjects - I think it's a valid way because eventually in React component I'm just using a handler function which could be anything. I choose to implement it with subject inside that receives events but someone else can choose otherwise - its flexible.

@lexfrl
Copy link
Contributor

lexfrl commented Nov 20, 2016

It would have made thinks easier if react components could accept pojos as long as observables as props. As for now it's not possible so what I've shown above is the way we chose.

observable props have a no sense in the long run. It has no sense at all in that context, actually..

@gaearon
Copy link
Collaborator

gaearon commented Aug 15, 2018

It sounds to me like we ended up with a different model with Suspense (cache + context). The cache itself might get support for subscriptions. You can track remaining work for Suspense in #13206.

We also provide a subscription package for more isolated cases.

@gaearon gaearon closed this as completed Aug 15, 2018
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