Skip to content

Commit 4697047

Browse files
committed
feat(state$): state$ now only emits subsequent values if the state shallowly 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
1 parent d3516bf commit 4697047

File tree

2 files changed

+62
-20
lines changed

2 files changed

+62
-20
lines changed

src/StateObservable.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Observable } from 'rxjs';
1+
import { Observable, Subject } from 'rxjs';
22

33
// Used as a placeholder value so we can tell
44
// whether or not the real state has been set,
@@ -17,26 +17,32 @@ export const UNSET_STATE_VALUE = {};
1717
// screw things up and it would just be a footgun.
1818
export class StateObservable extends Observable {
1919
constructor(stateSubject, store) {
20-
super();
21-
this.source = stateSubject;
20+
super(subscriber => {
21+
const subscription = this.__notifier.subscribe(subscriber);
22+
if (this.__value !== UNSET_STATE_VALUE && subscription && !subscription.closed) {
23+
subscriber.next(this.__value);
24+
}
25+
return subscription;
26+
});
27+
2228
// If you're reading this, keep in mind that this is
2329
// NOT part of the public API and will be removed!
2430
this.__store = store;
2531
this.__value = UNSET_STATE_VALUE;
32+
this.__notifier = new Subject();
2633

27-
this.source.subscribe(value => {
28-
this.__value = value;
34+
this.__subscription = stateSubject.subscribe(value => {
35+
// We only want to update state$ if it has actually changed since
36+
// redux requires reducers use immutability patterns.
37+
// This is basically what distinctUntilChanged() does but it's so simple
38+
// we don't need to pull that code in
39+
if (value !== this.__value) {
40+
this.__value = value;
41+
this.__notifier.next(value);
42+
}
2943
});
3044
}
3145

32-
_subscribe(subscriber) {
33-
const subscription = super._subscribe(subscriber);
34-
if (this.__value !== UNSET_STATE_VALUE && subscription && !subscription.closed) {
35-
subscriber.next(this.__value);
36-
}
37-
return subscription;
38-
}
39-
4046
get value() {
4147
if (this.__value === UNSET_STATE_VALUE) {
4248
if (process.env.NODE_ENV !== 'production') {
@@ -48,12 +54,6 @@ export class StateObservable extends Observable {
4854
}
4955
}
5056

51-
lift(operator) {
52-
const observable = new StateObservable(this);
53-
observable.operator = operator;
54-
return observable;
55-
}
56-
5757
getState() {
5858
if (process.env.NODE_ENV !== 'production') {
5959
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');

test/StateObservable-spec.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
/* globals describe it */
1+
/* globals describe it beforeEach afterEach */
22
import { expect } from 'chai';
3+
import sinon from 'sinon';
34
import { StateObservable } from '../';
45
import { Observable, Subject } from 'rxjs';
56

67
describe('StateObservable', () => {
8+
let spySandbox;
9+
10+
beforeEach(() => {
11+
spySandbox = sinon.sandbox.create();
12+
});
13+
14+
afterEach(() => {
15+
spySandbox.restore();
16+
});
17+
718
it('should exist', () => {
819
expect(StateObservable.prototype).to.be.instanceof(Observable);
920
});
@@ -34,4 +45,35 @@ describe('StateObservable', () => {
3445
input$.next('second');
3546
expect(state$.value).to.equal('second');
3647
});
48+
49+
it('should only update when the next value shallowly differs', () => {
50+
const input$ = new Subject();
51+
const state$ = new StateObservable(input$);
52+
const next = spySandbox.spy();
53+
state$.subscribe(next);
54+
55+
expect(state$.value).to.equal(undefined);
56+
expect(next.callCount).to.equal(0);
57+
58+
const first = { value: 'first' };
59+
input$.next(first);
60+
expect(state$.value).to.equal(first);
61+
expect(next.callCount).to.equal(1);
62+
expect(next.getCall(0).args).to.deep.equal([first]);
63+
64+
input$.next(first);
65+
expect(state$.value).to.equal(first);
66+
expect(next.callCount).to.equal(1);
67+
68+
first.value = 'something else';
69+
input$.next(first);
70+
expect(state$.value).to.equal(first);
71+
expect(next.callCount).to.equal(1);
72+
73+
const second = { value: 'second' };
74+
input$.next(second);
75+
expect(state$.value).to.equal(second);
76+
expect(next.callCount).to.equal(2);
77+
expect(next.getCall(1).args).to.deep.equal([second]);
78+
});
3779
});

0 commit comments

Comments
 (0)