redux-easy-models

0.0.4 • Public • Published

redux-easy-models

Easily define your models using standard redux libraries (redux, redux-actions, redux-thunk).

(This package is still experimental)

Installation

npm install --save redux-easy-models

Rationale

In a plain vanilla redux implementation with the help of some helper libraries like redux-actions and redux-thunk you might end up with something like this:

actions/timer.js

const start = createAction("TIMER_START")
//... more action creators here.
export default { start };

reducers/timer.js

import { start } from '../actions/timer';
const startReducer = handleAction(start, {
  next(state, action) {...},
  throw(state, action) {...}
});

Later in you containers or components you'd have to dispatch actions like this:

import { start } from '../actions/timer';
//...
dispatch(start());

While this decouples the code and keeps things simple, the amount of boilerplate and disconnection between actions and reducers can feel painful at times. With redux-easy-models you could achieve the same with the following code:

models/timer.js

export default new ReduxModel({
  name: timer,
  actions: [ 'start' ],
  reducers: {
    start: (state, action) => ...
  }
});

Notice how all the code for the "timer" model is in a single file. When you want to dispatch your actions, all you have to do is:

import model = '../model/timer';
//...
model.api.start();

This will automatically dispatch standard actions with standar action types. In this case the action would look like this:

{
  type: "TIMER_START"
}

redux-easy-models supports defining actions with sync and async functions, and chaining multiple actions.

Demo

For a working demo you can go here.

Usage

import ReduxModel from 'redux-easy-models';

Define your models using a single configuration object:

const timer = {
 
  // the name of the model. we will use this name to force
  // a convention where every action will have a type with
  // the name as the prefix. Can be used to combine reducers
  // as well. ie:
  //    const reducers = combineReducers({
  //      [timerModel.name]: timerModel.reducer
  //    });
  name: 'timer',
  
  // reducer's initial state.
  initialState: { started: false, count: 0, timerId: null },
  
  // actions can either be strings or named functions.
  actions: [
    // in case of strings, action creators will be generated
    // with redux-actions. the name of the model will be used
    //  as a prefix as well, ie:
    //     let actionCreator = createAction('TIMER_INCREASE');
    'increase',
    'clear',
    // functions should be named and will automatically
    // generate two actions, one when the execution starts,
    // one in case of success or failure.
    // action types will follow an upper snake convention with
    // the name as the prefix, ie:
    //   function doSomething() {...
    // will be translated to an action type of:
    //   TIMER_DO_SOMETHING_START
    //   TIMER_DO_SOMETHING_SUCCESS
    //   TIMER_DO_SOMETHING_FAIL
    function start() {
      this.clear();
      return setInterval(this.increase, 1000);
    },
    function getTimerId() {
      return this.getMyState().timerId;
    },
    function stop() {
      let timerId = this.getTimerId();
      clearInterval(timerId);
    },
    // functions can be async and/or return a promise,
    // and the right success/fail action will be generated based
    // on whether the promise is resolved/rejected.
    // in case the function is not async and is sync,
    // start/success/fail actions will also be generated, and
    // the execution will be wrapped in a try/catch statement
    async function delayStart() {
      await delay();
      this.start();
    }
  ],
  
  // reducers can be defined for actions generated by this model.
  // no need to use prefix, and sufix in the case of 'Success' is
  // options, so in the following list 'start' and 'startSuccess'
  // are pretty much the same.
  reducers: {
    start: (state, action) => {
      return Object.assign({}, state, { started: true, timerId: action.payload });
    },
    stopSuccess: (state/*, action*/) => {
      return Object.assign({}, state, { started: false, timerId: null });
    },
    // remember that actions generated with strings in the 'actions'
    // definition will not generate start/success/fail actions, so
    // in this case, 'increaseSuccess' and increaseFail will not work and
    // will never be called.
    increase: (state/*, action*/) => {
      return Object.assign({}, state, { count: state.count + 1});
    },
    clear: (state /*, action*/) => {
      return Object.assign({}, state, { count: 0 });
    }
  }
};
 
// helper function to demonstrate async
function delay() {
  return new Promise(resolve=>setTimeout(resolve, 2000));
}
 
// export your model
export default new ReduxModel(timer);

Once you have your model, you plug it in your redux store (redux-thunk is required).

import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
//models
import timerModel from './models/timer';
import { combineReducers } from 'redux';
const reducers = combineReducers({
  [timerModel.name]: timerModel.reducer
});
 
const logger = createLogger();
const store = createStore(
    reducers,
    applyMiddleware(...[thunkMiddleware, logger])
);
 
// make sure you init your model with the store!!
timerModel.init(store);

NOTICE: how the model is initialized right after the redux store is created.

Now you can consume your model from your containers or components.

import { connect } from 'react-redux'
import timerModel from '../../store/models/timer';
import HomeComponent from './HomeComponent';
 
const HomeContainer = connect(
  (state) => {
    return {
      started: state.timer.started,
      count: state.timer.count
    };
  },
  () => {
    return {
      onStart: () => timerModel.api.start(),
      onStop: () => timerModel.api.stop(),
      onClear: () => timerModel.api.clear(),
      onDelayStart: () => timerModel.api.delayStart()
    }
  }
)(HomeComponent);
 
export default HomeContainer;

Actions wills be generated as usual:

Helpers

There are two helper methods in case you have more than one model in your application.

combineModelReducers & initModels

You can arrange your models in an array or an object:

import user from './user';
import entries from './entries';
import comments from './comments';
 
export {
  user,
  entries,
  comments,
};
 
// could be export [ user, entries, comments]
import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
 
// import the new helper functions
import { combineModelReducers, initModels } from 'redux-easy-models'
 
//models
import * as models from './models';
 
// combine all of the ReduxModel reducers
const reducers = combineModelReducers(models);
 
const logger = createLogger();
const store = createStore(
    reducers,
    applyMiddleware(...[thunkMiddleware, logger])
);
 
// init all the ReduxModels
initModels(models, store);

Credits

Thanks to Dan for his contributions of helper methods!

Package Sidebar

Install

npm i redux-easy-models

Weekly Downloads

0

Version

0.0.4

License

MIT

Last publish

Collaborators

  • machadogj