Use RxJS with React
@MichalZalecki
Reactive
means
awesome
promises
await/async
RxJS
callbacks
async awesomeness
reactive
\rē-ˈak-tiv\
done in response to a problem or situation
‒ merriam-webster.com
Reactive JavaScript
http://slides.com/michalzalecki/reactive-javascript
Reactive state
// createState.js
import Rx from "rxjs";
function createState(reducer$,
initialState$ = Rx.Observable.of({})) {
return initialState$
.merge(reducer$)
.scan((state, reducer) => reducer(state))
.publishReplay(1)
.refCount();
}
export default createState;
Reactive state
it("creates state$ with initialState$", done => {
const reducer$ = new Rx.Subject();
const initialState$ = Rx.Observable.of({ counter: -10 });
const reducer = state => ({ ...state, counter: state.counter + 1 });
const state$ = createState(reducer$, initialState$);
state$.toArray().subscribe(results => {
expect(results).toEqual([
{ counter: -10 },
{ counter: -9 },
{ counter: -8 },
{ counter: -7 },
]);
}, () => {}, done);
reducer$.next(reducer);
reducer$.next(reducer);
reducer$.next(reducer);
reducer$.complete();
});
Actions, ActionCreators, Constants…
// CounterActions.js
import Rx from "rxjs";
const CounterActions = {
increment$: new Rx.Subject,
decrement$: new Rx.Subject,
};
export default CounterActions;
Reducer($)
// CounterReducer.js
import Rx from "rxjs";
import CounterActions from "app/actions/CounterActions";
const CounterReducer$ = Rx.Observable.merge(
CounterActions.increment$.map((n = 1) =>
state => ({ ...state, counter: state.counter+n })),
CounterActions.decrement$.map((n = 1) =>
state => ({ ...state, counter: state.counter-n }))
);
export default CounterReducer$;
Reducer($)
// state.js
import Rx from "rxjs";
import createState from "app/rx-state/createState";
import CounterReducer$ from "app/reducers/CounterReducer";
// import OtherReducer$ from "app/reducers/OtherReducer";
// import Other2Reducer$ from "app/reducers/Other2Reducer";
// import Other3Reducer$ from "app/reducers/Other3Reducer";
// ...
const reducer$ = Rx.Observable.merge(
CounterReducer$
// OtherReducer$,
// Other2Reducer$,
// Other3Reducer$
);
const initialState$ = Rx.Observable.of({ counter: 0 });
export default createState(reducer$, initialState$);
testReducer
it("increments counter", done => {
testReducer(CounterReducer$, [1, 4, 5],
{ counter: 0 }, v => v.counter)
.subscribe(() => {}, () => {}, done);
CounterActionsMock.increment$.next(1);
CounterActionsMock.increment$.next(3);
CounterActionsMock.increment$.next(1);
CounterActionsMock.increment$.next(1);
});
testReducer
function testReducer($reducer, values, initialState = {},
selector = v => v) {
const nextValues = [...values];
const observable = $reducer
.scan((state, reducer) => reducer(state), initialState)
.map(selector)
.take(values.length);
observable.subscribe({
next(val) { expect(val).toEqual(values.shift()) },
error(err) { throw err },
});
return observable;
}
export default testReducer;
connect
// connect.js
import React from "react";
function connect(state$, selector = (state) => state) {
return function wrapWithConnect(WrappedComponent) {
return class Connect extends React.Component {
constructor(props) {
super(props);
state$.take(1).map(selector).subscribe(state => this.state = state);
}
componentDidMount() {
this.subscription = state$.map(selector).subscribe(::this.setState);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.state} {...this.props} />
);
}
};
}
}
export default connect;
connect
it("creates connected component with selector", () => {
const selector = state => ({ counter: state.counter*2 });
const WrappedComponent = connect(state$, selector)(Component);
const tree = TestUtils.renderIntoDocument(<WrappedComponent />);
const heading = TestUtils
.findRenderedDOMComponentWithClass(tree, "heading");
expect(heading.textContent).toEqual("");
state$.next({ counter: 10 });
expect(heading.textContent).toEqual("20");
state$.next({ counter: 20 });
expect(heading.textContent).toEqual("40");
});
Component
// Counter.jsx
import React from "react";
import state$ from "app/rx-state/state";
import connect from "app/rx-state/connect";
import CounterActions from "app/actions/CounterActions";
class Counter extends React.Component {
render() {
return (
<div>
<h1>{ this.props.counter }</h1>
<hr/>
<button onClick={ () => this.props.increment(1) }>+</button>
<button onClick={ () => this.props.increment(10) }>+10</button>
<button onClick={ () => this.props.decrement(1) }>-</button>
<button onClick={ () => this.props.decrement(10) }>-10</button>
</div>
);
}
}
export default connect(state$, state => ({
counter: state.counter,
increment(n) { CounterActions.increment$.next(n) },
decrement(n) { CounterActions.decrement$.next(n) }
}))(Counter);
Component
it("increments by 10 on \"+10\" button click", () => {
const increment = jasmine.createSpy();
const tree = TestUtils
.renderIntoDocument(
<Counter counter={10} increment={increment} decrement={() => {}} />
);
const button = TestUtils
.findRenderedDOMComponentWithClass(tree, "counter__button--i10");
TestUtils.Simulate.click(button);
expect(increment).toHaveBeenCalledWith(10);
});
Async
UserActions.fetch$.flatMap(userId => {
return Rx.Observable.ajax(`/users/${userId}`)
});
UserActions.fetch$.concatMap(userId => {
return Rx.Observable.ajax(`/users/${userId}`)
.retryWhen(err$ => err$.delay(1000).take(10));
});
Questions?
react-form
import Superform from "react-superform";
class MyForm extends Superform {
onSuccessSubmit(data) {
console.log(data);
}
onErrorSubmit(errors, data) {}
render() {
return (
<form noValidate onSubmit={ this.handleSubmit.bind(this) }>
<input
type="email"
ref="email"
name="email"
valueLink={ this.linkStateOf("email") }
required
/>
<p className="error">{ this.getErrorMessageOf("email") }</p>
<input type="submit" />
</form>
);
}
}
ReactDOM.render(<MyForm />, document.getElementById("root"));
Use RxJS with React
By Michał Załęcki
Use RxJS with React
- 43,483