effectsMiddleware
This commit is contained in:
parent
7a5733425e
commit
95768706fa
47
src/Updux.ts
47
src/Updux.ts
@ -4,6 +4,8 @@ import {
|
||||
applyMiddleware,
|
||||
DeepPartial,
|
||||
Action,
|
||||
MiddlewareAPI,
|
||||
AnyAction,
|
||||
} from 'redux';
|
||||
import {
|
||||
configureStore,
|
||||
@ -16,6 +18,8 @@ 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 { buildEffectsMiddleware, EffectMiddleware } from './effects.js';
|
||||
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
|
||||
|
||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||
|
||||
@ -84,6 +88,8 @@ export default class Updux<
|
||||
>;
|
||||
#selectors: any;
|
||||
|
||||
#effects: any[] = [];
|
||||
|
||||
constructor(
|
||||
config: Partial<{
|
||||
initial: T_LocalState;
|
||||
@ -129,11 +135,31 @@ export default class Updux<
|
||||
) {
|
||||
const preloadedState: any = options.initial ?? this.initial;
|
||||
const store = configureStore({
|
||||
reducer: ((state) => state) as Reducer<T_LocalState, any>,
|
||||
reducer: ((state) => state) as Reducer<
|
||||
AggregateState<T_LocalState, T_Subduxes>,
|
||||
AnyAction
|
||||
>,
|
||||
preloadedState,
|
||||
middleware: [this.effectsMiddleware],
|
||||
});
|
||||
|
||||
return store;
|
||||
const dispatch: any = store.dispatch;
|
||||
for (const a in this.actions) {
|
||||
dispatch[a] = (...args) => {
|
||||
const action = (this.actions as any)[a](...args);
|
||||
dispatch(action);
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
return store as ToolkitStore<
|
||||
AggregateState<T_LocalState, T_Subduxes>
|
||||
> & {
|
||||
dispatch: AggregateActions<
|
||||
ResolveActions<T_LocalActions>,
|
||||
T_Subduxes
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
get selectors(): AggregateSelectors<
|
||||
@ -193,4 +219,21 @@ export default class Updux<
|
||||
) {
|
||||
this.#defaultMutation = { mutation, terminal };
|
||||
}
|
||||
|
||||
addEffect(effect: EffectMiddleware) {
|
||||
this.#effects.push(effect);
|
||||
}
|
||||
|
||||
get effects() {
|
||||
return this.#effects;
|
||||
}
|
||||
|
||||
get effectsMiddleware() {
|
||||
return buildEffectsMiddleware(
|
||||
this.effects,
|
||||
this.actions,
|
||||
this.selectors,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
79
src/effects.test.ts
Normal file
79
src/effects.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { buildEffectsMiddleware } from './effects.js';
|
||||
import Updux, { createAction } from './index.js';
|
||||
|
||||
test('buildEffectsMiddleware', () => {
|
||||
let seen = 0;
|
||||
|
||||
const mw = buildEffectsMiddleware(
|
||||
[
|
||||
(api) => (next) => (action) => {
|
||||
seen++;
|
||||
expect(api).toHaveProperty('getState');
|
||||
expect(api.getState).toBeTypeOf('function');
|
||||
|
||||
expect(api.getState()).toEqual('the state');
|
||||
|
||||
expect(action).toHaveProperty('type');
|
||||
expect(next).toBeTypeOf('function');
|
||||
|
||||
expect(api).toHaveProperty('actions');
|
||||
expect(api.actions.action1()).toHaveProperty('type', 'action1');
|
||||
|
||||
api.dispatch.action1();
|
||||
|
||||
expect(api.selectors.getFoo(2)).toBe(2);
|
||||
expect(api.getState.getFoo()).toBe('the state');
|
||||
expect(api.getState.getBar(2)).toBe('the state2');
|
||||
|
||||
next(action);
|
||||
},
|
||||
],
|
||||
{
|
||||
action1: createAction('action1'),
|
||||
},
|
||||
{
|
||||
getFoo: (state) => state,
|
||||
getBar: (state) => (i) => state + i,
|
||||
},
|
||||
);
|
||||
|
||||
expect(seen).toEqual(0);
|
||||
const dispatch = vi.fn();
|
||||
mw({ getState: () => 'the state', dispatch })(() => { })({
|
||||
type: 'noop',
|
||||
});
|
||||
expect(seen).toEqual(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'action1' });
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
const dux = new Updux({
|
||||
initial: {
|
||||
loaded: true,
|
||||
},
|
||||
actions: {
|
||||
foo: 0,
|
||||
},
|
||||
});
|
||||
|
||||
let seen = 0;
|
||||
dux.addEffect((api) => (next) => (action) => {
|
||||
seen++;
|
||||
expect(api).toHaveProperty('getState');
|
||||
expect(api.getState()).toHaveProperty('loaded');
|
||||
expect(action).toHaveProperty('type');
|
||||
expect(next).toBeTypeOf('function');
|
||||
next(action);
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect(seen).toEqual(0);
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(seen).toEqual(1);
|
||||
});
|
||||
|
||||
// TODO subdux effects
|
||||
// TODO allow to subscribe / unsubscribe effects?
|
47
src/effects.ts
Normal file
47
src/effects.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { MiddlewareAPI } from '@reduxjs/toolkit';
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
|
||||
export interface EffectMiddleware<S = any, D extends Dispatch = Dispatch> {
|
||||
(api: MiddlewareAPI<D, S>): (
|
||||
next: Dispatch<AnyAction>,
|
||||
) => (action: AnyAction) => any;
|
||||
}
|
||||
|
||||
const composeMw = (mws) => (api) => (originalNext) =>
|
||||
mws.reduceRight((next, mw) => mw(api)(next), originalNext);
|
||||
|
||||
export function buildEffectsMiddleware(
|
||||
effects = [],
|
||||
actions = {},
|
||||
selectors = {},
|
||||
) {
|
||||
return ({
|
||||
getState: originalGetState,
|
||||
dispatch: originalDispatch,
|
||||
...rest
|
||||
}) => {
|
||||
const dispatch = (action) => originalDispatch(action);
|
||||
|
||||
for (const a in actions) {
|
||||
dispatch[a] = (...args) => dispatch(actions[a](...args));
|
||||
}
|
||||
|
||||
const getState = () => originalGetState();
|
||||
for (const s in selectors) {
|
||||
getState[s] = (...args) => {
|
||||
let result = selectors[s](originalGetState());
|
||||
if (typeof result === 'function') return result(...args);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
let mws = effects.map((e) =>
|
||||
e({ getState, dispatch, actions, selectors, ...rest }),
|
||||
);
|
||||
|
||||
return (originalNext) => {
|
||||
return mws.reduceRight((next, mw) => mw(next), originalNext);
|
||||
};
|
||||
};
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import R from 'remeda';
|
||||
|
||||
export function buildSelectors(
|
||||
localSelectors,
|
||||
findSelectors = {},
|
||||
subduxes = {},
|
||||
) {
|
||||
const subSelectors = Object.entries(subduxes).map(
|
||||
([slice, { selectors }]) => {
|
||||
if (!selectors) return {};
|
||||
if (slice === '*') return {};
|
||||
|
||||
return R.mapValues(
|
||||
selectors,
|
||||
(func) => (state) => func(state[slice]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
let splat = {};
|
||||
|
||||
for (const name in findSelectors) {
|
||||
splat[name] =
|
||||
(mainState) =>
|
||||
(...args) => {
|
||||
const state = findSelectors[name](mainState)(...args);
|
||||
|
||||
return R.merge(
|
||||
{ state },
|
||||
R.mapValues(
|
||||
subduxes['*']?.selectors ?? {},
|
||||
(selector) =>
|
||||
(...args) => {
|
||||
let value = selector(state);
|
||||
if (typeof value !== 'function') return value;
|
||||
return value(...args);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
return R.mergeAll([...subSelectors, localSelectors, splat]);
|
||||
}
|
Loading…
Reference in New Issue
Block a user