All Projects → wellguimaraes → Actionware

wellguimaraes / Actionware

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

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Actionware

Thinkful Workshop React Redux Node Mongodb Webpack2
Stars: ✭ 12 (-94.15%)
Mutual labels:  boilerplate, react-redux
React Social Network
Simple React Social Network
Stars: ✭ 409 (+99.51%)
Mutual labels:  boilerplate, react-redux
Whitestorm Typescript Boilerplate
📦 🚀 TypeScript boilerplate for WhitestormJS using react/redux ⚛
Stars: ✭ 285 (+39.02%)
Mutual labels:  boilerplate, react-redux
Create React App Redux
React Router, Redux, Redux Thunk & Create React App boilerplate
Stars: ✭ 885 (+331.71%)
Mutual labels:  boilerplate, react-redux
Comicbook
React-Native跨平台漫画App免费视频:http://www.kongyixueyuan.com/course/3528
Stars: ✭ 199 (-2.93%)
Mutual labels:  react-redux
Node Rem
Node REM - NodeJS Rest Express MongoDB and more: typescript, passport, JWT, socket.io, HTTPS, HTTP2, async/await, nodemailer, templates, pagination, docker, etc. Live Demo: https://node-rem-ngduc.vercel.app
Stars: ✭ 192 (-6.34%)
Mutual labels:  boilerplate
Eleventy Starter Ghost
A starter template to build websites with Ghost & Eleventy
Stars: ✭ 187 (-8.78%)
Mutual labels:  boilerplate
Alldemo
🍑 2020全栈学习Demo大合集 包含最新 hooks TS 等 还有umi+dva,数据可视化等实战项目 (持续更新中)
Stars: ✭ 189 (-7.8%)
Mutual labels:  react-redux
Imooc React
慕课网 React 视频课程源代码
Stars: ✭ 203 (-0.98%)
Mutual labels:  react-redux
Saas
Build your own SaaS business with SaaS boilerplate. Productive stack: React, Material-UI, Next, MobX, WebSockets, Express, Node, Mongoose, MongoDB. Written with TypeScript.
Stars: ✭ 2,720 (+1226.83%)
Mutual labels:  boilerplate
Styled React Boilerplate
Minimal & Modern boilerplate for building apps with React & styled-components
Stars: ✭ 198 (-3.41%)
Mutual labels:  boilerplate
Yii2 Angular Boilerplate
Yii2 REST API + Angular10 Boilerplate (Frontend/Backend)
Stars: ✭ 194 (-5.37%)
Mutual labels:  boilerplate
Koa Vue Notes Web
🤓 This is a simple SPA built using Koa as the backend, Vue as the first frontend, and React as the second frontend. Features MySQL integration, user authentication, CRUD note actions, and Vuex store modules.
Stars: ✭ 200 (-2.44%)
Mutual labels:  boilerplate
Limestone
Boilerplate Rails 6 SaaS application with Webpack, Stimulus and Docker integration.
Stars: ✭ 191 (-6.83%)
Mutual labels:  boilerplate
React Ant Admin
使用 ant-design react react-hook ts 开发的类 ant-design-pro 管理后台,具有完整的权限系统和配套的node + ts 的 api
Stars: ✭ 199 (-2.93%)
Mutual labels:  react-redux
Koa Passport Mongoose Graphql
Koa 2 server with Passport + Mongoose + GraphQL
Stars: ✭ 190 (-7.32%)
Mutual labels:  boilerplate
Angular 8 Registration Login Example
Angular 8 User Registration and Login Example
Stars: ✭ 198 (-3.41%)
Mutual labels:  boilerplate
Express Graphql Boilerplate
Express GraphQL API with JWT Authentication and support for sqlite, mysql, and postgresql
Stars: ✭ 201 (-1.95%)
Mutual labels:  boilerplate
Koa2 Boilerplate
[Deprecated] Minimal koa v2 boilerplate. 🤣
Stars: ✭ 196 (-4.39%)
Mutual labels:  boilerplate
Expo Native Firebase
🔥 Native Firebase Expo App (iOS, Android) Demo for Firestore, Notifications, Analytics, Storage, Messaging, Database 🚨
Stars: ✭ 197 (-3.9%)
Mutual labels:  boilerplate

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

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].