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,
|
MiddlewareAPI,
|
||||||
AnyAction,
|
AnyAction,
|
||||||
Middleware,
|
Middleware,
|
||||||
|
Dispatch,
|
||||||
} from 'redux';
|
} from 'redux';
|
||||||
import {
|
import {
|
||||||
configureStore,
|
configureStore,
|
||||||
@ -19,7 +20,7 @@ import { AggregateActions, AggregateSelectors, Dux } from './types.js';
|
|||||||
import { buildActions } from './buildActions.js';
|
import { buildActions } from './buildActions.js';
|
||||||
import { buildInitial, AggregateState } from './initial.js';
|
import { buildInitial, AggregateState } from './initial.js';
|
||||||
import { buildReducer, MutationCase } from './reducer.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';
|
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
|
||||||
|
|
||||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||||
@ -54,6 +55,17 @@ type ResolveActions<
|
|||||||
: never;
|
: 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> = (
|
export type Mutation<A extends Action<any> = Action<any>, S = any> = (
|
||||||
payload: A extends {
|
payload: A extends {
|
||||||
payload: infer P;
|
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(
|
createStore(
|
||||||
options: Partial<{
|
options: Partial<{
|
||||||
initial: T_LocalState;
|
initial: T_LocalState;
|
||||||
@ -166,7 +191,7 @@ export default class Updux<
|
|||||||
|
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: ((state) => state) as Reducer<
|
reducer: this.reducer as Reducer<
|
||||||
AggregateState<T_LocalState, T_Subduxes>,
|
AggregateState<T_LocalState, T_Subduxes>,
|
||||||
AnyAction
|
AnyAction
|
||||||
>,
|
>,
|
||||||
@ -185,20 +210,27 @@ export default class Updux<
|
|||||||
|
|
||||||
store.getState = augmentGetState(store.getState, this.selectors);
|
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<
|
return store as ToolkitStore<
|
||||||
AggregateState<T_LocalState, T_Subduxes>
|
AggregateState<T_LocalState, T_Subduxes>
|
||||||
> & {
|
> & AugmentedMiddlewareAPI<
|
||||||
dispatch: AggregateActions<
|
AggregateState<T_LocalState, T_Subduxes>,
|
||||||
|
AggregateActions<
|
||||||
ResolveActions<T_LocalActions>,
|
ResolveActions<T_LocalActions>,
|
||||||
T_Subduxes
|
T_Subduxes
|
||||||
>;
|
>, AggregateSelectors<
|
||||||
} & {
|
|
||||||
getState: CurriedSelectors<AggregateSelectors<
|
|
||||||
T_LocalSelectors,
|
T_LocalSelectors,
|
||||||
T_Subduxes,
|
T_Subduxes,
|
||||||
AggregateState<T_LocalState, T_Subduxes>
|
AggregateState<T_LocalState, T_Subduxes>
|
||||||
>>
|
>
|
||||||
};
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectors(): AggregateSelectors<
|
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;
|
return dispatch;
|
||||||
};
|
};
|
||||||
|
|
||||||
const augmentMiddlewareApi = (api, actions, selectors) => {
|
export const augmentMiddlewareApi = (api, actions, selectors) => {
|
||||||
return {
|
return {
|
||||||
...api,
|
...api,
|
||||||
getState: augmentGetState(api.getState, selectors),
|
getState: augmentGetState(api.getState, selectors),
|
||||||
|
@ -2,26 +2,6 @@ import { test, expect, vi } from 'vitest';
|
|||||||
|
|
||||||
import { Updux } from './Updux.js';
|
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 () => {
|
test('subduxes reactions', async () => {
|
||||||
const spyA = vi.fn();
|
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]>,
|
action: ReturnType<ACTIONS[keyof ACTIONS]>,
|
||||||
) => STATE;
|
) => STATE;
|
||||||
effects: Middleware[];
|
effects: Middleware[];
|
||||||
|
reactions: ((...args: any[]) => void)[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
||||||
|
Loading…
Reference in New Issue
Block a user