add effects
This commit is contained in:
parent
af6db3403c
commit
80a356ff1a
44
docs/tutorial-effects.test.js
Normal file
44
docs/tutorial-effects.test.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
import u from 'updeep';
|
||||
import { action, Updux } from '../src/index.js';
|
||||
|
||||
const addTodoWithId = action('addTodoWithId');
|
||||
const incNextId = action('incNextId');
|
||||
const addTodo = action('addTodo');
|
||||
|
||||
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.nextId();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId({ description: action.payload, id });
|
||||
}
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: { nextId: 1, todos: [] },
|
||||
actions: { addTodo, incNextId, addTodoWithId },
|
||||
selectors: {
|
||||
nextId: ({nextId}) => nextId,
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: (todo) => u({ todos: (todos) => [...todos, todo] }),
|
||||
incNextId: () => u({ nextId: id => id+1 }),
|
||||
},
|
||||
effects: {
|
||||
'addTodo': addTodoEffect
|
||||
}
|
||||
});
|
||||
|
||||
const store = todosDux.createStore();
|
||||
|
||||
test( "tutorial example", async () => {
|
||||
store.dispatch.addTodo('Do the thing');
|
||||
|
||||
expect( store.getState() ).toMatchObject({
|
||||
nextId:2, todos: [ 'Do the thing' ]
|
||||
})
|
||||
|
||||
});
|
@ -72,3 +72,54 @@ todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
|
||||
todos: [...todos, { description, id, done: false }]
|
||||
}));
|
||||
```
|
||||
## Effects
|
||||
|
||||
In addition to mutations, Updux also provides action-specific middleware, here
|
||||
called effects.
|
||||
|
||||
Effects use the usual Redux middleware signature, plus a few goodies.
|
||||
The `getState` and `dispatch` functions are augmented with the dux selectors,
|
||||
and actions, respectively. The selectors and actions are also available
|
||||
from the api object.
|
||||
|
||||
```js
|
||||
import u from 'updeep';
|
||||
import { action, Updux } from 'updux';
|
||||
|
||||
// we want to decouple the increment of next_id and the creation of
|
||||
// a new todo. So let's use a new version of the action 'addTodo'.
|
||||
|
||||
const addTodoWithId = action('addTodoWithId');
|
||||
const incNextId = action('incNextId');
|
||||
const addTodo = action('addTodo');
|
||||
|
||||
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.nextId();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch.addTodoWithId({ description: action.payload, id });
|
||||
}
|
||||
|
||||
const todosDux = new Updux({
|
||||
initial: { nextId: 1, todos: [] },
|
||||
actions: { addTodo, incNextId, addTodoWithId },
|
||||
selectors: {
|
||||
nextId: ({nextId}) => nextId,
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: (todo) => u({ todos: (todos) => [...todos, todo] }),
|
||||
incNextId: () => u({ nextId: id => id+1 }),
|
||||
},
|
||||
effects: {
|
||||
'addTodo': addTodoEffect
|
||||
}
|
||||
});
|
||||
|
||||
const store = todosDux.createStore();
|
||||
|
||||
store.dispatch.addTodo('Do the thing');
|
||||
```
|
||||
|
||||
|
11
src/Updux.js
11
src/Updux.js
@ -4,11 +4,7 @@ import { createStore as reduxCreateStore } from 'redux';
|
||||
|
||||
import { buildSelectors } from './selectors.js';
|
||||
import { buildUpreducer } from './upreducer.js';
|
||||
import { action } from './actions.js';
|
||||
|
||||
function isActionGen(action) {
|
||||
return typeof action === 'function' && action.type;
|
||||
}
|
||||
import { action, isActionGen } from './actions.js';
|
||||
|
||||
/**
|
||||
* Updux configuration object
|
||||
@ -23,6 +19,7 @@ export class Updux {
|
||||
#mutations = {};
|
||||
#config = {};
|
||||
#selectors = {};
|
||||
#effects = {};
|
||||
|
||||
constructor(config = {}) {
|
||||
this.#config = config;
|
||||
@ -36,6 +33,10 @@ export class Updux {
|
||||
this.#addSubduxActions(slice, sub),
|
||||
);
|
||||
|
||||
Object.entries(config.mutations ?? {}).forEach((args) =>
|
||||
this.setMutation(...args),
|
||||
);
|
||||
|
||||
this.#selectors = buildSelectors(
|
||||
config.selectors,
|
||||
config.splatSelectors,
|
||||
|
@ -1,3 +1,7 @@
|
||||
function isActionGen(action) {
|
||||
return typeof action === 'function' && action.type;
|
||||
}
|
||||
|
||||
export function action(type, payloadFunction, transformer) {
|
||||
let generator = function (...payloadArg) {
|
||||
const result = { type };
|
||||
|
@ -1 +1,2 @@
|
||||
export { Updux, dux } from './Updux.js';
|
||||
export { action } from './actions.js';
|
||||
|
82
src/middleware.js
Normal file
82
src/middleware.js
Normal file
@ -0,0 +1,82 @@
|
||||
import R from 'remeda';
|
||||
|
||||
const composeMw = (mws) => (api) => (original_next) =>
|
||||
mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||
|
||||
export function augmentMiddlewareApi(api, actions, selectors) {
|
||||
const getState = () => api.getState();
|
||||
const dispatch = (action) => api.dispatch(action);
|
||||
|
||||
Object.assign(
|
||||
getState,
|
||||
R.mapValues(selectors, (selector) => {
|
||||
return (...args) => {
|
||||
let result = selector(api.getState());
|
||||
|
||||
if (typeof result === 'function') return result(...args);
|
||||
|
||||
return result;
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
dispatch,
|
||||
R.mapValues(actions, (action) => {
|
||||
return (...args) => api.dispatch(action(...args));
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...api,
|
||||
getState,
|
||||
dispatch,
|
||||
actions,
|
||||
selectors,
|
||||
};
|
||||
}
|
||||
|
||||
const sliceMw = (slice, mw) => (api) => {
|
||||
const getSliceState = () => get(api.getState(), slice);
|
||||
return mw({ ...api, getState: getSliceState });
|
||||
};
|
||||
|
||||
const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
|
||||
if (type !== '*' && action.type !== type) return next(action);
|
||||
|
||||
return middleware(api)(next)(action);
|
||||
};
|
||||
|
||||
export const effectToMiddleware = (effect, actions, selectors) => {
|
||||
let mw = effect;
|
||||
let action = '*';
|
||||
|
||||
if (Array.isArray(effect)) {
|
||||
action = effect[0];
|
||||
mw = effect[1];
|
||||
mw = middlewareFor(action, mw);
|
||||
}
|
||||
|
||||
return (api) => mw(augmentMiddlewareApi(api, actions, selectors));
|
||||
};
|
||||
|
||||
export function buildMiddleware(
|
||||
effects = [],
|
||||
actions = {},
|
||||
selectors = {},
|
||||
subduxes = {},
|
||||
) {
|
||||
let inner = R.compact(
|
||||
Object.entries(subduxes).map((slice, [{ middleware }]) =>
|
||||
slice !== '*' && middleware ? sliceMw(slice, middleware) : null,
|
||||
),
|
||||
);
|
||||
|
||||
const local = effects.map((effect) =>
|
||||
effectToMiddleware(effect, actions, selectors),
|
||||
);
|
||||
|
||||
let mws = [...local, ...inner];
|
||||
|
||||
return composeMw(mws);
|
||||
}
|
48
src/middleware.test.js
Normal file
48
src/middleware.test.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { test, expect, vi } from 'vitest';
|
||||
|
||||
import { buildMiddleware } from './middleware.js';
|
||||
import { action } from './actions.js';
|
||||
|
||||
test('buildMiddleware, effects', async () => {
|
||||
const effectMock = vi.fn();
|
||||
|
||||
const mw = buildMiddleware([
|
||||
['*', (api) => (next) => (action) => effectMock()],
|
||||
]);
|
||||
|
||||
mw({})(() => {})({});
|
||||
|
||||
expect(effectMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('buildMiddleware, augmented api', async () => {
|
||||
const myAction = action('myAction');
|
||||
|
||||
const mw = buildMiddleware(
|
||||
[
|
||||
[
|
||||
'*',
|
||||
(api) => (next) => (action) => {
|
||||
expect(api.getState.mySelector()).toEqual(13);
|
||||
api.dispatch(myAction());
|
||||
next();
|
||||
},
|
||||
],
|
||||
],
|
||||
{
|
||||
myAction,
|
||||
},
|
||||
{
|
||||
mySelector: (state) => state?.selected,
|
||||
},
|
||||
);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const getState = vi.fn(() => ({ selected: 13 }));
|
||||
const next = vi.fn();
|
||||
|
||||
mw({ dispatch, getState })(next)(myAction());
|
||||
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
expect(dispatch).toHaveBeenCalledWith(myAction());
|
||||
});
|
Loading…
Reference in New Issue
Block a user