Easily define your models using standard redux libraries (redux, redux-actions, redux-thunk).
(This package is still experimental)
npm install --save redux-easy-models
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.
For a working demo you can go here.
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:
There are two helper methods in case you have more than one model in your application.
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);
Thanks to Dan for his contributions of helper methods!