At first sight, RxJS is blown up lodash but for dealing also with async. In reality, it is so much more than that. With a few simple operators, you can implement a Redux-like state machine, schedule animation or deal with any type of events no matter whether it is WebSocket message or filling in the text input.
Angular is armed with RxJS out of the box, but how about its great competitor? I like React so much not only for what it is but also for that what you can make of it. While preparing for a presentation about Reactive JavaScript at meet.js Summit I have been asked couple times how to integrate RxJS into an existing app. All in all, there is a small chance that you are allowed to rewrite the app in Cycle.js.
Update I have decided to do a full rewrite. From now on createState
supports scoped reducers
and few helpers were added. What is more, state for connect
is passed
in Provider
context like in Redux.
Reactive state
Redux does the job and it is way simpler than its precursor. In real live, an app written with Flux has
many stores, but it leads to unnecessary complexity and managing state dependencies. Redux with
single store gained popularity really quickly. How about no store at all? With reactive state we
do not need passive store. We can replace createStore
with createState
. Observable state will
update components calling setState
, or better, with props
.
// createState
function createState(reducer$, initialState$ = Rx.Observable.of({})) {
return initialState$
.merge(reducer$)
.scan((state, [scope, reducer]) =>
({ ...state, [scope]: reducer(state[scope]) }))
.publishReplay(1)
.refCount();
}
Thanks to calling publishReplay
, refCount
we'll share the same state among all observers. In
RxJS 4 you could achieve the same with shareReplay
.
test("createState creates reactive state using scoped reducers", (t) => {
const add$ = new Rx.Subject();
const counterReducer$ = add$.map(payload => state => state + payload);
const rootReducer$ = counterReducer$.map(counter => ["counter", counter]);
const state$ = createState(rootReducer$, Rx.Observable.of({ counter: 10 }));
t.plan(1);
add$.next(1); // No subscribers yet
state$.toArray().subscribe((results) => {
t.deepEqual(results, [{ counter: 10 }, { counter: 12 }]);
});
add$.next(2);
add$.complete();
});
Actions, ActionCreators, Constants...
How about just actions? Subjects actually. Subject is both at the same time, observable and observer.
// counterActions
import { createActions } from "../state/RxState";
export default createActions(["increment", "decrement", "reset"]);
// which is the same as
export default {
increment: new Rx.Subject,
decrement: new Rx.Subject,
reset: new Rx.Subject,
};
Normally it would not be the best choice to drop the idea of this separation. We can take advantage
of consuming observables, using simple map
operator and later change an action into a reducer.
This eliminates the need for constants and those silly tests you would have to write. I do not like
the idea of testing basically a syntax.
Reducer($)
This part actually is going to be significantly different from what you already know about reducers. In Redux, reducers are pure functions which return new state if they can handle particular actions.
// CounterReducer
import Rx from "rxjs";
import counterActions from "../actions/counterActions";
const initialState = 0;
const CounterReducer$ = Rx.Observable.of(() => initialState)
.merge(
counterActions.increment.map(payload => state => state + payload),
counterActions.decrement.map(payload => state => state - payload),
counterActions.reset.map(_payload => _state => initialState),
);
export default CounterReducer$;
Reducers, CounterReducer$
actually, is a stream of lazy-evaluated, pure functions. We can take data
carried by an action and change it into single, pure function.
import test from "ava";
import { pipe } from "ramda";
import counterActions from "../actions/counterActions";
import CounterReducer$ from "./CounterReducer";
test("handles increment, decrement and reset actions", (t) => {
CounterReducer$.take(5).toArray().subscribe((fns) => {
t.is(pipe(...fns)(), 9);
});
counterActions.increment.next(1);
counterActions.reset.next();
counterActions.increment.next(10);
counterActions.decrement.next(1);
});
As you can see testing such reducer is really simple with little ramda help.
connect
Firstly, we do not want to tight couple components with any modules. Components props can be treated as a simple, yet powerful, dependency injection. The question is how to extract action creators (action subject in our case) so the implementation details will not leak.
Redux already solved this problem with connect
from
react-redux. Our implementation will be simpler but still
fully functional for our reactive use case.
// connect
export function connect(selector = state => state, actionSubjects) {
const actions = Object.keys(actionSubjects)
.reduce((akk, key) => ({ ...akk, [key]: value => actionSubjects[key].next(value) }), {});
return function wrapWithConnect(WrappedComponent) {
return class Connect extends Component {
static contextTypes = {
state$: PropTypes.object.isRequired,
};
componentWillMount() {
this.subscription = this.context.state$.map(selector).subscribe(::this.setState);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.state} {...this.props} {...actions} />
);
}
};
};
}
Connect
component is wrapping component to pass state to props. selector
is a deadly simple
function to map global state to component props. actionSubjects
is an object of subjects returned
from createActions
which hides the boilerplate of calling .next()
, we do not have to call dispatch
.
test("connect maps state to props in Provider context", (t) => {
const add$ = new Rx.Subject();
const counterReducer$ = add$.map(payload => state => state + payload);
const rootReducer$ = counterReducer$.map(counter => ["counter", counter]);
const state$ = createState(rootReducer$, Rx.Observable.of({ counter: 10 }));
const Counter = ({ counter, add }) => (
<div>
<h1>{counter}</h1>
<button onClick={() => add(1)}>add</button>
</div>
);
const ConnectedCounter = connect(state => ({ counter: state.counter }), { add: add$ })(Counter);
const tree = mount(
<Provider state$={state$}>
<ConnectedCounter />
</Provider>
);
t.is(tree.find("h1").text(), "10");
tree.find("button").simulate("click");
t.is(tree.find("h1").text(), "11");
});
Reactive components
import React from "react";
import PropTypes from "prop-types";
import { connect } from "../state/RxState";
import counterActions from "../actions/counterActions";
export const Counter = ({ counter, increment, decrement, reset }) => (
<div>
<h1>{counter}</h1>
<hr />
<button onClick={() => increment(1)} id="increment">+</button>
<button onClick={() => increment(10)} id="increment10">+10</button>
<button onClick={reset} id="reset">Reset</button>
<button onClick={() => decrement(1)} id="decrement">-</button>
<button onClick={() => decrement(10)} id="decrement10">-10</button>
</div>
);
Counter.propTypes = {
counter: PropTypes.number.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
};
export default connect(({ counter }) => ({ counter }), counterActions)(Counter);
We wrapped Counter
in higher-order components.
The main advantage is that it then can be a stateless component which is a pure
function. Pure functions are highly reusable, easy to test and maintain.
import React from "react";
import test from "ava";
import sinon from "sinon";
import { shallow } from "enzyme";
import { Counter } from "./Counter";
test("displays counter", (t) => {
const increment = sinon.spy();
const decrement = sinon.spy();
const reset = sinon.spy();
const tree = shallow(
<Counter
counter={123}
increment={increment}
decrement={decrement}
reset={reset}
/>
);
t.is(tree.find("h1").text(), "123");
});
test("calls passed actions", (t) => {
const increment = sinon.spy();
const decrement = sinon.spy();
const reset = sinon.spy();
const tree = shallow(
<Counter
counter={123}
increment={increment}
decrement={decrement}
reset={reset}
/>
);
tree.find("#increment").simulate("click");
t.true(increment.calledWith(1));
tree.find("#increment10").simulate("click");
t.true(increment.calledWith(10));
tree.find("#reset").simulate("click");
t.true(reset.called);
tree.find("#decrement").simulate("click");
t.true(decrement.calledWith(1));
tree.find("#decrement10").simulate("click");
t.true(decrement.calledWith(10));
});
Async
How to handle AJAX calls? According to good practices from Flux or Redux the initial call to the server starts in the action creator. What do we do when we do not have action creators? We can init the request in the reducer. While it does not sound great, keep in mind that we are dealing with one stream and whether an operation is async or not is irrelevant.
UserActions.fetch$.flatMap(userId => {
return Rx.Observable.ajax(`/users/${userId}`)
});
Observables have many advantages over Promises and one of the greatest is the ability to retry.
UserActions.fetch$.concatMap(userId => {
return Rx.Observable.ajax(`/users/${userId}`)
.retryWhen(err$ => err$.delay(1000).take(10));
});
Summary
We have only scratched the surface of possible RxJS use cases in React. With even basic knowledge about RxJS observables and operators, you can do pretty awesome stuff in just couple lines of code.
GitHub: MichalZalecki/connect-rxjs-to-react
Hero image by rawpixel on Unsplash.