reactions... work?

This commit is contained in:
Yanick Champoux 2023-03-14 14:00:11 -04:00
parent 5297aecba6
commit 56625c5083
5 changed files with 183 additions and 30 deletions

View File

@ -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);
}
} }

View File

@ -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),

View File

@ -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
View 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
});

View File

@ -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 : {};