From 5dcad519f933f3b3c9d53e7c3a3f5f0a3b818617 Mon Sep 17 00:00:00 2001 From: Zephyre Date: Thu, 21 Apr 2016 22:45:04 +0800 Subject: [PATCH] Add Redux support --- .eslintrc | 7 +++++-- package.json | 5 ++++- src/__tests__/app.js | 37 +++++++++++++++++++++++++++++++++++++ src/app.js | 24 ++++++++++++++++++++++++ src/index.js | 6 ++++++ src/redux_container.js | 24 ++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/redux_container.js diff --git a/.eslintrc b/.eslintrc index efb3491..5b13cac 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,8 @@ "parser": "babel-eslint", "plugins": [ - "babel" + "babel", + "react" ], "env": { @@ -209,6 +210,8 @@ "vars-on-top": 0, "wrap-iife": 2, "wrap-regex": 0, - "yoda": [2, "never", {"exceptRange": true}] + "yoda": [2, "never", {"exceptRange": true}], + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1 } } diff --git a/package.json b/package.json index 0da55fd..6ec197c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "mocha": "2.x.x", "chai": "3.x.x", "eslint": "1.7.x", + "eslint-plugin-react": "3.15.x", "babel-eslint": "4.x.x", "eslint-plugin-babel": "2.x.x", "babel-cli": "6.x.x", @@ -36,6 +37,8 @@ "dependencies": { "babel-runtime": "6.x.x", "react-komposer": "^1.8.0", - "react-simple-di": "^1.2.0" + "react-redux": "^4.4.5", + "react-simple-di": "^1.2.0", + "redux": "^3.5.1" } } diff --git a/src/__tests__/app.js b/src/__tests__/app.js index 4266374..cb6c4e8 100644 --- a/src/__tests__/app.js +++ b/src/__tests__/app.js @@ -56,6 +56,30 @@ describe('App', () => { }); }); + describe('has reducers field', () => { + it('should fail if reducers is not an object whose values are ' + + 'reducer functions', () => { + const app = new App({}); + const module = { + reducers() {} + }; + const errorMatch = /Module's reducers field should/; + expect(app.loadModule.bind(app, module)).to.throw(errorMatch); + }); + + it('should save reducers', () => { + const app = new App({}); + const module = { + reducers: { + foo() {} + } + }; + + app.loadModule(module); + expect(app._reducers).to.be.deep.equal(module.reducers); + }); + }); + it('should merge actions with app wide global actions', () => { const app = new App({}); app.actions = {bb: 10}; @@ -136,6 +160,19 @@ describe('App', () => { expect(calledRoutes).to.deep.equal([ 1, 2 ]); }); + it('should create the Redux store from saved reducers', () => { + const app = new App({}); + app._reducers = { + foo(state = {}) { + return state; + } + }; + + app.init(); + expect(app.context.ReduxStore.getState()).to.deep.equal({ + foo: {} + }); + }); it('should mark as initialized', () => { const app = new App({}); diff --git a/src/app.js b/src/app.js index 41f2472..29dcbed 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,7 @@ import { injectDeps } from 'react-simple-di'; +import { combineReducers, createStore } from 'redux'; export default class App { constructor(context) { @@ -12,6 +13,7 @@ export default class App { this.context = context; this.actions = {}; this._routeFns = []; + this._reducers = {}; } loadModule(module) { @@ -36,6 +38,20 @@ export default class App { this._routeFns.push(module.routes); } + // 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]; + } + } + const actions = module.actions || {}; this.actions = { ...this.actions, @@ -64,7 +80,15 @@ export default class App { routeFn(inject, this.context, this.actions); } + // Create Redux store in the context + const reducers = this._reducers; + if (Object.keys(reducers).length > 0) { + const combined = combineReducers(reducers); + this.context.ReduxStore = createStore(combined); + } + this._routeFns = []; + this._reducers = {}; this.__initialized = true; } diff --git a/src/index.js b/src/index.js index 9a1be8b..10e8bef 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,9 @@ import { } from 'react-komposer'; import App from './app'; +import { + default as _withRedux +} from './redux_container'; // export this module's functions export const createApp = (...args) => (new App(...args)); @@ -26,3 +29,6 @@ export const composeWithPromise = _composeWithPromise; export const composeWithObservable = _composeWithObservable; export const composeAll = _composeAll; export const disable = _disable; + +// export +export const withRedux = _withRedux; diff --git a/src/redux_container.js b/src/redux_container.js new file mode 100644 index 0000000..4cbc2e8 --- /dev/null +++ b/src/redux_container.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Provider, connect } from 'react-redux'; +import { useDeps } from 'react-simple-di'; +import { composeAll } from 'react-komposer'; + +const withRedux = (mapStateToProps, mapDispatchToProps, mergeProps, + options) => { + const provider = Component => { + return props => { + const {context, children, ...nextProps} = props; + const component = React.createElement(Component, nextProps, children); + return React.createElement(Provider, + {store: context().ReduxStore}, component); + }; + }; + + return composeAll( + connect(mapStateToProps, mapDispatchToProps, mergeProps, options), + provider, + useDeps() + ); +}; + +export default withRedux;