reactions... work?
This commit is contained in:
parent
5297aecba6
commit
56625c5083
108
src/Updux.ts
108
src/Updux.ts
@ -7,6 +7,7 @@ import {
|
||||
MiddlewareAPI,
|
||||
AnyAction,
|
||||
Middleware,
|
||||
Dispatch,
|
||||
} from 'redux';
|
||||
import {
|
||||
configureStore,
|
||||
@ -19,7 +20,7 @@ import { AggregateActions, AggregateSelectors, Dux } from './types.js';
|
||||
import { buildActions } from './buildActions.js';
|
||||
import { buildInitial, AggregateState } from './initial.js';
|
||||
import { buildReducer, MutationCase } from './reducer.js';
|
||||
import { augmentGetState, buildEffectsMiddleware, EffectMiddleware } from './effects.js';
|
||||
import { augmentGetState, augmentMiddlewareApi, buildEffectsMiddleware, EffectMiddleware } from './effects.js';
|
||||
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
|
||||
|
||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||
@ -54,6 +55,17 @@ type ResolveActions<
|
||||
: never;
|
||||
};
|
||||
|
||||
type Reaction<S = any, M extends MiddlewareAPI = MiddlewareAPI> =
|
||||
(api: M, state: S, previousState: S, unsubscribe: () => void) => void;
|
||||
|
||||
type AugmentedMiddlewareAPI<S, A, SELECTORS> =
|
||||
MiddlewareAPI<Dispatch<AnyAction>, S> & {
|
||||
dispatch: A,
|
||||
getState: CurriedSelectors<SELECTORS>,
|
||||
actions: A,
|
||||
selectors: SELECTORS,
|
||||
};
|
||||
|
||||
export type Mutation<A extends Action<any> = Action<any>, S = any> = (
|
||||
payload: A extends {
|
||||
payload: infer P;
|
||||
@ -151,6 +163,19 @@ export default class Updux<
|
||||
]
|
||||
}
|
||||
|
||||
get reactions(): any {
|
||||
return [...this.#localReactions,
|
||||
...Object.entries(this.#subduxes).flatMap(
|
||||
([slice, { reactions }]) => reactions.map(
|
||||
(r) => (api, unsub) => r({
|
||||
...api,
|
||||
getState: () => api.getState()[slice],
|
||||
}, unsub)
|
||||
)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
createStore(
|
||||
options: Partial<{
|
||||
initial: T_LocalState;
|
||||
@ -166,7 +191,7 @@ export default class Updux<
|
||||
|
||||
|
||||
const store = configureStore({
|
||||
reducer: ((state) => state) as Reducer<
|
||||
reducer: this.reducer as Reducer<
|
||||
AggregateState<T_LocalState, T_Subduxes>,
|
||||
AnyAction
|
||||
>,
|
||||
@ -185,20 +210,27 @@ export default class Updux<
|
||||
|
||||
store.getState = augmentGetState(store.getState, this.selectors);
|
||||
|
||||
for (const reaction of this.reactions) {
|
||||
let unsub;
|
||||
const r = reaction(store);
|
||||
|
||||
unsub = store.subscribe(() => r(unsub));
|
||||
}
|
||||
|
||||
|
||||
return store as ToolkitStore<
|
||||
AggregateState<T_LocalState, T_Subduxes>
|
||||
> & {
|
||||
dispatch: AggregateActions<
|
||||
> & AugmentedMiddlewareAPI<
|
||||
AggregateState<T_LocalState, T_Subduxes>,
|
||||
AggregateActions<
|
||||
ResolveActions<T_LocalActions>,
|
||||
T_Subduxes
|
||||
>;
|
||||
} & {
|
||||
getState: CurriedSelectors<AggregateSelectors<
|
||||
>, AggregateSelectors<
|
||||
T_LocalSelectors,
|
||||
T_Subduxes,
|
||||
AggregateState<T_LocalState, T_Subduxes>
|
||||
>>
|
||||
};
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
get selectors(): AggregateSelectors<
|
||||
@ -271,5 +303,63 @@ export default class Updux<
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
#localReactions: any[] = [];
|
||||
addReaction(reaction: Reaction<AggregateState<T_LocalState, T_Subduxes>,
|
||||
AugmentedMiddlewareAPI<
|
||||
AggregateState<T_LocalState, T_Subduxes>,
|
||||
AggregateActions<
|
||||
ResolveActions<T_LocalActions>,
|
||||
T_Subduxes
|
||||
>, AggregateSelectors<
|
||||
T_LocalSelectors,
|
||||
T_Subduxes,
|
||||
AggregateState<T_LocalState, T_Subduxes>
|
||||
>
|
||||
>
|
||||
>) {
|
||||
|
||||
let previous: any;
|
||||
|
||||
const memoized = (api: any) => {
|
||||
api = augmentMiddlewareApi(api,
|
||||
this.actions,
|
||||
this.selectors
|
||||
);
|
||||
return (unsub: () => void) => {
|
||||
const state = api.getState();
|
||||
if (state === previous) return;
|
||||
let p = previous;
|
||||
previous = state;
|
||||
reaction(api, state, p, unsub);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
this.#localReactions.push(memoized);
|
||||
}
|
||||
|
||||
// internal method REMOVE
|
||||
subscribeTo(store, subscription) {
|
||||
const localStore = augmentMiddlewareApi({
|
||||
...store,
|
||||
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
|
||||
}, this.actions, this.selectors);
|
||||
|
||||
const subscriber = subscription(localStore);
|
||||
|
||||
let previous;
|
||||
let unsub;
|
||||
|
||||
const memoSub = () => {
|
||||
const state = store.getState();
|
||||
if (state === previous) return;
|
||||
let p = previous;
|
||||
previous = state;
|
||||
subscriber(state, p, unsub);
|
||||
};
|
||||
|
||||
return store.subscribe(memoSub);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ const augmentDispatch = (originalDispatch, actions) => {
|
||||
return dispatch;
|
||||
};
|
||||
|
||||
const augmentMiddlewareApi = (api, actions, selectors) => {
|
||||
export const augmentMiddlewareApi = (api, actions, selectors) => {
|
||||
return {
|
||||
...api,
|
||||
getState: augmentGetState(api.getState, selectors),
|
||||
|
@ -2,26 +2,6 @@ import { test, expect, vi } from 'vitest';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
|
||||
test('basic reactions', async () => {
|
||||
const spyA = vi.fn();
|
||||
const spyB = vi.fn();
|
||||
const foo = new Updux({
|
||||
initial: { i: 0 },
|
||||
reactions: [() => spyA],
|
||||
actions: { inc: null },
|
||||
mutations: {
|
||||
inc: () => (state) => ({ ...state, i: state.i + 1 }),
|
||||
},
|
||||
});
|
||||
|
||||
foo.addReaction((api) => spyB);
|
||||
|
||||
const store = foo.createStore();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(spyA).toHaveBeenCalledOnce();
|
||||
expect(spyB).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('subduxes reactions', async () => {
|
||||
const spyA = vi.fn();
|
||||
|
82
src/reactions.test.ts
Normal file
82
src/reactions.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { test, expect, vi } from 'vitest';
|
||||
import Updux from './index.js';
|
||||
|
||||
test('basic reactions', () => {
|
||||
const foo = new Updux({
|
||||
initial: 0,
|
||||
actions: { inc: 0, reset: 0 },
|
||||
});
|
||||
|
||||
// TODO immer that stuff
|
||||
foo.addMutation(foo.actions.inc, () => (state) => state + 1);
|
||||
foo.addMutation(foo.actions.reset, () => (state) => 0);
|
||||
|
||||
foo.addReaction((api, state, _previous, unsubscribe) => {
|
||||
if (state < 3) return;
|
||||
unsubscribe();
|
||||
api.dispatch.reset();
|
||||
});
|
||||
|
||||
// TODO
|
||||
//reaction: (api) => (state,previous,unsubscribe)
|
||||
|
||||
const store = foo.createStore();
|
||||
|
||||
store.dispatch.inc();
|
||||
expect(store.getState()).toEqual(1);
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(store.getState()).toEqual(0); // we've been reset
|
||||
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(store.getState()).toEqual(4); // we've unsubscribed
|
||||
});
|
||||
|
||||
test('subdux reactions', () => {
|
||||
const bar = new Updux({
|
||||
initial: 0,
|
||||
actions: { inc: 0, reset: 0 },
|
||||
selectors: {
|
||||
getIt: (x) => x,
|
||||
},
|
||||
});
|
||||
|
||||
const foo = new Updux({ actions: { notInBar: 0 }, subduxes: { bar } });
|
||||
|
||||
// TODO immer that stuff
|
||||
bar.addMutation(foo.actions.inc, () => (state) => state + 1);
|
||||
bar.addMutation(foo.actions.reset, () => (state) => 0);
|
||||
|
||||
let seen = 0;
|
||||
bar.addReaction((api, state, _previous, unsubscribe) => {
|
||||
seen++;
|
||||
expect(api.actions).not.toHaveProperty('notInBar');
|
||||
expect(state).toBeTypeOf('number');
|
||||
if (state < 3) return;
|
||||
unsubscribe();
|
||||
api.dispatch.reset();
|
||||
});
|
||||
|
||||
const store = foo.createStore();
|
||||
|
||||
store.dispatch.inc();
|
||||
expect(seen).toEqual(1);
|
||||
expect(store.getState()).toEqual({ bar: 1 });
|
||||
expect(store.getState.getIt()).toEqual(1);
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(store.getState.getIt()).toEqual(0); // we've been reset
|
||||
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
store.dispatch.inc();
|
||||
|
||||
expect(store.getState.getIt()).toEqual(4); // we've unsubscribed
|
||||
});
|
@ -12,6 +12,7 @@ export type Dux<
|
||||
action: ReturnType<ACTIONS[keyof ACTIONS]>,
|
||||
) => STATE;
|
||||
effects: Middleware[];
|
||||
reactions: ((...args: any[]) => void)[];
|
||||
}>;
|
||||
|
||||
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
||||
|
Loading…
Reference in New Issue
Block a user