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

Custom events should only be subscribed to, when a listener exists #37

Closed
FunkMonkey opened this issue Mar 17, 2017 · 9 comments
Closed

Comments

@FunkMonkey
Copy link

Currently in create-react-class#L144 all custom events are being subscribed to, even if nobody ever listens to them. As a performance optimization, a custom event observable should only be subscribed to, when a listener is passed as a property.

p.s. Thanks for the great library. I'm using it in multiple projects...

@pH200
Copy link
Owner

pH200 commented Mar 19, 2017

Good suggestion. However, doing that means we have to either ignore the changes to event listener props after mounted, or handle the subscription and disposal with componentWillReceiveProps. The former is probably not a good idea. And the later will probably bring more overheads than we saved from this optimization.

I think I need some help here. We can put if (this.props[eventName]) before https://github.com/pH200/cycle-react/blob/master/src/create-react-class.js#L38 . However, I don't really have a good idea how to fix the problems I mentioned.

You can also check ES6 version by checking out branch template-component.

@FunkMonkey
Copy link
Author

I see what you are saying. I believe, there are actually two different use-cases for cycle-react components:

  1. Components, which are used from a non-reactive React application, meaning that the outer application uses the cycle-react component like any other React component, not caring that it was created using cycle-react.
  2. Components, which are used from a reactive React application, meaning a cycle-react component, which is for example used as a child of another cycle-react component.

The current cycle-react implementation serves the first use-case very well. Prop updates are internally rewired to Observables, custom event updates coming from observables can be listened to using normal callback functions.

For the second use-case I would actually prefer to interact with the cycle-react component in a purely reactive way. This means the following things:

  1. Pass input props directly as Observables, which are only subscribed to, when needed
  2. Receive output props (custom events) as Observables, which are only subscribed to, when needed

However, doing that means we have to either ignore the changes to event listener props after mounted

In this second use-case I believe there is no necessity to ever update the props of the component from the outside after passing in the initial props. Observables are passed in for the input and thus don't need updating. Also in a reactive world I dont' really see a point in changing the event listener prop - necessary behaviour changes can be handled within the chain of Observables.

I am not really sure of the best way to implement this. Should the cycle-react API provide two types of components or should this be handled internally? If we have two types of components, we could certainly share a lot of the code...

When handling it internally, I see the following changes:

  • for input Observables: check if a given prop is an Observable and if so don't use the PropsSubject, but return it directly when calling props.get or even better: have them as members on the props object
  • for custom events: provide a new function interactions.toObservable as an alternative for interactions.listener and interactions.getObservable as an alternative to interactions.get
    • getObservable will create a Subject for the given name (if not yet created) and return subject.flatMap( x => x). We may need a replay feature here.
    • toObservable returns a function that, when called during _subscribeCycleEvents will push the according custom event observable (which it received as an argument) into the subject created by getObservable. The custom event will thus only be subscribed to, when the result of getObservable was subscribed to. The function object returned from toObservable must have a special js property to differentiate the function from a normal callback function (like the ones created by interactions.listener).
  • if there was a way to differentiate between both types of components upfront (e.g. an option), then throw an Error when componentWillReceiveProps is called

What do you think? Do you agree with those two use-cases and if so, how would you implement it: additional API or internal changes?

I am also not quite sure, where you are going with the template components... Could you please elaborate?

@pH200
Copy link
Owner

pH200 commented Mar 21, 2017

First of all, thank you very much for your valuable input. And sorry I didn't respond immediately, I was thinking about what's the best solution. I agree with both use-cases. In fact, I think what makes cycle-react different from other libraries is the way we handle events, by separating events from PropsSubject.

That being said, although I think we should probably change the name interactions.listener to effects.dispatch, store.dispatch or source.emit, I think overall this API design is in a good shape. Because, I don't think one should put observable into props (or event prop), that is the source where component fires the event. It should be a source of observable, not where we subscribe the observable.

For example, I think this is a good design and close to what we're doing right now https://github.com/Day8/re-frame/blob/master/examples/simple/src/simple/core.cljs#L67-L73. store.dispatch from redux is the similar idea, too.

I think there are two actions we can take:

  1. Like you said, there's really not a point to change event listener prop. I will ignore the change to that and see how it effects real applications.
  2. To provide an application-wide solution. cycle-react, like cyclejs, can be isolated by component. On the other hand, redux is more like an application framework (correct me if I am wrong). I think people are more likely to use cycle-react to build application, instead of making components which is portable. Providing a way to handle the events for the whole application could be helpful, it saves us the trouble of passing events layers by layers throughout the components.

Template component is my experiment of separating view out of observables. The name "template component" is not very good, so I will change that. However, I think it's a better way to write components.

@FunkMonkey
Copy link
Author

sorry I didn't respond immediately, I was thinking about what's the best solution.

No problem. I prefer the thinking-approach ;)

Because, I don't think one should put observable into props (or event prop), that is the source where component fires the event. It should be a source of observable, not where we subscribe the observable.

Just to prevent a misunderstanding: I did not mean that cycle-react should subscribe to the Observables passed in as props, but only forward them to the definition function directly. Coming from Cycle.js, I like the approach of sources and sinks as input/output, which my proposal for the second use-case of components living in a reactive world is half based on. As props are the main communication layer for both component's inputs and outputs in React, they would have to serve for passing in sources and retrieving sinks. Maybe I should give a (stupid) example of how I imagine a solution for the second use-case could look like:

const Component = Cycle.component( 'Component', ( interactions, props ) => {
  // props.count and props.text are exactly the observables that where passed in
  const view = Rx.Observable.combineLatest( props.count, props.text,
    ( count, text ) => <div> {text}:{count} </div> ); 

  const someEvent = Rx.Observable.interval( 100 );

  return { view, someEvent };
} );

function someFunction()
  const sinks = Cycle.sinks();
  
  const count = Rx.Observable.interval( 1000 );
  const text = Rx.Observable.just( 'foo' );

  const someEventSink = sinks.get( 'someEventName' );
  const subscription = someEventSink.subscribe( ev => console.log( 'Some event happened' );

  return ( 
    <Component 
      count={count} text={text}                    /* sources */
      someEvent={sinks.forward( 'someEventName' )} /* sinks */
    />   
  );
}

I introduced Cycle.sinks simply to be independent of interactions as in the given example there is no outer component that could provide interactions. Its purpose is to connect the cycle (using a Subject), so events can be declared beforehand. Maybe sinks isn't the appropriate name though...

As I said - this would only be my imaginary solution for the second use-case. It also aims at a minimum alteration of the observables within cycle-react. Do you think this React-adapted approach of sources and sinks is a suitable idea?

I think people are more likely to use cycle-react to build application, instead of making components which is portable.

I can imagine people doing both, though I personally am also more interested in using cycle-react for building applications.

Providing a way to handle the events for the whole application could be helpful, it saves us the trouble of passing events layers by layers throughout the components.

I see the point, but I personally disagree. I prefer the Cycle-approach of not having a centralized store or event-system. Though no one is stopping anyone from using redux or even redux-rx / rx-redux in combination with cycle-react.

Template component is my experiment of separating view out of observables. The name "template component" is not very good, so I will change that. However, I think it's a better way to write components.

I think Frint is trying something similar with Observed component (here's the code) using higher order components. Maybe that's the way to go?

Sorry for my extensive response and thank you for your insights.

@pH200
Copy link
Owner

pH200 commented Mar 22, 2017

Cool, I see your point. I think cycle-react does provide a similar solution right now. I changed your code just a little bit:

const Component = Cycle.component( 'Component', ( interactions, props ) => {
  const view = Rx.Observable.combineLatest( props.count, props.text,
    ( count, text ) => <div> {text}:{count} </div> ); 

  const someEvent = Rx.Observable.interval( 100 );

  return {
    view,
    events: { someEvent }
  };
} );

function someFunction()
  const count = Rx.Observable.interval( 1000 );
  const text = Rx.Observable.just( 'foo' );

  const someEventSink = new Rx.Subject();
  const subscription = someEventSink.subscribe( ev => console.log( 'Some event happened' );

  return ( 
    <Component 
      count={count} text={text}                    /* sources */
      someEvent={someEventSink.onNext.bind(someEventSink)} /* sinks */
    />   
  );

The problem is that the sink is not provided by the framework, so I was thinking about the application-wide solution. I think it's a good idea to introduce sink like your example. I will try do implement that idea.

@FunkMonkey
Copy link
Author

OK. Good to see that my ideas aren't too far off ;)

Your example shows that what I intend is nearly possible, though it still has the problem of the original issue: subscribing to all custom events. Also there is currently no way of forwarding errors and completion of custom events in a reactive way.

I think I have a working plan in my mind of how we could achieve both our visions and handle both use-cases. In general I think, cycle-react should actually provide two types of components - one that is closer to Cycle with sources and sinks in the way I proposed and one that is closer to React with updating props and custom event callbacks. I believe the second could easily be implemented on top of the first. I won't have much time the coming days, but I'll try and implement this idea early next week and we'll see if it makes sense or not.

Thanks again!

@pH200
Copy link
Owner

pH200 commented Apr 8, 2017

Fixed in f80fb3c. The idea off working with custom events is tracked in the backlog https://github.com/pH200/cycle-react/projects/1

@pH200 pH200 closed this as completed Apr 8, 2017
@FunkMonkey
Copy link
Author

FunkMonkey commented Oct 20, 2017

Better late than never... while working on a solution that supports the ideas I proposed in this comment, I ended up rewriting everything as I wanted to support RX5 (or any ES Observable compatible library) and made a move to ES7, etc.

There isn't much left of your original code, even though it obviously was a big inspiration!

You can find the repository in reactive-react-component (yes the name is weird, but my idea is to support other frameworks in a similar fashion). It is not yet on npm.

Anyway. I hope it may be of use for you as well and maybe you even have further ideas or critique.

Thanks again for your great library, which as I said, was the main source / inspiration! :)

@pH200
Copy link
Owner

pH200 commented Oct 22, 2017

@FunkMonkey That's a really good job, and honestly I like the decision of not using some strange word for your library. TBH I think the most challenging part would be handling errors in the right way, and make it a lot simpler by using the framework. That's something I found recently, and I think maybe you will find it useful for your framework, too.

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

2 participants