Reputation: 1581
can someone help me how to bind React State to RxJS Observable? I did sth like
componentDidMount() {
let source = Rx.Observable.of(this.state.val)
}
The ideal result is, whenever this.state.val
updated (via this.setState(...)
) source
get updated too, so I can combine source
with other RxJS observable stream.
However, in this case, source
only get updated once, even after this.state.val
is updated and component is re-rendered.
// Ideal result:
this.state.val = 1
source.subscribe(val => console.log(x)) //=> 1
this.state.val = 2
source.subscribe(val => console.log(val)) //=> 2
// Real result:
this.state.val = 1
source.subscribe(val => console.log(x)) //=> 1
this.state.val = 2
source.subscribe(val => console.log(val)) //=> 1 ???WTH
It might be because componentDidMount()
only invoked once in React lifetime. so I move source
to componentDidUpdate()
which is invoked everytime after component is rendered. However, the result still remain the same.
So the question is how to make source
updated whenever this.state.val
updated?
Updated: Here is the solution I used to solve the prob, using Rx.Subject
// Component file
constructor() {
super(props)
this.source = new Rx.Subject()
_onChangeHandler(e) {
this.source.onNext(e.target.value)
}
componentDidMount() {
this.source.subscribe(x => console.log(x)) // x is updated
}
render() {
<input type='text' onChange={this._onChangeHandler} />
}
//
Upvotes: 4
Views: 3645
Reputation: 795
You can do it using hooks.
Here is a code sample
import { Observable, Subscription } from 'rxjs';
import { useState, useEffect } from 'react';
export default function useObservable<T = number | undefined>(
observable: Observable<T | undefined>,
initialState?: T): T | undefined {
const [state, setState] = useState<T | undefined>(initialState);
useEffect(() => {
const subscription: Subscription = observable.subscribe(
(next: T | undefined) => {
setState(next);
},
error => console.log(error),
() => setState(undefined));
return () => subscription.unsubscribe();
}, [observable])
return state;
}
Upvotes: 0
Reputation: 96
I would highly recommend reading this blog post on streaming props to a React component using RxJS:
It uses FrintJS, and applies the observe
higher-order component for returning the props as a stream:
import React from 'react';
import { Observable } from 'rxjs';
import { observe } from 'frint-react';
function MyComponent(props) {
return <p>Interval: {props.interval}</p>;
}
export default observe(function () {
// return an Observable emitting a props-compatible object here
return Observable.interval(1000)
.map(x => ({ interval: x }));
})(MyComponent);
Upvotes: 0
Reputation: 3673
To abstract out some of the below complexity, use recompose's mapPropsStream or componentFromStream. E.g.
const WithMouseMove = mapPropsStream((props$) => {
const { handler: mouseMove, stream: mouseMove$ } = createEventHandler();
const mousePosition$ = mouseMove$
.startWith({ x: 0, y: 0 })
.throttleTime(200)
.map(e => ({ x: e.clientX, y: e.clientY }));
return props$
.map(props => ({ ...props, mouseMove }))
.combineLatest(mousePosition$, (props, mousePosition) => ({ ...props, ...mousePosition }));
});
const DumbComponent = ({ x, y, mouseMove }) => (
<div
onMouseMove={mouseMove}
>
<span>{x}, {y}</span>
</div>
);
const DumbComponentWithMouseMove = WithMouseMove(DumbComponent);
For a slightly updated answer to the OP's updated answer, using rxjs5, I came up with the following:
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.mouseMove$ = new Rx.Subject();
this.mouseMove$.next = this.mouseMove$.next.bind(this.mouseMove$);
this.mouseMove$
.throttleTime(1000)
.subscribe(idx => {
console.log('throttled mouse move');
});
}
componentWillUnmount() {
this.mouseMove$.unsubscribe();
}
render() {
return (
<div
onMouseMove={this.mouseMove$.next}
/>
);
}
}
Some notable additions:
onNext()
is now next()
next
method allows it to be passed directly to the mouseMove handlercomponentWillUnmount
hookFurthermore, the subject streams initialized in the component constructor
hook can be passed as properties to 1+ child component(s), which could all push to the stream using any of the observable next/error/complete methods. Here's a jsbin example I put together demonstrating multiple event streams shared between multiple components.
Curious if anyone has ideas on how to better encapsulate this logic to simplify stuff like binding and unsubscribing.
Upvotes: 3
Reputation: 4585
Although a subject will work, I think the best practice is to avoid using a subject when you can use an observable. In this case you can use Observable.fromEvent
:
class MouseOverComponent extends React.Component {
componentDidMount() {
this.mouseMove$ = Rx.Observable
.fromEvent(this.mouseDiv, "mousemove")
.throttleTime(1000)
.subscribe(() => console.log("throttled mouse move"));
}
componentWillUnmount() {
this.mouseMove$.unsubscribe();
}
render() {
return (
<div ref={(ref) => this.mouseDiv = ref}>
Move the mouse...
</div>
);
}
}
ReactDOM.render(<MouseOverComponent />, document.getElementById('app'));
Here it is on codepen....
It seems to me that there are other times when a Subject like the best choice, like when a custom React component executes a function when an event occurs.
Upvotes: 1
Reputation: 18665
One option could be to use Rx.Observable.ofObjectChanges
> cf. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/ofobjectchanges.md
.
However :
Object.observe
which is not a standard feature, hence will have to be polyfilled in some browsers and is actually being removed from inclusion in ecmascript (cf. http://www.infoq.com/news/2015/11/object-observe-withdrawn). Not the choice for the future, but it is easy to use, so if it is just for your own needs, why not.Other option is to use a subject in one of the three methods at your disposal according to your use case : shouldComponentUpdate
, componentWillUpdate
, componentDidUpdate
. Cf. https://facebook.github.io/react/docs/component-specs.html for when each function is executed. In one of these methods, you would check if this.state.val
has changed, and emits its new value on the subject if it did.
I am not a reactjs
specialist, so I guess they might be other options.
Upvotes: 2