Skip to content

Define your Redux actions and reducers using object literal or ES6 class syntax

License

Notifications You must be signed in to change notification settings

vasyas/redux-action-object

Repository files navigation

redux-action-object

NPM version Dependencies Build status

Define your Redux actions and reducers using object literal or ES6 class syntax. Also, a handy method for working with side effects.

Show me the code

import * as actionObject from 'redux-action-object';

// Model
class TodoModel {
  todos = [{                    // Class properties are used as the initial state
    ...
  }];

  add(text) {                   // That's reducer
    return {
      todos: [
        ...
      ]
    }
  }
}

// Bootstrap code
const { actionCreators, reducer } = actionObject.split(new TodoModel());
const store = createStore(reducer);
const actions:TodoModel = actionObject.bind(actionCreators, store.dispatch);

// UI
@connect(state => ({ todos: state.todos }))
class TodosComponent extends React.Component {
  handleSave(text) {
    actions.add(text);          // Actions are already bound to the store
  }
}

Why?

When using conventional Redux code related to actions become spread across different places:

  • Switch in reducers
  • Action type constants
  • Action creators

So when you add a new action or modify existing one you need to make changes in at least 3 different places.

Redux-action-object helps this by enabling you to simultaneously define actions and reducers in the same code constructs, using ES6 classes.

Usage

You start with creating a class for you model. The class may contain properties and methods. Class properties are used to define the initial state. Member functions are used to define reducers and actions:

class TodoModel {
  todos = [{
    text: 'Use Redux',
    completed: false,
    id: 0
  }];

  add(text) {
    return {
      todos: [
        ...this.todos,
        {
          id: this.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: text
        }
      ]
    }
  }
}

Instance of this model class is split into Redux action creators and reducer:

import * as actionObject from 'redux-action-object';

const { actionCreators, reducer } = actionObject.split(new TodoModel());

actionCreators is an object with property keys equals to the method names in the model. Property values are Redux action creators. Action type will be equals to method name. For the code above, actionCreators will be:

actionCreators = {
    add: function(...arguments) {
        return {
            type: 'add',
            params: [].slice.call(arguments)
      }}
    }
}

Then you create a store and bind action creators to it

const store = createStore(reducer);
const actions:TodoModel = actionObject.bind(actionCreators, store.dispatch);

Now, you are ready to use it in your components:

@connect(state => ({ todos: state.todos }))
class TodosComponent extends React.Component {
  handleSave(text) {
    actions.add(text);          // Actions are already bound to the store
  }
}

Working with Side Effects

In order to completely define actions within classes, redux-action-object provides a way to define side effects within reducer code.

import * as actionObject from 'redux-action-object';

add(text) { // reducer function
  actionObject.sideEffect(function() {
      localStorage.todos = JSON.stringify(this.todos);
  });

  return {
    todos: [
      ...
    ]
  }
}

Side effect function will be called after executing reducer. Inside side effect, this will be pointing to actual store state. It means that the side effect can use reducer results, like in the above example.

To enable side effects you need to use actionObject.withSideEffects while creating Redux store:

import * as actionObject from 'redux-action-object';

const store = compose(
    actionObject.withSideEffects,
    // your other enhancers
)(createStore)(reducer);

Side effects can also be used without the classes:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      actionObject.sideEffect(function() {
          localStorage.todos = JSON.stringify(this.todos);
      });

      return {
        todos: [
          ...
        ]
      }
    default:
      return state
  }
}

More examples

Using object literals instead of classes

let model = {
    a: false,
    someAction: () => ({ a: !this.a })
};

Nesting properties and functions with literals

let model = {
  a: 1,
  b: { c: 5 },
  topLevel: val => ({ a: val, b: { c: 6 } }),
  nested: {
    inner: val => ({ a: val, b: { c: 6 } })
  }
);

Actions can be reference as expected:

import * as actionObject from 'redux-action-object';

const { actionCreators, reducer } = actionObject.split(model);
const store = createStore(reducer);
const actions = actionObject.bind(actionCreators, store.dispatch);

actions.nested.inner(3);

Define action creators in models

import * as actionObject from 'redux-action-object';

let model = {
  a: 1,
  
  findUserRequest: actionObject.creator(() =>
      fetch('/api/findUser')
  )
};

or

import * as actionObject from 'redux-action-object';

class Model {
  a = 1;
  
  @actionObject.creator
  findUserRequest() {
    return fetch('/api/findUser')
  }
}

Full example

Full example is located in todo-demo.js file. To start it, run

npm start

and the open

http://localhost:3000/

Contribution

Feel free to create an issue or send a PR

License

MIT. See LICENSE

About

Define your Redux actions and reducers using object literal or ES6 class syntax

Resources

License

Stars

Watchers

Forks

Packages

No packages published