Skip to content
This repository has been archived by the owner on Jan 21, 2024. It is now read-only.

Commit

Permalink
Merge pull request #7 from mulesoft-labs/chores/use-redux-connect-for…
Browse files Browse the repository at this point in the history
…-async-operations

Implement redux-connect to allow server-side rendering
  • Loading branch information
nanovazquez committed Jun 20, 2016
2 parents 56f4f3b + df35416 commit e64f57f
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 43 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"autoprefixer": "^6.3.6",
"config": "^1.21.0",
"isomorphic-fetch": "^2.2.1",
"normalizr": "^2.1.0",
"postcss-import": "^8.1.2",
"postcss-nested": "^1.0.0",
Expand All @@ -42,6 +43,7 @@
"react-router": "^2.4.1",
"redux": "^3.5.2",
"redux-actions": "^0.10.0",
"redux-connect": "^2.4.0",
"redux-promise": "^0.5.3",
"redux-thunk": "^2.1.0",
"reselect": "^2.5.1",
Expand All @@ -62,7 +64,9 @@
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"chai": "^3.5.0",
"chai-as-promised": "^5.3.0",
"chai-enzyme": "^0.5.0",
"chai-things": "^0.2.0",
"cheerio": "^0.20.0",
"coveralls": "^2.11.9",
"css-loader": "^0.23.1",
Expand Down Expand Up @@ -91,6 +95,8 @@
"react-transform-catch-errors": "^1.0.2",
"react-transform-hmr": "^1.0.4",
"redbox-react": "^1.2.5",
"sinon": "^1.17.4",
"sinon-chai": "^2.8.0",
"style-loader": "^0.13.1",
"webpack-dev-middleware": "^1.6.1",
"webpack-dev-server": "^1.14.1",
Expand Down
3 changes: 2 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'babel-polyfill';
import React from 'react';
import { Router, Route, browserHistory } from 'react-router';
import { Provider } from 'react-redux';
import { ReduxAsyncConnect } from 'redux-connect';
import { Home } from 'containers';
import { configureStore } from './store';

Expand All @@ -13,7 +14,7 @@ const App = ({
const store = configureStore({ initialState, enhancers });
return (
<Provider store={store} key="provider">
<Router history={browserHistory}>
<Router history={browserHistory} render={(props) => <ReduxAsyncConnect {...props} />}>
<Route path="/" component={Home} />
</Router>
</Provider>
Expand Down
42 changes: 16 additions & 26 deletions src/components/Home/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,27 @@ const Home = ({
isSelected,
matchingProducts,
match,
fetchFeatures,
fetchProducts,
selectFeature,
ignoreFeature
}) => {
if (!allFeatures) {
// Initialize features and products
fetchFeatures();
fetchProducts();
}

return (
<div className={styles.home}>
<div className={styles.banner}>
<div className={styles.logo}></div>
<div className={styles.info}>
<h1 className={styles.title}>Mule match</h1>
<p className={styles.description}>Find the right app for you with this simple swiping app!</p>
</div>
}) => (
<div className={styles.home}>
<div className={styles.banner}>
<div className={styles.logo}></div>
<div className={styles.info}>
<h1 className={styles.title}>Mule match</h1>
<p className={styles.description}>Find the right app for you with this simple swiping app!</p>
</div>
<div className={styles.body}>
<div className={styles.features} display-if={allFeatures}>
<FeatureList allFeatures={allFeatures} isSelected={isSelected} />
<ControlPanel currentFeature={currentFeature} selectFeature={selectFeature} ignoreFeature={ignoreFeature} />
</div>
<Loading display-if={!allFeatures} />
</div>
<div className={styles.body}>
<div className={styles.features} display-if={allFeatures}>
<FeatureList allFeatures={allFeatures} isSelected={isSelected} />
<ControlPanel currentFeature={currentFeature} selectFeature={selectFeature} ignoreFeature={ignoreFeature} />
</div>
<Match display-if={match || (!currentFeature && matchingProducts)} match={match} potentialMatches={matchingProducts} />
<Loading display-if={!allFeatures} />
</div>
);
};
<Match display-if={match || (!currentFeature && matchingProducts)} match={match} potentialMatches={matchingProducts} />
</div>
);

Home.propTypes = propTypes;
export default Home;
13 changes: 10 additions & 3 deletions src/containers/Home/Home.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { Home } from 'components';
import async from 'containers/async';
import {
actions,
selectors
Expand All @@ -14,10 +15,16 @@ const homeState = (state) => ({
});

const homeActions = (dispatch) => ({
fetchFeatures: (payload) => dispatch(actions.fetchFeatures(payload)),
fetchProducts: (payload) => dispatch(actions.fetchProducts(payload)),
selectFeature: (payload) => dispatch(actions.selectFeature(payload)),
ignoreFeature: (payload) => dispatch(actions.ignoreFeature(payload))
});

export default connect(homeState, homeActions)(Home);
const resolve = ({ dispatch }) => {
// Initialize features and products
const featuresPromise = dispatch(actions.fetchFeatures());
const productsPromise = dispatch(actions.fetchProducts());

return Promise.all([featuresPromise, productsPromise]);
};

export default async(resolve)(connect(homeState, homeActions)(Home));
21 changes: 21 additions & 0 deletions src/containers/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { asyncConnect } from 'redux-connect';

const defaultPromise = Promise.resolve();

const async = (
resolve = defaultPromise
) => (
asyncConnect([{
promise: (options) => {
const payload = {
...options,
getState: options.store.getState,
dispatch: options.store.dispatch
};

return resolve(payload);
}
}])
);

export default async;
5 changes: 2 additions & 3 deletions src/domains/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { combineReducers } from 'redux';
import {
actions as featuresActions,
actionTypes as featuresActionTypes,
Expand Down Expand Up @@ -32,11 +31,11 @@ const selectors = {
...uiSelectors
};

const reducers = combineReducers({
const reducers = {
features: featuresReducers,
products: productsReducers,
ui: uiReducers
});
};

export {
actionTypes,
Expand Down
2 changes: 2 additions & 0 deletions src/services/featureService.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'isomorphic-fetch';

const baseUri = CONFIG.BACKEND.URI;

const featureService = {
Expand Down
2 changes: 2 additions & 0 deletions src/services/productService.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'isomorphic-fetch';

const baseUri = CONFIG.BACKEND.URI;

const productService = {
Expand Down
9 changes: 5 additions & 4 deletions src/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { applyMiddleware, createStore, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import { reducers } from 'domains';
import { applyMiddleware, createStore, compose, combineReducers } from 'redux';
import promiseMiddleware from 'redux-promise';
import { reducer as reduxAsyncReducer } from 'redux-connect';
import { reducers } from 'domains';

const defaultState = {};
const defaultEnhancers = applyMiddleware(promiseMiddleware);
Expand All @@ -10,7 +11,7 @@ const configureStore = ({
enhancers = []
}) => (
createStore(
reducers,
combineReducers({ ...reducers, reduxAsyncConnect: reduxAsyncReducer }),
initialState,
compose(defaultEnhancers, enhancers)
)
Expand Down
13 changes: 10 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'babel-polyfill';
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinonChai from 'sinon-chai';
import chaiThings from 'chai-things';
import chaiEnzyme from 'chai-enzyme';

// To create a code coverage report for all components you have to require all the sources and test
// See https://github.com/deepsweet/isparta-loader
Expand All @@ -26,5 +29,9 @@ const servicesContext = require.context('../src/services/', false, /index\.js$/)
servicesContext.keys().forEach(servicesContext);

// Setup libraries used for testing
global.should = chai.should();
chai.use(chaiAsPromised);
chai.use(sinonChai);
chai.use(chaiThings);
chai.use(chaiEnzyme());

global.should = chai.should();
2 changes: 0 additions & 2 deletions test/src/components/Home.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ describe('Home', () => {
isSelected = true;
matchingProducts = [];
match = {};
fetchFeatures = () => {};
fetchProducts = () => {};
selectFeature = () => {};
ignoreFeature = () => {};
component = shallow(
Expand Down
32 changes: 32 additions & 0 deletions test/src/services/featureService.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sinon from 'sinon';
import { featureService } from 'services';

describe('featureService', () => {
let response;
let expectedResult;
let mockResponseJson;
let mockFetch;
let result;

beforeEach(() => {
response = { json: () => {} };
expectedResult = { status: 200, data: 'result' };

mockResponseJson = sinon.stub(response, 'json').returns(Promise.resolve(expectedResult));
mockFetch = sinon.stub(window, 'fetch').returns(Promise.resolve(response));
});

afterEach(() => {
window.fetch.restore();
});

describe('getFeatures', () => {
beforeEach(async () => {
result = await featureService.getFeatures();
});

it('should call fetch to retrieve the features', () => mockFetch.should.have.been.called);
it('should convert the result to JSON', () => mockResponseJson.should.have.been.called);
it('should retrieve the result', () => result.should.be.equal(expectedResult));
});
});
32 changes: 32 additions & 0 deletions test/src/services/productService.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sinon from 'sinon';
import { productService } from 'services';

describe('productService', () => {
let response;
let expectedResult;
let mockResponseJson;
let mockFetch;
let result;

beforeEach(() => {
response = { json: () => {} };
expectedResult = { status: 200, data: 'result' };

mockResponseJson = sinon.stub(response, 'json').returns(Promise.resolve(expectedResult));
mockFetch = sinon.stub(window, 'fetch').returns(Promise.resolve(response));
});

afterEach(() => {
window.fetch.restore();
});

describe('getProducts', () => {
beforeEach(async () => {
result = await productService.getProducts();
});

it('should call fetch to retrieve the items', () => mockFetch.should.have.been.called);
it('should convert the result to JSON', () => mockResponseJson.should.have.been.called);
it('should retrieve the result', () => result.should.be.equal(expectedResult));
});
});
4 changes: 3 additions & 1 deletion webpack/configs/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ var webpack = require('webpack');
var aliases = require('../aliases');
var rootPath = path.resolve(__dirname, '../../');

aliases['test'] = 'src/test/src';
// Add new keys for tests
aliases['test'] = 'src/test/src';
aliases['sinon'] = 'sinon/pkg/sinon',

module.exports = {
devtool: 'inline-source-map',
Expand Down

0 comments on commit e64f57f

Please sign in to comment.