feat!: use ts-action for action creation

This commit is contained in:
Yanick Champoux 2020-02-04 12:02:28 -05:00
parent f6b7c2b15a
commit 6349d720b8
10 changed files with 88 additions and 99 deletions

View File

@ -1,4 +1,19 @@
# Updux concepts # Updux concepts
## actions
Updux internally uses the package `ts-action` to create action creator
functions. Even if you don't use typescript, I recommend that you use it,
as it does what it does very well. But if you don't want to, no big deal.
Updux will recognize a function as an action creator if it has a `type`
property. So a homegrown creator could be as simple as:
```js
function action(type) {
return Object.assign( payload => ({type, payload}), { type } )
}
```
## effects ## effects

View File

@ -2,22 +2,23 @@
"dependencies": { "dependencies": {
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mobx": "^5.14.2", "mobx": "^5.14.2",
"redux": "^4.0.4" "redux": "^4.0.4",
"ts-action": "^11.0.0",
"updeep": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"docsify": "^4.10.2",
"docsify-cli": "^4.4.0",
"@babel/cli": "^7.6.4", "@babel/cli": "^7.6.4",
"@babel/core": "^7.6.4", "@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3", "@babel/preset-env": "^7.6.3",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"@types/lodash": "^4.14.144", "@types/lodash": "^4.14.144",
"babel-jest": "^24.9.0", "babel-jest": "^24.9.0",
"docsify": "^4.10.2",
"docsify-cli": "^4.4.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"ts-jest": "^24.1.0", "ts-jest": "^24.1.0",
"tsd": "^0.10.0", "tsd": "^0.10.0",
"typescript": "^3.6.4", "typescript": "^3.6.4"
"updeep": "^1.2.0"
}, },
"license": "MIT", "license": "MIT",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -1,26 +1,27 @@
import Updux, {actionCreator} from '.'; import { action, payload } from 'ts-action';
import u from 'updeep'; import u from 'updeep';
import Updux from '.';
const noopEffect = () => () => () => {}; const noopEffect = () => () => () => {};
test('actions defined in effects and mutations, multi-level', () => { test('actions defined in effects and mutations, multi-level', () => {
const bar = action('bar',(payload,meta) => ({payload,meta}) );
const foo = action('foo',(limit:number) => ({payload:{ limit} }) );
const {actions} = new Updux({ const {actions} = new Updux({
effects: { effects: [ [ foo, noopEffect ] ],
foo: noopEffect, mutations: [ [ bar, () => () => null ] ],
},
mutations: {bar: () => () => null},
subduxes: { subduxes: {
mysub: { mysub: {
effects: {baz: noopEffect}, effects: {baz: noopEffect},
mutations: {quux: () => () => null}, mutations: {quux: () => () => null},
actions: { actions: {
foo: (limit: number) => ({limit}), foo
}, },
}, },
myothersub: { myothersub: {
effects: { effects: [ [foo, noopEffect] ],
foo: noopEffect,
},
}, },
}, },
}); });
@ -32,7 +33,7 @@ test('actions defined in effects and mutations, multi-level', () => {
expect(actions.bar()).toEqual({type: 'bar'}); expect(actions.bar()).toEqual({type: 'bar'});
expect(actions.bar('xxx')).toEqual({type: 'bar', payload: 'xxx'}); expect(actions.bar('xxx')).toEqual({type: 'bar', payload: 'xxx'});
expect(actions.bar(undefined, 'yyy')).toEqual({type: 'bar', meta: 'yyy'}); expect(actions.bar(undefined, 'yyy')).toEqual({type: 'bar', payload: undefined, meta: 'yyy'});
expect(actions.foo(12)).toEqual({type: 'foo', payload: {limit: 12}}); expect(actions.foo(12)).toEqual({type: 'foo', payload: {limit: 12}});
}); });
@ -41,23 +42,15 @@ describe('different calls to addAction', () => {
const updux = new Updux(); const updux = new Updux();
test('string', () => { test('string', () => {
updux.addAction('foo'); updux.addAction( action('foo', payload() ));
expect(updux.actions.foo('yo')).toMatchObject({ expect(updux.actions.foo('yo')).toMatchObject({
type: 'foo', type: 'foo',
payload: 'yo', payload: 'yo',
}); });
}); });
test('actionCreator', () => {
const bar = actionCreator('bar', null);
updux.addAction(bar);
expect(updux.actions.bar()).toMatchObject({
type: 'bar',
});
});
test('actionCreator inlined', () => { test('actionCreator inlined', () => {
updux.addAction('baz', (x) => ({x})); updux.addAction( 'baz', (x) => ({payload: {x}}));
expect(updux.actions.baz(3)).toMatchObject({ expect(updux.actions.baz(3)).toMatchObject({
type: 'baz', payload: { x: 3 } type: 'baz', payload: { x: 3 }
}); });

View File

@ -1,4 +1,6 @@
import Updux, { actionCreator } from "./updux"; import { action } from 'ts-action';
import Updux from "./updux";
type MyState = { type MyState = {
sum: number; sum: number;
@ -9,7 +11,7 @@ test("added mutation is present", () => {
initial: { sum: 0 } initial: { sum: 0 }
}); });
const add = actionCreator("add", (n: number) => ({ n })); const add = action("add", (n: number) => ({ payload: { n } }));
updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n })); updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n }));

View File

@ -1,47 +1,9 @@
import fp from 'lodash/fp'; import fp from 'lodash/fp';
import { import {
Action,
ActionCreator, ActionCreator,
ActionPayloadGenerator,
Dictionary, Dictionary,
} from '../types'; } from '../types';
export function actionCreator<T extends string, P extends any>(
type: T,
transform: (...args: any[]) => P
): ActionCreator<T, P>;
export function actionCreator<T extends string>(
type: T,
transform: null
): ActionCreator<T, null>;
export function actionCreator<T extends string>(
type: T
): ActionCreator<T, undefined>;
export function actionCreator(type: any, transform?: any) {
if (transform) {
return Object.assign(
(...args: any[]) => ({ type, payload: transform(...args) }),
{ type }
);
}
if (transform === null) {
return Object.assign(() => ({ type }), { type });
}
return Object.assign((payload: unknown) => ({ type, payload }), { type });
}
export function actionFor(type: string): ActionCreator {
const f = (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({ type, payload, meta }) as Action;
return Object.assign(f, {
_genericAction: true,
type,
});
}
type ActionPair = [string, ActionCreator]; type ActionPair = [string, ActionCreator];
function buildActions(actions: ActionPair[] = []): Dictionary<ActionCreator> { function buildActions(actions: ActionPair[] = []): Dictionary<ActionCreator> {

View File

@ -3,6 +3,4 @@ import Updux from "./updux";
export { default as Updux } from "./updux"; export { default as Updux } from "./updux";
export { UpduxConfig } from "./types"; export { UpduxConfig } from "./types";
export { actionCreator } from "./buildActions";
export default Updux; export default Updux;

View File

@ -1,5 +1,7 @@
import Updux, { actionCreator } from '.';
import u from 'updeep'; import u from 'updeep';
import { action, payload } from 'ts-action';
import Updux from '.';
import mwUpdux from './middleware_aux'; import mwUpdux from './middleware_aux';
test('simple effect', () => { test('simple effect', () => {
@ -179,7 +181,7 @@ test('middleware as map', () => {
let rootState; let rootState;
let rootFromChild; let rootFromChild;
const doIt = actionCreator('doIt'); const doIt = action('doIt', () => ({payload: ''}));
const child = new Updux({ const child = new Updux({
initial: '', initial: '',

View File

@ -1,7 +1,9 @@
import Updux, { actionCreator } from "./updux"; import { action } from 'ts-action';
import Updux from "./updux";
describe("as array of arrays", () => { describe("as array of arrays", () => {
const doIt = actionCreator("doIt"); const doIt = action("doIt");
const updux = new Updux({ const updux = new Updux({
initial: "", initial: "",

View File

@ -13,11 +13,6 @@ test('actions from mutations', () => {
expect(foo(true)).toEqual({type: 'foo', payload: true}); expect(foo(true)).toEqual({type: 'foo', payload: true});
expect(foo({bar: 2}, {timestamp: 613})).toEqual({
type: 'foo',
payload: {bar: 2},
meta: {timestamp: 613},
});
}); });
test('reducer', () => { test('reducer', () => {

View File

@ -1,7 +1,8 @@
import fp from "lodash/fp"; import fp from "lodash/fp";
import u from "updeep"; import u from "updeep";
import { action, payload } from 'ts-action';
import buildActions, { actionFor, actionCreator } from "./buildActions"; import buildActions from "./buildActions";
import buildInitial from "./buildInitial"; import buildInitial from "./buildInitial";
import buildMutations from "./buildMutations"; import buildMutations from "./buildMutations";
@ -24,7 +25,6 @@ import {
import { Middleware, Store, PreloadedState } from "redux"; import { Middleware, Store, PreloadedState } from "redux";
import buildSelectors from "./buildSelectors"; import buildSelectors from "./buildSelectors";
export { actionCreator } from "./buildActions";
type StoreWithDispatchActions< type StoreWithDispatchActions<
S = any, S = any,
@ -76,9 +76,9 @@ export class Updux<S = any> {
); );
const actions = fp.getOr({}, "actions", config); const actions = fp.getOr({}, "actions", config);
Object.entries(actions).forEach(([type, payload]: [string, any]): any => Object.entries(actions).forEach(([type, p]: [string, any]): any =>
this.addAction( this.addAction(
(payload as any).type ? payload : actionCreator(type, payload as any) (p as any).type ? p : action(type, p)
) )
); );
@ -157,14 +157,23 @@ export class Updux<S = any> {
}; };
} }
addMutation<A extends ActionCreator>( addMutation<A extends ActionCreator=any>(
creator: A, creator: A,
mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>, mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>,
isSink?: boolean isSink?: boolean
) { )
let c = fp.isFunction(creator) ? creator : actionFor(creator); addMutation<A extends ActionCreator=any>(
creator: string,
this.addAction(c); mutation: Mutation<S, any>,
isSink?: boolean
)
addMutation<A extends ActionCreator=any>(
creator,
mutation,
isSink
)
{
let c = this.addAction(creator);
this.localMutations[c.type] = [ this.localMutations[c.type] = [
this.groomMutations(mutation as any) as Mutation<S>, this.groomMutations(mutation as any) as Mutation<S>,
@ -177,24 +186,34 @@ export class Updux<S = any> {
middleware: UpduxMiddleware<S>, middleware: UpduxMiddleware<S>,
isGenerator: boolean = false isGenerator: boolean = false
) { ) {
let c = fp.isFunction(creator) ? creator : actionFor(creator); const c = this.addAction(creator);
this.addAction(c);
this.localActions[c.type] = c;
this.localEffects.push([c.type, middleware, isGenerator]); this.localEffects.push([c.type, middleware, isGenerator]);
} }
addAction(action: string, transform?: any): ActionCreator<string,any> // can be
addAction(action: ActionCreator<any>, transform?: never): ActionCreator<string,any> //addAction( actionCreator )
addAction(action: any,transform:any) { // addAction( 'foo', transform )
if (typeof action === "string") { addAction(theaction: string, transform?: any): ActionCreator<string,any>
if (!this.localActions[action]) { addAction(theaction: string|ActionCreator<any>, transform?: never): ActionCreator<string,any>
this.localActions[action] = actionCreator(action,transform); addAction(theaction: any,transform:any) {
if (typeof theaction === "string") {
if(transform !== undefined ) {
theaction = action(theaction,transform);
}
else {
theaction = this.actions[theaction] || action(theaction,payload())
} }
return this.localActions[action];
} }
return this.localActions[action.type] = action; const already = this.actions[theaction.type];
if( already ) {
if ( already !== theaction ) {
throw new Error(`action ${theaction.type} already exists`)
}
return already;
}
return this.localActions[theaction.type] = theaction;
} }
get _middlewareEntries() { get _middlewareEntries() {