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

Open
sebmarkbage opened this Issue Mar 13, 2015 · 135 comments

Comments

Projects
None yet
@sebmarkbage
Member

sebmarkbage commented Mar 13, 2015

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

This comment has been minimized.

Show comment
Hide comment
@josephsavona

josephsavona Mar 13, 2015

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.

Contributor

josephsavona commented Mar 13, 2015

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Mar 13, 2015

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;
}

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Mar 13, 2015

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>;
  }
}

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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 13, 2015

Member

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.

Member

sebmarkbage commented Mar 13, 2015

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

This comment has been minimized.

Show comment
Hide comment
Contributor

vjeux commented Mar 13, 2015

@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Mar 13, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 13, 2015

Member

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.

Member

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

This comment has been minimized.

Show comment
Hide comment
@RickWong

RickWong Mar 14, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@aaronshaf

aaronshaf Mar 14, 2015

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

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

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 14, 2015

Member

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.

Member

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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 14, 2015

Member

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

Member

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

This comment has been minimized.

Show comment
Hide comment
@aaronshaf

aaronshaf Mar 14, 2015

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)
);

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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 14, 2015

Member

@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.

Member

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

This comment has been minimized.

Show comment
Hide comment
@nmn

nmn Mar 14, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@elierotenberg

elierotenberg Mar 14, 2015

(@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.

(@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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 14, 2015

Member

@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.
Member

sebmarkbage commented Mar 14, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 14, 2015

Member

@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.

Member

sebmarkbage commented Mar 14, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 14, 2015

Member

@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. :)

Member

sebmarkbage commented Mar 14, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@mridgway

mridgway Mar 14, 2015

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.

Contributor

mridgway commented Mar 14, 2015

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

This comment has been minimized.

Show comment
Hide comment
@elierotenberg

elierotenberg Mar 14, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 15, 2015

Member

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);
Member

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

This comment has been minimized.

Show comment
Hide comment
@jquense

jquense Mar 15, 2015

Collaborator

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

Collaborator

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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 15, 2015

Member

@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.

Member

sebmarkbage commented Mar 15, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@RickWong

RickWong Mar 15, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@elierotenberg

elierotenberg Mar 15, 2015

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

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

This comment has been minimized.

Show comment
Hide comment
@jquense

jquense Mar 15, 2015

Collaborator

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.

Collaborator

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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 15, 2015

Member

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.

Member

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

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 15, 2015

Member

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

Member

sebmarkbage commented Mar 15, 2015

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

@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Mar 15, 2015

That would bother some a lot of pattern are based on the fact that onNext is synchronous in RxJS :/

That would bother some a lot of pattern are based on the fact that onNext is synchronous in RxJS :/

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 16, 2015

Member

I think a common Flux store pattern might be to keep a Map of Observables on a per-key basis so that they can be reused. Then clean them up when everyone is unsubscribed.

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

Member

sebmarkbage commented Mar 16, 2015

I think a common Flux store pattern might be to keep a Map of Observables on a per-key basis so that they can be reused. Then clean them up when everyone is unsubscribed.

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

@RickWong

This comment has been minimized.

Show comment
Hide comment
@RickWong

RickWong Mar 16, 2015

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

Would using this.props.key (gone I know) make sense? In most cases you already pass such unique identifier with <... key={child.id} .. />.

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

Would using this.props.key (gone I know) make sense? In most cases you already pass such unique identifier with <... key={child.id} .. />.

@elierotenberg

This comment has been minimized.

Show comment
Hide comment
@elierotenberg

elierotenberg Mar 16, 2015

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

That's the pattern I use for React Nexus, too. Store#observe returns a memoized, immutable observer; it is cleaned-up (including relevant backend-specific cleanup mechanism, such as sending an actual "unsubscribe" message or whatever) when all subscribers are gone for at least one tick.

That way you can do things like: MyStore.get(this.props.someID) and always get back the same Observable.

That's the pattern I use for React Nexus, too. Store#observe returns a memoized, immutable observer; it is cleaned-up (including relevant backend-specific cleanup mechanism, such as sending an actual "unsubscribe" message or whatever) when all subscribers are gone for at least one tick.

@jeffbski

This comment has been minimized.

Show comment
Hide comment
@jeffbski

jeffbski Mar 20, 2015

Contributor

@sebmarkbage @gaearon How would observe work on the server in v0.14?
Would it be able to properly wait for all the observers to resolve before rendering to string similar to how react-nexus does (but built in to react)?

Contributor

jeffbski commented Mar 20, 2015

@sebmarkbage @gaearon How would observe work on the server in v0.14?
Would it be able to properly wait for all the observers to resolve before rendering to string similar to how react-nexus does (but built in to react)?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 20, 2015

Member

IMO it would be great if components waited for the first observed value before being “ready” for rendering on server.

Member

gaearon commented Mar 20, 2015

IMO it would be great if components waited for the first observed value before being “ready” for rendering on server.

@RickWong

This comment has been minimized.

Show comment
Hide comment
@RickWong

RickWong Mar 20, 2015

@gaearon: IMO it would be great if components waited for the first observed value before being “ready” for rendering on server.

Yes, 👍 for asynchronous rendering. In the meantime react-async by @andreypopp is an alternative, but it requires fibers to "hack" React. It would be great if React could support asynchronous rendering out-of-the-box.

@gaearon: IMO it would be great if components waited for the first observed value before being “ready” for rendering on server.

Yes, 👍 for asynchronous rendering. In the meantime react-async by @andreypopp is an alternative, but it requires fibers to "hack" React. It would be great if React could support asynchronous rendering out-of-the-box.

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Mar 20, 2015

Member
Member

sebmarkbage commented Mar 20, 2015

@mweststrate

This comment has been minimized.

Show comment
Hide comment
@mweststrate

mweststrate Jul 12, 2015

MOBservable follows a very similar pattern to refresh a component side-ways once some observable values are changed, so it seems that the current lifecycle methods + decorators offer enough flexibility for third party libraries to express these kind of patterns and imho a third data source concept would only over complicate things.

    componentWillMount: function() {
        var baseRender = this.render;
        this.render = function() {
            if (this._watchDisposer)
                this._watchDisposer();
            var[rendering, disposer] = mobservableStatic.watch(() => baseRender.call(this), () => {
                    this.forceUpdate();
            });
            this._watchDisposer = disposer;
            return rendering;
        }
    },

MOBservable follows a very similar pattern to refresh a component side-ways once some observable values are changed, so it seems that the current lifecycle methods + decorators offer enough flexibility for third party libraries to express these kind of patterns and imho a third data source concept would only over complicate things.

    componentWillMount: function() {
        var baseRender = this.render;
        this.render = function() {
            if (this._watchDisposer)
                this._watchDisposer();
            var[rendering, disposer] = mobservableStatic.watch(() => baseRender.call(this), () => {
                    this.forceUpdate();
            });
            this._watchDisposer = disposer;
            return rendering;
        }
    },
@jimfb

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Jul 12, 2015

Contributor

@Mitranim I agree, that's a really good read, thanks for finding that! It's effectively what #3920 is suggesting.

We are undecided on which proposal is better. Having played with both, I'm mostly convinced that reactive programming (as you linked; https://github.com/meteor/meteor/wiki/Tracker-Manual) is the way to go, but we haven't reached consensus and we're still trying to figure out what makes the most sense, so feedback is welcomed.

Contributor

jimfb commented Jul 12, 2015

@Mitranim I agree, that's a really good read, thanks for finding that! It's effectively what #3920 is suggesting.

We are undecided on which proposal is better. Having played with both, I'm mostly convinced that reactive programming (as you linked; https://github.com/meteor/meteor/wiki/Tracker-Manual) is the way to go, but we haven't reached consensus and we're still trying to figure out what makes the most sense, so feedback is welcomed.

@ccorcos

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos Oct 12, 2015

@Mitranim @jimfb I've been a huge Meteor fan for several years now. Tracker is really cool and very fascinating. I created a presentation demonstrating how a very simple version of tracker works:

https://github.com/ccorcos/meteor-track/blob/master/client/main.js

And I even created an observable streams library with Tracker:

https://github.com/ccorcos/meteor-tracker-streams

But as I've fully transitioned to using React instead of Blaze, I've come to realize that Tracker is just too complicated and sometimes a simple publish/subsrcribe/onChange methods is just 100x easier.

Also, for all the functional programming fans, Tracker comes with some side-effects that make sense 99% of the time, but sometimes they get you. Here's how it would look in a React component

componentWillMount: ->
  @c = Tracker.autorun =>
    @setState({loading: true})
    Meteor.subscribe 'users', => @setState({loading: false})
    Tracker.autorun =>
      @setState({users: Users.find({}, {sort:{name:-1}}).fetch()})
componentWillUnmount: ->
  @c.stop()

My point about side-effects is that c.stop() will stop the subscription and the inner autorun as well.

ccorcos commented Oct 12, 2015

@Mitranim @jimfb I've been a huge Meteor fan for several years now. Tracker is really cool and very fascinating. I created a presentation demonstrating how a very simple version of tracker works:

https://github.com/ccorcos/meteor-track/blob/master/client/main.js

And I even created an observable streams library with Tracker:

https://github.com/ccorcos/meteor-tracker-streams

But as I've fully transitioned to using React instead of Blaze, I've come to realize that Tracker is just too complicated and sometimes a simple publish/subsrcribe/onChange methods is just 100x easier.

Also, for all the functional programming fans, Tracker comes with some side-effects that make sense 99% of the time, but sometimes they get you. Here's how it would look in a React component

componentWillMount: ->
  @c = Tracker.autorun =>
    @setState({loading: true})
    Meteor.subscribe 'users', => @setState({loading: false})
    Tracker.autorun =>
      @setState({users: Users.find({}, {sort:{name:-1}}).fetch()})
componentWillUnmount: ->
  @c.stop()

My point about side-effects is that c.stop() will stop the subscription and the inner autorun as well.

@jimfb

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Oct 12, 2015

Contributor

@ccorcos Yes, in #3920 all the side-effects would be completely internal to React. In fact, React core could implement them in a completely immutable/functional way that would not perform any mutations. But that's an implementation detail, since the side effects wouldn't be externally visible anyway.

Contributor

jimfb commented Oct 12, 2015

@ccorcos Yes, in #3920 all the side-effects would be completely internal to React. In fact, React core could implement them in a completely immutable/functional way that would not perform any mutations. But that's an implementation detail, since the side effects wouldn't be externally visible anyway.

@ccorcos

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos Oct 13, 2015

Interesting... So why not just use onChange callbacks? I've found those to be the most compatible. Whether you're using Meteor (Tracker), RxJS, Highland.js, etc., you can always integrate with them trivially with an event callback. And same with a React high-order component.

What I like about Redux is it keeps this logic out of the framework and keeps React components as pure functions.

ccorcos commented Oct 13, 2015

Interesting... So why not just use onChange callbacks? I've found those to be the most compatible. Whether you're using Meteor (Tracker), RxJS, Highland.js, etc., you can always integrate with them trivially with an event callback. And same with a React high-order component.

What I like about Redux is it keeps this logic out of the framework and keeps React components as pure functions.

@jimfb

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Oct 13, 2015

Contributor

@ccorcos The main problem is that when a component instance gets destroyed, all the "subscriptions" need to be cleaned up. Component authors often forget about this cleanup, thus creating a memory leak. We want it to be automatic, which makes it easier to write (less boilerplate) and less error-prone (cleanup is automatic).

Contributor

jimfb commented Oct 13, 2015

@ccorcos The main problem is that when a component instance gets destroyed, all the "subscriptions" need to be cleaned up. Component authors often forget about this cleanup, thus creating a memory leak. We want it to be automatic, which makes it easier to write (less boilerplate) and less error-prone (cleanup is automatic).

@Mitranim

This comment has been minimized.

Show comment
Hide comment
@Mitranim

Mitranim Oct 13, 2015

@jimfb Exactly. Automatic unsubscription is one of the really neat things about Tracker's approach. Each call to the reactive data source re-establishes the subscription, and once the component stops calling for data, there's nothing to clean up!

@jimfb Exactly. Automatic unsubscription is one of the really neat things about Tracker's approach. Each call to the reactive data source re-establishes the subscription, and once the component stops calling for data, there's nothing to clean up!

@ccorcos

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos Oct 13, 2015

Yeah, but its not really "automatically" unsubscribed. You have to call c.stop() in component will unmount. You can abstract that away with a mixin though.

componentWillMount: ->
  @autorun =>
    @setState({loading: true})
    Meteor.subscribe 'users', => @setState({loading: false})
    Tracker.autorun =>
      @setState({users: Users.find({}, {sort:{name:-1}}).fetch()})

But this is really no different from any other api. You can create a built-in method that automatically unsubscribes for you on unmount, etc. Look, I'm a huge Meteor fan. So I don't want to talk you out of this. Its just that sometimes, I find that its really hard to explain this stuff to someone who's unfamiliar with it. Meanwhile, using simple listeners / event emitters are much more simple to understand and implement, and they tend to be very compatible with whatever reactivity system you want to use...

ccorcos commented Oct 13, 2015

Yeah, but its not really "automatically" unsubscribed. You have to call c.stop() in component will unmount. You can abstract that away with a mixin though.

componentWillMount: ->
  @autorun =>
    @setState({loading: true})
    Meteor.subscribe 'users', => @setState({loading: false})
    Tracker.autorun =>
      @setState({users: Users.find({}, {sort:{name:-1}}).fetch()})

But this is really no different from any other api. You can create a built-in method that automatically unsubscribes for you on unmount, etc. Look, I'm a huge Meteor fan. So I don't want to talk you out of this. Its just that sometimes, I find that its really hard to explain this stuff to someone who's unfamiliar with it. Meanwhile, using simple listeners / event emitters are much more simple to understand and implement, and they tend to be very compatible with whatever reactivity system you want to use...

@Mitranim

This comment has been minimized.

Show comment
Hide comment
@Mitranim

Mitranim Oct 13, 2015

@ccorcos We might be talking about different mechanisms. Last I checked, Tracker didn't have permanent subscriptions; 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. If it accesses the data source again, this re-establishes the "subscription" for one more change. And so on.

@ccorcos We might be talking about different mechanisms. Last I checked, Tracker didn't have permanent subscriptions; 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. If it accesses the data source again, this re-establishes the "subscription" for one more change. And so on.

@jimfb

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Oct 13, 2015

Contributor

@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.

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

This comment has been minimized.

Show comment
Hide comment
@mweststrate

mweststrate Oct 13, 2015

@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 @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

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos 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()

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

This comment has been minimized.

Show comment
Hide comment
@Mitranim

Mitranim Oct 14, 2015

@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 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

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos 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...

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

This comment has been minimized.

Show comment
Hide comment
@Mitranim

Mitranim Oct 23, 2015

@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 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

This comment has been minimized.

Show comment
Hide comment
@ccorcos

ccorcos Oct 26, 2015

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

ccorcos commented Oct 26, 2015

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

@oztune

This comment has been minimized.

Show comment
Hide comment
@oztune

oztune 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.

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

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Nov 16, 2015

Contributor

@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.

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

This comment has been minimized.

Show comment
Hide comment
@arunoda

arunoda 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

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

This comment has been minimized.

Show comment
Hide comment
@oztune

oztune 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?

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

This comment has been minimized.

Show comment
Hide comment
@arunoda

arunoda 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?

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

This comment has been minimized.

Show comment
Hide comment
@oztune

oztune 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).

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

This comment has been minimized.

Show comment
Hide comment
@arunoda

arunoda Jan 4, 2016

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

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

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap 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

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

This comment has been minimized.

Show comment
Hide comment
@giltig

giltig 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/

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

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap 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
)

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

This comment has been minimized.

Show comment
Hide comment
@giltig

giltig 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.

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.

@fckt

This comment has been minimized.

Show comment
Hide comment
@fckt

fckt Nov 20, 2016

Contributor

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..

Contributor

fckt 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 gaearon referenced this issue Jan 30, 2017

Closed

React 16 Umbrella ☂️ (and 15.5) #8854

117 of 120 tasks complete

@bfred-it bfred-it referenced this issue Jul 19, 2018

Merged

Hide comments in fewer clicks #1428

5 of 6 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment