actionware

by wellguimaraes

wellguimaraes / actionware

Redux with less boilerplate, actions statuses and controlled side-effects in a single shot.

208 Stars 7 Forks Last release: Not found MIT License 137 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

Actionware

Build Status

Redux with less boilerplate, actions statuses and controlled side-effects in a single shot.

  • no more action creators and action types, just actions¹ and reducers
  • actions dispatch their result automatically
  • error status for every action with no extra code
  • busy status for every async action (yep, no extra code!)
  • cancellable actions

¹ With Actionware, actions have a different meaning: they're just functions which execution generate events. See usage section to better understand.

Extra power

Wanna have state selectors/getters in a decent way? Use it combined with Stateware lib.

Setup

Install it

  • Yarn:
    yarn add actionware
  • NPM:
    npm i actionware --save

After creating your Redux store, let Actionware know your store instance. Optionally you

can define custom action types prefix and suffixes: ```js import * as actionware from 'actionware';

actionware.setup({ store, defaultPrefix, // default: 'actionware:' errorSuffix, // default: ':error' cancelSuffix, // default: ':cancel' busySuffix // default: ':busy' }); ```

Add actionware reducer to your root reducer:

To make Redux store react to busy and error status changes, make sure you add the Actionware reducer into your root reducer. ```js import { combineReducers } from 'redux'; import { actionwareReducer } from 'actionware';

const rootReducer = combineReducers({ actionware: actionwareReducer, // your reducers }); ```

Usage

Simple actions

export function incrementCounter() { }

Async actions

Whatever you return will be the action payload

// Note that the store is always the last arg
export async function loadUsers(arg1, arg2, argN, store) {
  const response = await fetch('/my/api/users');
  return response.json();
}

Invoke any action

Use

call
to invoke an action and let Actionware handle the execution lifecycle (managing error and busy statuses, notifying listeners, etc). ```js import { call } from 'actionware';

call(loadUsers, arg1, arg2, argN); ```

Cancel an action execution

import { call } from 'actionware';

const actionCall = call(loadUsers, arg1, arg2, argN);

actionCall.cancel()

To cancel inner calls or other async executions, use

setExtra
inside an async action to keep information needed and use them on a cancellation listener: ```js import { call, onCancel} from 'actionware'; import api from './path/to/api';

// Don't use arrow functions here, // otherwise a context value can't be set export async function someAction() { const apiCall = api.get('/some/endpoint') const anotherActionCall = call(anotherAction, 'someParam')

this.setExtra({ apiCall }) this.setExtra({ anotherActionCall }) // you can call it multiple times

const apiResponse = await apiCall const anotherResponse = await anotherActionCall

// ...

return apiResponse.data }

export async function anotherAction() { // ... }

onCancel(someAction, ({ extras }) => { // Check if the action execution is still cancellable if (extras.anotherActionCall.canBeCancelled) extras.anotherActionCall.cancel()

// Cancel the api call... }) ```

Clear action error

import { clearError } from 'actionware'

export async function someAction() { // ... }

clearError(someAction)

Reducers:

import { createReducer } from 'actionware';
import { loadUsers, persistUser, incrementCounter } from 'path/to/actions';

const initialState = { users: [], count: 0 };

export default createReducer(initialState) .on(loadUsers, (state, users) => ({ ...state, users }))

.on(incrementCounter, (state) => ({ ...state, counter: state.counter + 1 }))

// Bind legacy action types .on('OLD_ACTION_TYPE', (state, payload) => { /* return new state */ })

// Bind multiple actions to the same handler
.on( someAction, anotherAction, (state, payload) => { /* return new state */ })

// Actionware handles errors, cancellation and 'before' events, // but if you need to do something else

.onError(persistUser, (state, error, ...args) => { /* return new state */ })

.onCancel(loadUsers, (state, extras, ...args) => { /* return new state */ })

.before(loadUsers, (state, ...args) => { /* return new state */ });

Busy and failure statuses for all your actions:

import { getError, isBusy } from 'actionware';
import { loadUsers } from 'path/to/userActions';

// Whenever needed... isBusy(loadUsers); getError(loadUsers);

Use listeners to manage side effects:

Note that busy listeners are called when busy status changes. ```js import { onSuccess, onError, onCancel, before, beforeAll } from 'actionware'; import { createUser } from 'path/to/actions';

// global success listener onSuccess(({ action, args, payload, store }) => eventTracker.register(action.name));

// per action success listener onSuccess(createUser, ({ args, payload, store }) => history.push(

/users/${user.id}
));

// error listeners onError(({ action, args, error }) => { /* ... / }); onError(createUser, ({ args, error }) => { / ... */ });

// cancellation listeners onCancel(({ action, args, extras }) => { /* ... / }); onCancel(createUser, ({ args, extras }) => { / ... */ });

// before listeners // NOTE: 'beforeAll' is just an alias for 'before' beforeAll(({ action, args, store}) => { /* ... / }); before(createUser, ({ args, store }) => { / ... */ }); ```

Interaction-dependent flows

When you have "complex" flows that depend on some interaction to start or continue, you can use

next
to wait for some action completion in this fashion: ```js import { call, next } from 'actionware'; import { login, showTip, acknowledgeTip } from 'path/to/actions';

export async function appEducationFlow() { // Wait for the next successful login await next(login);

call(showTip, 'headerButtons'); await next(acknowledgeTip);

history.redirect('/some/route');

call(showTip, 'sideMenu'); await next(acknowledgeTip); }

// At some point, start the flow appEducationFlow(); ```

Usage with React

Inject actions and status into components as props

By using

withActions
to wrap a component, actions are injected into it as props and can be invoked without using
call
. ```js import * as React from 'react'; import { connect } from 'react-redux'; import { withActions, isBusy, getError } from 'actionware'; import { loadUsers } from 'path/to/actions';

const actions = { loadUsers };

const mapStateToProps = ({ company }) => ({ users : company.users, loading : isBusy(loadUsers), error : getError(loadUsers) });

@connect(mapStateToProps) @withActions(actions) class MyConnectedComponent extends Component { componentDidMount() { this.props.loadUsers();
}

render() { const { loading, error } = this.props;

if (loading) return (
Loading...
); if (error) return (
Failed to load users...
);

return (

{ users.map(it => ) }
);

} }

export default MyConnectedComponent ```

Without injecting actions as props

In case you prefer not injecting actions as props into your component, you can use

createActions
this way: ```js import { createActions } from 'actionware'

const actions = createActions('optionalPrefix:', { someAction, anotherAction })

const MyComponent = () => (

)

Testing

Mock call and next functions

While testing, you're able to replace the call and next functions by custom spy/stub to simplify tests.

```js import { mockCallWith, mockNextWith } from 'actionware';

const callSpy = sinon.spy(); const nextStub = sinon.stub().returns(Promise.resolve());

mockCallWith(callSpy); mockNextWith(nextStub);

// Get back to default behavior mockCallWith(null); mockNextWith(null);

Reducers

For testing reducers, you can do the following:

import { successType } from 'actionware';
import { loadUsers } from 'path/to/userActions';
import usersReducer from 'path/to/usersReducer';

describe('usersReducer', () => { describe('on loadUsers', () => { it('should replace the "users" array with the loaded users', () => { const currentState = { users: [ ] }; const loadedUsers = [ 'John Doe', 'Joane Doe', 'Steve Gates' ];

  // Call reducer with currentState and a regular Redux action       
  const newState = usersReducer(
    currentState, 
    { type: successType(loadUsers), payload: loadedUsers }
  );

  expect(newState.items).to.equals(loadedUsers);
});  

}); });

API

Setup

  • setup({ store, defaultPrefix?, errorSuffix?, busySuffix?, cancelSuffix? }): void

Most used

  • withActions(actions: object): Function(wrappedComponent: Component)
  • createActions(actions: object): object
  • isBusy(action: Function): bool
  • getError(action: Function): object
  • clearError(action: Function): void
  • call(action: Function, ...args)
  • next(action: Function)
  • createReducer(initialState: object, handlers: []): Function

Listeners

Global
  • onSuccess(listener: ({ action, payload, args, store }) => void)
  • onError(listener: ({ action, error, args, store }) => void)
  • beforeAll(listener: ({ action, args, store}) => void)
Per action
  • onSuccess(action: Function, listener: ({ payload, args, store }) => void)
  • onError(action: Function, listener: ({ error, args, store }) => void)
  • before(action: Function, listener: ({ args, store }) => void)

Test helpers

  • mockCallWith(fakeCall: Function)
  • mockNextWith(fakeNext: Function)
  • successType(action: Function)
  • errorType(action: Function)
  • busyType(action: Function)

License

MIT © Wellington Guimaraes

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.