diff --git a/README.md b/README.md index de09571..8fecd4e 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ see [running on CodeSandbox][cs]. ## API -This package exports a grand total of three functions. +This package exports a grand total of four functions. A lot of the generics for these functions can be inferred (see above example). The typings below provided are optimized for readability. @@ -155,6 +155,39 @@ The `reduceReducers` function takes an array of reducer functions and an optional initial state value and returns a single reducer which runs all of the input reducers in sequence. +### `createAsyncActions(type: string, startPayloadCreator, successPayloadCreator, failPayloadCreator)` + +Oftentimes when working with sagas, thunks, or some other asynchronous, +side-effecting middleware you need to create three actions which are named +similarly. This is a convenience function which calls `createAction` three +times for you. Consider the following example: + +```ts +import { noop } from 'lodash'; +import { createAsyncActions } from 'redux-ts-utils'; + +type User = { name: string }; + +export const [ + requestUsers, + requestUsersSuccess, + requestUsersFailure, +] = createAsyncActions('REQUEST_USERS', noop, (users: User[]) => users); + +requestUsers(); // returns action of type `REQUEST_USERS` +requestUsersSuccess([{ name: 'knpwrs' }]); // returns action of type `REQUEST_USERS/SUCCESS` +requestUsersError(); // returns action of type `REQUEST_USERS/ERROR` +``` + +The first argument is the action/triad name, and the second through third +(optional) arguments are payload creators for the initial action, the success +action, and the error action, respectively. `noop` is imported from lodash in +order to be explicit that in this case the payload for `requestUsers` is +`void`. You can just as easily use `() => {}` inline. The action creators infer +their payload types from the supplied payload creators. See [the +implementation](./src/create-async-actions.ts) for complete type information. + + ## Design Philosophy ### A Strong Emphasis on Type Safety diff --git a/src/create-action.ts b/src/create-action.ts index 9a00896..49c568f 100644 --- a/src/create-action.ts +++ b/src/create-action.ts @@ -13,7 +13,7 @@ export interface TsActionCreator

{ } export type PayloadCreator = (...args: A) => P; -const identity = (...arg: T): T[0] => arg[0]; +export const identity = (...arg: T): T[0] => arg[0]; // eslint-disable-next-line arrow-parens export default ( diff --git a/src/create-async-actions.test.ts b/src/create-async-actions.test.ts new file mode 100644 index 0000000..f50adfc --- /dev/null +++ b/src/create-async-actions.test.ts @@ -0,0 +1,75 @@ +import createAsyncActions from './create-async-actions'; + +test('creates a triad of identity action creators', () => { + const [start, success, fail] = createAsyncActions('foo'); + expect(start.type).toBe('foo'); + expect(success.type).toBe('foo/SUCCESS'); + expect(fail.type).toBe('foo/ERROR'); + + expect(start('foo')).toEqual({ + type: 'foo', + payload: 'foo', + }); + + expect(success('foo')).toEqual({ + type: 'foo/SUCCESS', + payload: 'foo', + }); + + const err = new Error('foo'); + expect(fail(err)).toEqual({ + type: 'foo/ERROR', + payload: err, + error: true, + }); +}); + +test('creates a triad of action creators with custom payloads', () => { + const [start, success, fail] = createAsyncActions( + 'bar', + (str: string) => str, + (length: number) => length, + ); + expect(start.type).toBe('bar'); + expect(success.type).toBe('bar/SUCCESS'); + expect(fail.type).toBe('bar/ERROR'); + + expect(start('bar')).toEqual({ + type: 'bar', + payload: 'bar', + }); + + expect(success(3)).toEqual({ + type: 'bar/SUCCESS', + payload: 3, + }); + + const err = new Error('foo'); + expect(fail(err)).toEqual({ + type: 'bar/ERROR', + payload: err, + error: true, + }); +}); + +test('allows for mixed void and any', () => { + const [start, success, fail] = createAsyncActions( + 'baz', + () => {}, + (users: { name: string }[]) => users, + ); + + expect(start()).toEqual({ + type: 'baz', + }); + expect(success([{ name: 'knpwrs' }])).toEqual({ + type: 'baz/SUCCESS', + payload: [{ name: 'knpwrs' }], + }); + const err = new Error('baz'); + expect(fail(err)).toEqual({ + type: 'baz/ERROR', + payload: err, + error: true, + }); +}); diff --git a/src/create-async-actions.ts b/src/create-async-actions.ts new file mode 100644 index 0000000..a063dd4 --- /dev/null +++ b/src/create-async-actions.ts @@ -0,0 +1,23 @@ +import createAction, { PayloadCreator, TsActionCreator, identity } from './create-action'; + +export default < + PStart, + AStart extends any[] = [PStart], + PSuc = PStart, + ASuc extends any[] = AStart, + PErr = Error, + AErr extends any[] = [PErr] +>( + name: string, + startPc: PayloadCreator = identity, + sucPc: PayloadCreator = identity, + errPc: PayloadCreator = identity, +): [ + TsActionCreator, + TsActionCreator, + TsActionCreator, +] => [ + createAction(name, startPc), + createAction(`${name}/SUCCESS`, sucPc), + createAction(`${name}/ERROR`, errPc), +]; diff --git a/src/index.test.ts b/src/index.test.ts index 9f1e250..b3c2a82 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,5 @@ import createAction from './create-action'; +import createAsyncActions from './create-async-actions'; import handleAction from './handle-action'; import reduceReducers from './reduce-reducers'; import * as mod from '.'; @@ -6,6 +7,7 @@ import * as mod from '.'; test('module exports', () => { expect(mod).toEqual({ createAction, + createAsyncActions, handleAction, reduceReducers, }); diff --git a/src/index.ts b/src/index.ts index 25e7c52..89c2b83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { TsAction, TsActionCreator, } from './create-action'; +export { default as createAsyncActions } from './create-async-actions'; export { default as handleAction, Draft,