npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

actionware

v8.2.3

Published

Use Redux with much less boilerplate and get busy and error states for every action with no extra code.

Downloads

188

Readme

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:

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.

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).

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:

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.

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:

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.

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 (<div>Loading...</div>);
    if (error) return (<div>Failed to load users...</div>);
    
    return (
      <div>
        { users.map(it => <User key={it.id} {...it} />) }
      </div>
    );
  }
}

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:

import { createActions } from 'actionware'

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

const MyComponent = () => (
  <div>
    <button onClick={ actions.someAction }></button>
  </div>
)

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.

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