JSPM

  • Created
  • Published
  • Downloads 16
  • Score
    100M100P100Q66521F
  • License MIT

An opinionated library that allows you to create actions and reducers for Redux, automatically. Speedux reduces the amount of code that needs to be written in a Redux application giving you peace of mind and more time to code the important stuff.

Package Exports

  • speedux

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (speedux) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Speedux

npm version License npm downloads build status

An opinionated library for managing state in React apps.

   

Motivation

There are many things in life that are fun, working with Redux is not one of them. Redux is a great solution for state management but requires writing a lot of boilerplate code to setup and use.

Speedux reduces the amount of boilerplate code that you need to write in order to use Redux, which gives you peace of mind and more time to build the important stuff.

Speedux's API is simple and intuitive, you basically describe the state object and how it should be updated using plain JavaScript objects and functions.

   

Installation

Install with npm

npm install --save speedux

Install with yarn

yarn add speedux

   

Getting Started

Let's start with a simple counter app that displays three buttons. One button increases the count on click, another button decreases the count and a third button would reset the count.

The entry file

Start with the application entry file, it's usually the src/index.js file (assuming create-react-app). You would only need to import the store and Provider from Speedux and wrap your application with the Provider while passing it the store as a property. Normal Redux stuff but with less code.

import React from 'react';
import ReactDOM from 'react-dom';
import { store, Provider } from 'speedux';

import Counter from './Counter';

const App = (
  <Provider store={store}>
    <Counter />
  </Provider>
);

ReactDOM.render(App, document.getElementById('root'));

That's pretty much all you need to do here.

The module file

Next, create a module.js file that will contain the initial state for your stateful counter component and all the logic required to update it.

To create a module, simply call createModule and pass it a name for the module (any unique identifier string) and a configuration object then export the returned object.

We want our state to contain a count property with an initial value of zero. To update the count property, we need three actions: increaseCount, decreaseCount and resetCount.

import { createModule } from 'speedux';

export default createModule('counter', {
  state: { count: 0 },

  actions: {
    increaseCount() {
      return {
        count: this.state.count + 1,
      };
    },

    decreaseCount() {
      return {
        count: this.state.count - 1,
      };
    },

    resetCount: () => ({
      count: 0,
    }),
  },
});

Note that this.state which is used inside the module file is completely different from the local state of the component. Inside the module, this refers to the module object and this.state refers to the component related state object which lives in the Redux store.

Now we have a module object that describes the state and how it should be updated. Next, we need to use it to connect a component to the Redux store.

The component file

Finally, inside your stateful component file, you would need to call the connect function from Speedux and pass it the component and the module object as parameters then export the returned component.

The connect function will inject the module state and actions into the component props, each as an object.

import React, { Component } from 'react';
import { connect } from 'speedux';

import module from './module';

class Counter extends Component {
  render() {
    // use object destructuring to access module state and actions
    const { state, actions } = this.props;

    return (
      <div>
        <h1>Count is: {state.count}</h1>
        <button onClick={actions.increaseCount}>Increase count</button>
        <button onClick={actions.decreaseCount}>Decrease count</button>
        <button onClick={actions.resetCount}>Reset count</button>
      </div>
    );
  }
}

export default connect(Counter, module);

That's it! You have a fully working counter component that is connected to the Redux store. This was a very simple example to get you started. Keep reading to learn how to create asyncronous actions and listen to actions dispatched by other components.

   

Asyncronous Actions

In a real world application, you might need to fetch data from a remote source and update the UI accordingly. For such cases, you can use an asyncronous action. To create an asyncronous action, simply use a generator function instead of a normal function.

Whenever your generator function yields an object, that object will be used to update the Redux state. If your generator function yields a Promise object, the function execution will pause until that promise is resolved and the result will be passed to the generator function on the next call. Here is an example:

export default createModule('fetcher', {
  state: {
    loading: false,
    data: '',
  },

  actions: {
    // asyncronous action using a generator function
    *fetchData() {
      // yield an object to update the state and indicate that the data is being loaded
      yield { loading: true };

      // yield a promise to fetch the data
      const data = yield fetch('/api/posts').then(response => response.json());

      // yield an object to update the state and indicate that the data has been completely loaded
      yield {
        loading: false,
        data,
      };
    },
  },
});

   

Handling Errors

To handle errors in an asyncronous action, you can catch a rejected promise then check if the response is an instance of Error:

export default createModule('faultyFetcher', {
  state: {
    loading: false,
    data: '',
    error: null,
  },

  actions: {
    *fetchData() {
      // yield an object to update the state and indicate that the data is being loaded
      yield { loading: true };

      // yield a promise to fetch the data
      const result = yield fetch('/api/posts').then(response => response.json()).catch(err => err);

      if (result instanceof Error) {
        yield {
          error: result.message,
        };
      } else {
        // yield an object to update the state and indicate that the data has been completely loaded
        yield {
          loading: false,
          data: result.posts,
        };
      }
    },
  },
});

   

Listening to Actions

You can use the handlers configuration option to listen to any action dispatched by the Redux store. Simply, use the action type as the key and the handler function as the value. For example, if a foo module needs to listen to an action WOO_HOO dispatched by another module baz and also needs to listen to another action STOP_AUDIO:

export default createModule('foo', {
    handlers: {
      '@@baz/WOO_HOO'(action) { ... },
      'STOP_AUDIO'(action) { ... },
    }
});

If your code contains side effects, you can use a generator function instead of a normal function:

export default createModule('foo', {
    handlers: {
        *'@@baz/WOO_HOO'(action) { ... }
    }
});

Note:

// This syntax:
{ '@@baz/WOO_HOO'(action) { ... } }

// is identical to this:
{ '@@baz/WOO_HOO': function(action) { ... } }

If the action was created by another Speedux module, you can use the action reference instead of passing the action type as a string:

import bazModule from './components/Baz/module';

// This:
export default createModule('foo', {
  handlers: {
    [bazModule.actions.wooHoo](action) { ... }
  }
});

// is identical to this:
export default createModule('foo', {
  handlers: {
    '@@baz/WOO_HOO'(action) { ... }
  }
});

   

Updating the State

Both action and handler functions define how the state should be updated by returning an object. This object specifies the state keys that need to be updated and their new values. In the following example, changeFoo will only update foo in the state with value Bingo while fiz will remain the same.

export default createModule('foo', {
    state: {
        foo: 'baz',
        fiz: 'boo',
    },
    actions: {
        changeFoo() {
            return { foo: 'Bingo' };
        }
    }
});

Nested State Keys

To update deeply nested state keys, you can provide a string that uses dot notation:

export default createModule('foo', {
    state: {
        result: 0,
        data: {
            list: [
                { props: { name: 'feeb' } },
                { props: { name: 'foo' } },
                { props: { name: 'fiz' } },
            ],
        },
    },
    
    actions: {
        changeFooName(newName) {
            return { 'data.list[1].props.name': newName };
        },
    },
});

Wildcard Character: *

If you would like to modify all items of an array or an object in the state, use a wildcard character:

export default createModule('foo', {
    state: {
        list: [
            { name: 'feeb' },
            { name: 'foo' },
            { name: 'fiz' },
        ],
    },
    
    actions: {
        changeAllNames(newName) {
            return { 'list.*.name': newName };
        },
    },
});

/*
Invoking action changeAllNames('jane') will modify the state to:
{
    list: [
        { name: 'jane' },
        { name: 'jane' },
        { name: 'jane' },
    ],
}
*/

You can also use a wildcard for reading the state as well:

export default createModule('foo', {
    state: {
        list: [
            { name: 'feeb' },
            { name: 'foo' },
            { name: 'fiz' },
        ],
    },
    
    actions: {
        logAllNames() {
            const names = this.getState('list.*.name');
            console.log(names); // ['feeb', 'foo', 'fiz']
        },
    },
});

Mapper Function

You can pass a mapper function that returns the new value of the state key:

export default createModule('foo', {
    state: {
        list: [
            { count: 151 },
            { count: 120 },
            { count: 2 },
        ],
    },
    
    actions: {
        setMinimum() {
            return {
                'list.*.count': (oldValue) => {
                    if (oldValue < 50) return 50;
                    return oldValue;
                },
            };
        },
    },
});

/*
Invoking action setMinimum() will modify the state to:
{
    list: [
        { count: 151 },
        { count: 120 },
        { count: 50 },
    ],
}
*/

   

Dispatching Actions

Speedux provides a handy dispatch function that lets you dispatch any action and specify the action payload as well. Here is an example:

import React from 'react';
import { dispatch } from 'speedux';

const MyComponent = (props) => {
    return (
        <div>
            <button
                onClick={() => {
                    dispatch('SOME_ACTION', {
                        message: 'Hello!',
                    });
                }}
            >Dispatch a custom action</button>
            
            <button
                onClick={() => {
                    dispatch('foo', 'doSomething', {
                        status: 'something is done',
                    });
                }}
            >Dispatch an action 'doSomething' from foo module</button>
        </div>
    );
};

Local actions inside a module can be dispatched by calling this.props.actions.actionName().

   

Testing

Testing modules is easy and straight-forward, assuming that the initial counter value is 5. You can test as follows:

import counterModule from './Counter/module';

describe('counter tests', () => {
  it('should increase count', () => {
    expect(counterModule.actions.increaseCount()).toEqual({
      count: 6,
    });
  });

  it('should decrease count', () => {
    expect(counterModule.actions.decreaseCount()).toEqual({
      count: 4,
    });
  });

  it('should reset count', () => {
    expect(counterModule.actions.resetCount()).toEqual({
      count: 0,
    });
  });
});

   

Middlewares

To use a middleware, import useMiddleware method and pass it the middleware function. You don't need to use applyMiddleware from Redux, this method will be called internally by Speedux. Here is an example using React Router (v4.2.0) and React Router Redux (v5.0.0-alpha.9):

import React from 'react';
import { render } from 'react-dom';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import { Provider, store, addReducer, useMiddleware } from 'speedux';

import Main from './Main';

const history = createHistory();

// add router reducer
addReducer('router', routerReducer);

// use the routing middleware
useMiddleware(routerMiddleware(history));

const App = (
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Main />
    </ConnectedRouter>
  </Provider>
);

// render your app
render(App, document.getElementById('root'));

   

API

createModule(name, config)

Uses a configuration object to create and return a reference to a module object that contains the initial state, action creators object and a reducer function. This module object can be used with connect function to connect a component to the Redux store.

Parameter Type Description
name String Unique identifier key for the module.
config Object The configuration object for the module.
Example:
import { createModule } from 'speedux';

export default createModule('foo', {
  state: {
    flag: false,
  },

  actions: {
    toggleFlag() {
      return {
        flag: !this.state.flag,
      };
    }
  }
});

 

connect(component, module)

Connects a component to the Redux store and injects its state and actions into the component props, each as an object. It accepts two parameters, the component to be connected and the module configuration object and returns the connected component.

The connect function will automatically map the component state and the actions defined in the module file to the component props. You will be able to access the state via this.props.state and component actions can be accessed via this.props.actions.

Parameter Type Description
component Class | Function Reference to the class/function of the component to be connected to the store.
module Object The configuration object for the module.
Example:
import React from 'react';
import { connect } from 'speedux';

import module from './module';

const MyComponent = props => (
  <div>...</div>
);

export default connect(MyComponent, module);

 

getState(query)

This method returns a Promise that resolves with the state object of a module or part of it based on a given query. If the query parameter is a string that uses dot notation, it will return the resolved value of the given key. If the query is an object, it will return an object that has the same structure but contains the resolved values. If the query parameter is not provided, the complete state object will be returned.

Note: If you want to get the state from within the same module, use this.getState() instead. Only use this method if, for any reason, you would like to read the updated state of another module.

Parameter Type Description
query String | Object A query string or a query object that represents part of the state object that needs to be fetched. This parameter is not required.
Example:
import { createModule, getState } from 'speedux';

export default createModule('foo', {
  state: {
    count: 0,
  },

  handlers: {
    *'@@baz/INCREASE_COUNT'() {
      // assuming that there are two counters, foo and baz
      // foo count should be synced with baz count, so foo
      // listens for any INCREASE_COUNT action dispatched by
      // baz and updates its count value with the current
      // count value in baz
      const bazCount = yield getState('baz.count');
      yield { count: bazCount };
    },
  },
});

 

dispatch(actionType, payload)

The dispatch function lets you dispatch any action and specify the action payload as well.

Parameter Type Description
actionType String Type of the action to be dispatched.
payload Object Action payload object.
Example:
import React from 'react';
import { dispatch } from 'speedux';

const MyComponent = (props) => {
    return (
        <div>
            <button
                onClick={() => {
                    dispatch('SOME_ACTION', {
                        message: 'Hello!',
                    });
                }}
            >Dispatch a custom action</button>
        </div>
    );
};

For dispatching actions from within modules, the dispatch function accepts parameters in a slightly different way:

dispatch(moduleName, actionName, payload)

Parameter Type Description
moduleName String Name of the module that contains the action.
actionName String Name of the action to be dispatched.
payload Object Action payload object.
import React from 'react';
import { dispatch } from 'speedux';

const MyComponent = (props) => {
    return (
        <div>
            <button
                onClick={() => {
                    dispatch('foo', 'logMessage', {
                        message: 'Hello!',
                    });
                }}
            >Dispatch a 'logMessage' action</button>
        </div>
    );
};

And foo module would look something like this:

import { createModule } from 'speedux';

export default createModule('foo', {
  state: {
    logs: [],
  },
  
  actions: {
    logMessage(msg) {
      return {
        logs: [...this.state.logs, msg],
      }
    },
  },

 

addReducer(key, reducer)

Allows registering a reducer function that can listen to any action dispatched by the store.

Parameter Type Description
key String A unique identifier key for the reducer.
reducer Function Reducer function to use.
Example:
import { routerReducer } from 'react-router-redux';
import { addReducer } from 'speedux';

addReducer('router', routerReducer);

 

useMiddleware(middleWare)

Allows using middleware functions such as React Router middleware and others. You don't need to use applyMiddleware from Redux before passing the middleware to this function.

Parameter Type Description
middleWare Function Middleware function to use.
Example:
import { routerMiddleware } from 'react-router-redux';
import { useMiddleware } from 'speedux';

useMiddleware(routerMiddleware(history)); // assuming a defined history object

   

The Configuration Object

The module configuration object may contain one or more of the following properties:

actions (Object)

A hash table representing all the actions that may need to be dispatched from the component to update the state. The key or function name will be used to generate the action type. For example, a module with a name calculator and a defined action addNumbers will dispatch an action of type @@calculator/ADD_NUMBERS whenever addNumbers() is called.

import { createModule } from 'speedux';

export default createModule('calculator', {
    state: {
        result: 0,
    },
    
    actions: {
        addNumbers(x, y) {
            return {
                result: x + y,
            };
        }
    }
});

The addNumbers action can be dispatched from the component by calling this.props.actions.addNumbers(2,4).

An action function should describe how the state is updated by returning an object. Read Updating the State section for more information.

handlers (Object)

The handlers object allows listening to any action dispatched by the store. The key represents the action type that needs to be handled and the value represents the handler function. For example, if a foo module needs to listen to an action WOO_HOO dispatched by baz:

import { createModule } from 'speedux';

export default createModule('foo', {
    handlers: {
        '@@baz/WOO_HOO'(action) { console.log('baz has dispatched woo_hoo!'); },
        'ANY_OTHER_ACTION'(action) { console.log('some other action was dispatched!'); },
    }
});

In this example, whenever the Baz component dispatches a WOO_HOO action, Foo will be able detect it and act accordingly. A handler function always receives the action object as a single parameter.

A handler function should describe how the state is updated by returning an object. Read Updating the State section for more information.

stateKey (String)

The stateKey is used as a property name when the related Redux state object is injected into the component props. The default value is 'state'.

actionsKey (String)

The actionsKey is used as a property name when the action creator functions object is injected into the component props. The default value is 'actions'.

state (Object)

The initial state object for the module. This object is used to populate the Redux state object with initial values. If not provided, an empty object will be used as the initial state.

 

The Module Object

In most cases you will not need to work directly with the module object that is returned from a createModule call. You do not need to use or know anything about the following methods, but keep reading if you are curious.

config(configObject)

Updates the current configuration of the module. The configObject parameter represents keys that should be configured and their new values. Here is an example:

import fooModule from './foo/module';

fooModule.config({
  stateKey: 'baz', // changes stateKey to 'baz'
});

setName(name)

Accepts a name for the module as a string. This method sets the name of the module and updates the action types and reducers. Here is an example:

import fooModule from './foo/module';

fooModule.setName('baz');

createAction(name, callback)

This method builds an action creator function and a sub-reducer to handle the created action. Using this method is equivalent to defining an action in the module configuration object.

Parameter Type Description
name String A string that represents the action name.
callback Function A callback function that defines how the state should be updated by returning an object. Read Updating the State section for more information.
Example:
import counterModule from './Counter/module';

counterModule.createAction('addNumbers', function(numA, numB) {
    return {
        result: numA + numB,
    };
});

If your code contains side effects, you can use a generator function instead of a normal function:

counterModule.createAction('addNumbers', function* (numA, numB) {
    yield {
        result: numA + numB,
    };
    
    ...
});

handleAction(name, callback)

Use this method to handle any action dispatched by the store and update the state accordingly. Just like createAction, it may accept a generator function as a callback to handle side effects in your code.

Parameter Type Description
name String A string that represents the action type.
callback Function A callback function that receives the action object as a single parameter and defines how the state should be updated by returning an object. Read Updating the State section for more information.
Example:
import fooModule from './Foo/module';

fooModule.handleAction('@@router/CHANGE_PATH', function({ payload }) {
    console.log(payload.newPath);
    return {
        routeChanged: true,
    };
});

getState(query)

This method returns the state object of the module or part of it based on a given query. If the query parameter is a string that uses dot notation, it will return the resolved value of the given key. If the query is an object, it will return an object that has the same structure but contains the resolved values. If the query parameter is not provided, the complete state object will be returned.

Note: If this method is used outside of an action function or a handler function, it is not guaranteed to return the updated state of the module. For such cases, use getState from Speedux instead.

Parameter Type Description
query String | Object A query string or a query object that represents part of the state object that needs to be fetched. This parameter is not required.
Example:
import { createModule } from 'speedux';

export default createModule('foo', {
  state: {
    count: 0,
    data: {
      items: [
        { title: 'Item one' },
        { title: 'Item two' },
        { title: 'Item three' },
      ],
      atts: {
        tags: [ 'js', 'react', 'redux' ],
      }
    },
  },

  actions: {
    logData() {
      // a simple query string
      console.log(this.getState('count')); // 0

      // query string that uses dot notation
      console.log(this.getState('data.items[1].title')); // Item two
      console.log(this.getState('data.atts.tags').length); // 3
      console.log(this.getState('data.atts.tags[2]')); // redux

      // query object
      const state = this.getState({
          thirdItemTitle: 'data.items[2].title',
          secondTag: 'data.atts.tags[2]',
      });
      console.log(state); // { thirdItemTitle: 'Item three', secondTag: 'react' }

      // complete state object
      console.log(this.getState());
    },
  },
});

   

License

MIT