Skip to content

Commit

Permalink
refactor: 💡 remove .state and use BehaviourSubject
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Dec 6, 2019
1 parent fbe3ab6 commit 8f1785b
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
test('can create store', () => {
const { store } = create({});
expect(store).toMatchObject({
state: expect.any(Object),
getState: expect.any(Function),
state$: expect.any(Object),
transitions: expect.any(Object),
Expand All @@ -47,7 +46,7 @@ test('can set default state', () => {
foo: 'bar',
};
const { store } = create(defaultState);
expect(store.state).toEqual(defaultState);
expect(store.get()).toEqual(defaultState);
expect(store.getState()).toEqual(defaultState);
});

Expand All @@ -62,7 +61,7 @@ test('can set state', () => {

mutators.set(newState);

expect(store.state).toEqual(newState);
expect(store.get()).toEqual(newState);
expect(store.getState()).toEqual(newState);
});

Expand All @@ -77,7 +76,7 @@ test('does not shallow merge states', () => {

mutators.set(newState as any);

expect(store.state).toEqual(newState);
expect(store.get()).toEqual(newState);
expect(store.getState()).toEqual(newState);
});

Expand Down Expand Up @@ -137,23 +136,23 @@ test('mutators can update state', () => {
}
);

expect(store.state).toEqual({
expect(store.get()).toEqual({
value: 0,
foo: 'bar',
});

mutators.add(11);
mutators.setFoo('baz');

expect(store.state).toEqual({
expect(store.get()).toEqual({
value: 11,
foo: 'baz',
});

mutators.add(-20);
mutators.setFoo('bazooka');

expect(store.state).toEqual({
expect(store.get()).toEqual({
value: -9,
foo: 'bazooka',
});
Expand All @@ -170,9 +169,9 @@ test('mutators methods are not bound', () => {
}
);

expect(store.state).toEqual({ value: -3 });
expect(store.get()).toEqual({ value: -3 });
mutators.add(4);
expect(store.state).toEqual({ value: 1 });
expect(store.get()).toEqual({ value: 1 });
});

test('created mutators are saved in store object', () => {
Expand All @@ -188,15 +187,15 @@ test('created mutators are saved in store object', () => {

expect(typeof store.transitions.add).toBe('function');
mutators.add(5);
expect(store.state).toEqual({ value: 2 });
expect(store.get()).toEqual({ value: 2 });
});

test('throws when state is modified inline - 1', () => {
const container = createStateContainer({ a: 'b' }, {});

let error: TypeError | null = null;
try {
(container.state.a as any) = 'c';
(container.get().a as any) = 'c';
} catch (err) {
error = err;
}
Expand All @@ -209,14 +208,30 @@ test('throws when state is modified inline - 2', () => {

let error: TypeError | null = null;
try {
(container.get().a as any) = 'c';
(container.getState().a as any) = 'c';
} catch (err) {
error = err;
}

expect(error).toBeInstanceOf(TypeError);
});

test('throws when state is modified inline in subscription', done => {
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });

container.subscribe(value => {
let error: TypeError | null = null;
try {
(value.a as any) = 'd';
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(TypeError);
done();
});
container.transitions.set({ a: 'c' });
});

describe('selectors', () => {
test('can specify no selectors, or can skip them', () => {
createStateContainer({}, {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* under the License.
*/

import { Subject } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { skip } from 'rxjs/operators';
import { RecursiveReadonly } from '@kbn/utility-types';
import {
PureTransitionsToTransitions,
Expand Down Expand Up @@ -47,31 +48,32 @@ export const createStateContainer = <
pureTransitions: PureTransitions,
pureSelectors: PureSelectors = {} as PureSelectors
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors> => {
const state$ = new Subject<RecursiveReadonly<State>>();
const data$ = new BehaviorSubject<RecursiveReadonly<State>>(freeze(defaultState));
const state$ = data$.pipe(skip(1));
const get = () => data$.getValue();
const container: ReduxLikeStateContainer<State, PureTransitions, PureSelectors> = {
state: freeze(defaultState),
get: () => container.state,
getState: () => container.state,
get,
state$,
getState: () => data$.getValue(),
set: (state: State) => {
state$.next((container.state = freeze(state)));
data$.next(freeze(state));
},
state$,
reducer: (state, action) => {
const pureTransition = (pureTransitions as Record<string, PureTransition<State, any[]>>)[
action.type
];
return pureTransition ? freeze(pureTransition(state)(...action.args)) : state;
},
replaceReducer: nextReducer => (container.reducer = nextReducer),
dispatch: action => state$.next((container.state = container.reducer(container.state, action))),
dispatch: action => data$.next(container.reducer(get(), action)),
transitions: Object.keys(pureTransitions).reduce<PureTransitionsToTransitions<PureTransitions>>(
(acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }),
{} as PureTransitionsToTransitions<PureTransitions>
),
selectors: Object.keys(pureSelectors).reduce<PureSelectorsToSelectors<PureSelectors>>(
(acc, selector) => ({
...acc,
[selector]: (...args: any) => (pureSelectors as any)[selector](container.state)(...args),
[selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args),
}),
{} as PureSelectorsToSelectors<PureSelectors>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ test('<Provider> passes state to <Consumer>', () => {

ReactDOM.render(
<Provider value={store}>
<Consumer>{(s: any) => s.state.hello}</Consumer>
<Consumer>{(s: typeof store) => s.get().hello}</Consumer>
</Provider>,
container
);
Expand Down Expand Up @@ -108,7 +108,7 @@ test('context receives Redux store', () => {
ReactDOM.render(
/* eslint-disable no-shadow */
<Provider value={store}>
<context.Consumer>{store => store.state.foo}</context.Consumer>
<context.Consumer>{store => store.get().foo}</context.Consumer>
</Provider>,
/* eslint-enable no-shadow */
container
Expand All @@ -127,7 +127,7 @@ describe('hooks', () => {
const Demo: React.FC<{}> = () => {
// eslint-disable-next-line no-shadow
const store = useContainer();
return <>{store.state.foo}</>;
return <>{store.get().foo}</>;
};

ReactDOM.render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const createStateContainerReactHelpers = <Container extends StateContaine
const useContainer = (): Container => useContext(context);

const useState = (): UnboxState<Container> => {
const { state$, state } = useContainer();
const value = useObservable(state$, state);
const { state$, get } = useContainer();
const value = useObservable(state$, get());
return value;
};

Expand All @@ -41,10 +41,10 @@ export const createStateContainerReactHelpers = <Container extends StateContaine
selector: (state: UnboxState<Container>) => Result,
comparator: Comparator<Result> = defaultComparator
): Result => {
const { state$, state } = useContainer();
const lastValueRef = useRef<Result>(state);
const { state$, get } = useContainer();
const lastValueRef = useRef<Result>(get());
const [value, setValue] = React.useState<Result>(() => {
const newValue = selector(state);
const newValue = selector(get());
lastValueRef.current = newValue;
return newValue;
});
Expand Down
1 change: 0 additions & 1 deletion src/plugins/kibana_utils/public/state_containers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export type PureTransitionsToTransitions<T extends object> = {
};

export interface BaseStateContainer<State> {
state: RecursiveReadonly<State>;
get: () => RecursiveReadonly<State>;
set: (state: State) => void;
state$: Observable<RecursiveReadonly<State>>;
Expand Down

0 comments on commit 8f1785b

Please sign in to comment.