@MichalZalecki
promises
await/async
RxJS
callbacks
\rē-ˈak-tiv\
done in response to a problem or situation
‒ merriam-webster.com
// 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;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();
});// CounterActions.js
import Rx from "rxjs";
const CounterActions = {
  increment$: new Rx.Subject,
  decrement$: new Rx.Subject,
};
export default CounterActions;// 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$;// 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$);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);
});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.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;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");
});// 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);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);
});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));
});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"));