mantra-core
mantra-core copied to clipboard
Provide Redux support
As a practical architecture of Meteor projects, it would be great if mantra provides a standard way of integrating Redux into mantra projects. The mantra guide does mention that we can manage applications' local states via Redux [ref]. Unfortunately, up till now, there are no "official" and easy ways of doing so in mantra.
Therefore, I have extended the mantra-core
package to include Redux support.
First of all, a module might contain the definition of an map of reducer functions:
// definition of a module
const reducers = {
foo: function(state, action) { return state; }
};
export default {
actions,
routes,
reducers,
load() {
}
};
When the App
instance "loads" the module by loadModule
, as well as dealing with routes and actions, it also collects the module's reducers and adds them to app._reducers
.
After all modules have been loaded, in init()
, the App
tries to combine the reducers, and from which a Redux store is created afterwards. The store will be added to the context, named ReduxStore
.
For Redux containers to access the store, I use Provider
and connect
from the react-redux
package to bind React, Redux and mantra together. All these behaviours are implemented in the withRedux
function.
Imagine we want to build a Redux container, which maps the app's state to props and provide them to its children presentation components. We can do it easily with just one line of code:
import { withRedux } from 'mantra-core';
// defines mapStateToProps
const ReduxContainer = withRedux(mapStateToProps)(ChildComponent);
Then we can happily use the container:
render() {
return (
<ReduxContainer {...props} />
);
}
I've forked mantra-sample-blog-app
and added a Redux counter component to it, which may serve as a demo of the integration of Redux into mantra projects:
https://github.com/haizi-zh/mantra-sample-blog-app/tree/new-mantra-core/client/modules/redux_demo
+1
Should factor into this middleware, multiple dispatches, and other areas that affect the store before it is created. Here is an alternate consideration (click the arrow to expand it):
Modified redux code
import { createApp } from 'mantra-core';
import initContext from './configs/context';
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
import thunkMiddleware from 'redux-thunk'
import reduxMulti from 'redux-multi'
import { batchedSubscribe } from 'redux-batched-subscribe'
// modules
import coreModule from './modules/core';
import commentsModule from './modules/comments';
import reduxDemo from './modules/redux_demo';
// init context
const context = initContext();
const app = ((ctx) => {
const proto = createApp(ctx);
const result = {
_reducers: {},
loadModule(module) {
this._checkForInit();
// Load Redux reducers
if (module.reducers) {
const reducers = module.reducers;
if (typeof reducers !== 'object') {
const message = `Module's reducers field should be a map of reducers.`;
throw new Error(message);
}
for (const key of Object.keys(reducers)) {
this._reducers[key] = reducers[key];
}
}
Object.getPrototypeOf(this).loadModule(module);
},
loadModules(...modules) {
modules.forEach((m) => {
this.loadModule(m);
});
},
init() {
this._checkForInit();
const middleware = [
thunkMiddleware,
reduxMulti
//loggerMiddleware
];
const createStoreWithMiddleware = compose(
applyMiddleware(...middleware),
//we use Redux devtools via the a Chrome extension from
//https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
//if it is installed, the icon will show up in chrome and our app can use it
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
// Ensure our listeners are only called once, even when one of the above
// middleware call the underlying store's `dispatch` multiple times
const finalCreateStore = batchedSubscribe(
fn => fn()
)(createStoreWithMiddleware)
const reducers = this._reducers;
if (Object.keys(reducers).length > 0) {
const combined = combineReducers(reducers);
this.context.ReduxStore = finalCreateStore(combined);
}
Object.getPrototypeOf(this).init();
}
};
Object.setPrototypeOf(result, proto);
return result;
})(context);
app.loadModules(coreModule, commentsModule, reduxDemo);
app.init();