cujojs / most

Ultra-high performance reactive programming
MIT License
3.49k stars 231 forks source link

Using with React #71

Open roman01la opened 9 years ago

roman01la commented 9 years ago

I'm looking for a way of using Most with React, in particular to handle DOM events via React assignment style. Similar solution is presented with RxJS.

My implementation using Most:

function handlerToStream (stream, event) {
  try {
    stream.source.sink.event(performance.now(), event)
  } catch (err) {
    stream.source.sink.error(performance.now(), err);
  }
}

let Button = React.createClass({

  _onClick: handlerToStream.bind(null, (() => {

    let stream = most.create(() => {});
    stream.forEach(event => console.log(event.target));

    return stream;
  })()),
  render() {

    return <button onClick={this._onClick}>click</button>
  }
});

Is there a cleaner way to make this work?

briancavalier commented 9 years ago

Hey @roman01la, good question. Here's a version of Button that seems like it should do what you intended. It's untested, but seems close enough for discussion :) Please let me know if this seems like it's on the right track!

let Button = React.createClass({
    componentWillMount() {
        // emitEvent starts out as a noop, but will be re-assigned below
        // when clickStream is activated
        let emitEvent = () => undefined;
        this._onClick = event => emitEvent(event);

        // Stash the stream somewhere.  This isn't necessary, but it's not
        // clear why we'd use a stream if we don't allow someone else
        // to observe it.
        this.clickStream = most.create(add => emitEvent = add);
        this.clickStream.drain(); // Create demand to activate most.create callback
    } 

    render() {
        return <button onClick={this._onClick}>click</button>
    }
});

This example, and the RxJS one are slightly strange cases. Reactive streams typically facilitate communication between a producer and one or more consumers. In this case, though, the producer and consumer are, in fact, the same component. The RxJS example seems to use that producer-consumer-loopback for every view, and AFAICT, no component consumes a stream produced by another component.

The point of that TodoMVC impl seems to be to try to do as much as possible with RxJS, so I can understand why the author may have chosen to do it that way. Still, it seems like using reactive streams in that way makes the impl more complicated.

I recently did an experiment with TodoMVC, most.js reactive streams, and virtual-dom (ie similar to React). I'm sure it could be improved and simplified, but you might find it interesting, and I'd love to hear your feedback.

The RxJS example also does something else that seems odd to me: it creates 3 subscriptions to the same stream (subscribe and forEach are synonyms in RxJS) to handle a single kind of event, keyDown events. That creates implicit ordering dependencies between the 3 different subscriptions ... for example here and here. There is no explicit dependency established between those two, but rather it relies on the implicit fact that the two subscriptions will be processed in a specific order.

I hope that helps, and please do feel free to discuss!

roman01la commented 9 years ago

@briancavalier Thank you, this looks good. I thought it would be great to have React bindings to Most, similar to RxJS ones and so I did setup a repo starting with this EventHandler helper, a bit improved for cleaner usage.

But there's still a thing about re-assigning of stream pushing function. Because of async creation of a stream, it will not catch a value pushed immediately after. Is there a way to fix this?

briancavalier commented 9 years ago

Because of async creation of a stream, it will not catch a value pushed immediately after. Is there a way to fix this?

Right, that's intentional so that streams consume practically no resources when they're not being observed. In this case, though, it does make for some clumsy capturing, and allows a small window where the emitEvent function points at a noop. The this.clickStream.drain() line will trigger the assignment, but it will still happen async.

I've been thinking about ways most.js can provide an imperative adapter for this kind of case. Check out this gist for one approach I'm considering. A Publisher is basically an imperative push api, and has a stream property which can be given out to consumers. It'd be great to have your feedback on it.

I'll take a look at your React bindings repo as well!

nissoh commented 9 years ago

@briancavalier the todoMVC is absolutley brilliant. It's by far the best example of how to benefit from uni data flow and virtual dom that i've seen so far.

I'm still trying to wrap my head around reactive streams and how to propely maintain application state. It seems to me that the hype is currently striving towards react/flux when it come's to building large applications. My issue with flux is the large ammount of boilerplate that you would have to write. todo app using most.js show's little ammount of boilerplate and very good performance. i would love to see this fully functional. (:

briancavalier commented 9 years ago

Hey @nissoh, thanks very much for the kind words! I agree: reactive programming is a great way to model highly interactive front-ends. Switching from maintaining shared mutable state, to reactive values where state updates flow through the system is definitely a different way of thinking, but has lots of advantages.

I love the simple programming model you get when using most.js + reactive-dom. There's practically no shared state to worry about.

i would love to see this fully functional. (:

I've been meaning to update that TodoMVC and submit it, but I haven't had time lately. When you say "fully functional", do you mean the inline todo editing (I didn't implement that yet)? Or did you mean something more?

TrySound commented 7 years ago

There's recompose library with configurable utilities compatible with most/rxjs/xstream/flyd/kefir/bacon https://github.com/acdlite/recompose/blob/master/docs/API.md

const RegularComponent = componentFromStream(propsStream =>
  propsStream.map(props =>
    <div>{props.value}</div>
  )
)
const enhance = mapPropsStream(propsStream =>
  propsStream.map(props =>
    Object.assign({}, props, { value: 1 })
  )
)
const RegularComponent = enhance(props =>
  <div>{props.value}</div>
);
const { stream, handler } = createEventHandler();
stream
  .scan((s, a) => s + a, 0)

const dom = <div onClick={() => handler(1)}></div>;