Skip to content
This repository has been archived by the owner on Feb 3, 2020. It is now read-only.

Commit

Permalink
feat(create-async-actions): add createAsyncActions utility function
Browse files Browse the repository at this point in the history
  • Loading branch information
knpwrs committed Feb 10, 2019
1 parent c84781f commit 407dadd
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 2 deletions.
35 changes: 34 additions & 1 deletion README.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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<T, A extends any[], ...>(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
Expand Down
2 changes: 1 addition & 1 deletion src/create-action.ts
Expand Up @@ -13,7 +13,7 @@ export interface TsActionCreator<P = void, A extends any[] = [P], M = void> {
}

export type PayloadCreator<P, A extends any[] = [P?]> = (...args: A) => P;
const identity = <T extends any[]>(...arg: T): T[0] => arg[0];
export const identity = <T extends any[]>(...arg: T): T[0] => arg[0];

// eslint-disable-next-line arrow-parens
export default <P, A extends any[] = [P?], M = void>(
Expand Down
75 changes: 75 additions & 0 deletions 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<string>('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,
});
});
23 changes: 23 additions & 0 deletions 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<PStart, AStart> = identity,
sucPc: PayloadCreator<PSuc, ASuc> = identity,
errPc: PayloadCreator<PErr, AErr> = identity,
): [
TsActionCreator<PStart, AStart>,
TsActionCreator<PSuc, ASuc>,
TsActionCreator<PErr, AErr>,
] => [
createAction<PStart, AStart>(name, startPc),
createAction<PSuc, ASuc>(`${name}/SUCCESS`, sucPc),
createAction<PErr, AErr>(`${name}/ERROR`, errPc),
];
2 changes: 2 additions & 0 deletions src/index.test.ts
@@ -1,11 +1,13 @@
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 '.';

test('module exports', () => {
expect(mod).toEqual({
createAction,
createAsyncActions,
handleAction,
reduceReducers,
});
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -4,6 +4,7 @@ export {
TsAction,
TsActionCreator,
} from './create-action';
export { default as createAsyncActions } from './create-async-actions';
export {
default as handleAction,
Draft,
Expand Down

0 comments on commit 407dadd

Please sign in to comment.