pH200 / cycle-react

Rx functional interface to Facebook's React
MIT License
370 stars 18 forks source link

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

Closed FunkMonkey closed 7 years ago

FunkMonkey commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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:

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

FunkMonkey commented 7 years ago

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 commented 7 years ago

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