Skip to content

Commit

Permalink
feat(state$): state$ now only emits subsequent values if the state sh…
Browse files Browse the repository at this point in the history
…allowly is different (e.g. prevValue !== nextValue). It still emits the current state immediately on subscribe regardless, as it did before, similar to BehaviorSubject. Closes #497
  • Loading branch information
jayphelps committed May 23, 2018
1 parent d3516bf commit 4697047
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 20 deletions.
38 changes: 19 additions & 19 deletions src/StateObservable.js
Original file line number Original file line Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable } from 'rxjs'; import { Observable, Subject } from 'rxjs';


// Used as a placeholder value so we can tell // Used as a placeholder value so we can tell
// whether or not the real state has been set, // whether or not the real state has been set,
Expand All @@ -17,26 +17,32 @@ export const UNSET_STATE_VALUE = {};
// screw things up and it would just be a footgun. // screw things up and it would just be a footgun.
export class StateObservable extends Observable { export class StateObservable extends Observable {
constructor(stateSubject, store) { constructor(stateSubject, store) {
super(); super(subscriber => {
this.source = stateSubject; const subscription = this.__notifier.subscribe(subscriber);
if (this.__value !== UNSET_STATE_VALUE && subscription && !subscription.closed) {
subscriber.next(this.__value);
}
return subscription;
});

// If you're reading this, keep in mind that this is // If you're reading this, keep in mind that this is
// NOT part of the public API and will be removed! // NOT part of the public API and will be removed!
this.__store = store; this.__store = store;
this.__value = UNSET_STATE_VALUE; this.__value = UNSET_STATE_VALUE;
this.__notifier = new Subject();


this.source.subscribe(value => { this.__subscription = stateSubject.subscribe(value => {
this.__value = value; // We only want to update state$ if it has actually changed since
// redux requires reducers use immutability patterns.
// This is basically what distinctUntilChanged() does but it's so simple
// we don't need to pull that code in
if (value !== this.__value) {
this.__value = value;
this.__notifier.next(value);
}
}); });
} }


_subscribe(subscriber) {
const subscription = super._subscribe(subscriber);
if (this.__value !== UNSET_STATE_VALUE && subscription && !subscription.closed) {
subscriber.next(this.__value);
}
return subscription;
}

get value() { get value() {
if (this.__value === UNSET_STATE_VALUE) { if (this.__value === UNSET_STATE_VALUE) {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
Expand All @@ -48,12 +54,6 @@ export class StateObservable extends Observable {
} }
} }


lift(operator) {
const observable = new StateObservable(this);
observable.operator = operator;
return observable;
}

getState() { getState() {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
require('./utils/console').deprecate('calling store.getState() in your Epics is deprecated and will be removed. The second argument to your Epic is now a stream of state$ (a StateObservable), instead of the store. To imperatively get the current state use state$.value instead of getState(). Alternatively, since it\'s now a stream you can compose and react to state changes.\n\n function <T, R, S, D>(action$: ActionsObservable<T>, state$: StateObservable<S>, dependencies?: D): Observable<R>\n\nLearn more: https://redux-observable.js.org/MIGRATION.html'); require('./utils/console').deprecate('calling store.getState() in your Epics is deprecated and will be removed. The second argument to your Epic is now a stream of state$ (a StateObservable), instead of the store. To imperatively get the current state use state$.value instead of getState(). Alternatively, since it\'s now a stream you can compose and react to state changes.\n\n function <T, R, S, D>(action$: ActionsObservable<T>, state$: StateObservable<S>, dependencies?: D): Observable<R>\n\nLearn more: https://redux-observable.js.org/MIGRATION.html');
Expand Down
44 changes: 43 additions & 1 deletion test/StateObservable-spec.js
Original file line number Original file line Diff line number Diff line change
@@ -1,9 +1,20 @@
/* globals describe it */ /* globals describe it beforeEach afterEach */
import { expect } from 'chai'; import { expect } from 'chai';
import sinon from 'sinon';
import { StateObservable } from '../'; import { StateObservable } from '../';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';


describe('StateObservable', () => { describe('StateObservable', () => {
let spySandbox;

beforeEach(() => {
spySandbox = sinon.sandbox.create();
});

afterEach(() => {
spySandbox.restore();
});

it('should exist', () => { it('should exist', () => {
expect(StateObservable.prototype).to.be.instanceof(Observable); expect(StateObservable.prototype).to.be.instanceof(Observable);
}); });
Expand Down Expand Up @@ -34,4 +45,35 @@ describe('StateObservable', () => {
input$.next('second'); input$.next('second');
expect(state$.value).to.equal('second'); expect(state$.value).to.equal('second');
}); });

it('should only update when the next value shallowly differs', () => {
const input$ = new Subject();
const state$ = new StateObservable(input$);
const next = spySandbox.spy();
state$.subscribe(next);

expect(state$.value).to.equal(undefined);
expect(next.callCount).to.equal(0);

const first = { value: 'first' };
input$.next(first);
expect(state$.value).to.equal(first);
expect(next.callCount).to.equal(1);
expect(next.getCall(0).args).to.deep.equal([first]);

input$.next(first);
expect(state$.value).to.equal(first);
expect(next.callCount).to.equal(1);

first.value = 'something else';
input$.next(first);
expect(state$.value).to.equal(first);
expect(next.callCount).to.equal(1);

const second = { value: 'second' };
input$.next(second);
expect(state$.value).to.equal(second);
expect(next.callCount).to.equal(2);
expect(next.getCall(1).args).to.deep.equal([second]);
});
}); });

0 comments on commit 4697047

Please sign in to comment.