Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add redux support and implement small example #9

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 52 additions & 7 deletions client/components/App.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { PropTypes } from 'react';
import React, { PropTypes, Component } from 'react';
import { Link, IndexLink } from 'react-router';
import classnames from 'classnames/bind';
import 'normalize.css';
import {
actions,
createSelector,
connect,
isFetching as isFetchingSelector,
reasons as reasonsSelector,
} from '../redux';

// Using CSS Modules so we assign the styles to a variable
import s from './App.styl';
Expand All @@ -11,8 +18,32 @@ import logo from './react-logo.png';
// Favicon link is in the template, this just makes webpack package it up for us
import './favicon.ico';

export class Home extends React.Component {
const { array, func, node } = PropTypes;

const selectors = createSelector(
[ isFetchingSelector, reasonsSelector ],
( isFetching, reasons) => ({
isFetching, reasons })
);

export class Home extends Component {
// the following are all coming from redux
static propTypes = {
reasons: array,
generateRandomData: func,
allReasons: func,
clearReasons: func,
}
componentWillMount() {
// let us request a populatin of data via redux action
const { generateRandomData } = this.props;
if( generateRandomData ) {
generateRandomData();
}
}
render() {
// everything in next line provided by redux
const { reasons, generateRandomData, allReasons, clearReasons } = this.props;
return (
<div className={cx('page')}>
<div className={cx('siteTitle')}>
Expand All @@ -25,12 +56,23 @@ export class Home extends React.Component {
<li><span className={cx('hl')}>User</span> friendly</li>
<li><span className={cx('hl')}>SEO</span> friendly</li>
</ul>
{ reasons && <div>
<p>More reasons? <button onClick={clearReasons}>clear (we will disappear)</button></p>
<button onClick={generateRandomData}>random</button>
<button onClick={allReasons}>all</button>

<ul>
{ reasons.map((item, index) => <li key={index}>
<span className={cx('hl')}>{item}</span> supported
</li>)}
</ul>
</div>}
</div>
);
}
}

export class About extends React.Component {
export class About extends Component {
render() {
return (
<div className={cx('page')}>
Expand All @@ -43,7 +85,7 @@ export class About extends React.Component {
}
}

export class NotFound extends React.Component {
export class NotFound extends Component {
render() {
return (
<div className={cx('page')}>
Expand All @@ -58,9 +100,9 @@ export class NotFound extends React.Component {
* component as the base compoenent that's passed to ReactDOM.render, so we
* still use createClass here.
*/
export class App extends React.Component {
export class App extends Component {
static propTypes = {
children: PropTypes.any,
children: node,
}
render() {
return (
Expand All @@ -69,8 +111,11 @@ export class App extends React.Component {
<IndexLink to='/' activeClassName={cx('active')}>Home</IndexLink>
<Link to='/about' activeClassName={cx('active')}>About</Link>
</nav>
{this.props.children}
{React.cloneElement(this.props.children, { ...this.props })}
</div>
);
}
}


export default connect(selectors, actions)(App);
43 changes: 43 additions & 0 deletions client/components/AppWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { PropTypes, Component } from 'react';

import App from './App';

import {
createAppStore,
Provider,
} from '../redux';

// store workaround to export app even if no document is active
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';

let store;
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
store = createAppStore();
} else {
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
store = mockStore({});
}

const { node } = PropTypes;

export default class AppWrapper extends Component {
static propTypes = {
children: node,
}
render() {
/*
* normally provider is injected even before routes,
* however that would fail since no provider may be selected.
* We generate a mock store in case we do not have a window env
*/
return (
<Provider store={store}>
<App>
{React.cloneElement(this.props.children, { ...this.props })}
</App>
</Provider>
);
}
}
93 changes: 93 additions & 0 deletions client/redux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { applyMiddleware, compose, createStore } from 'redux';
import { createSelector } from 'reselect';
import keymirror from 'keymirror';
import thunk from 'redux-thunk';
import { Record }from 'immutable';

export const reasonsSample = [
'redux',
'redux devToolsExtension',
'react-router',
'es6',
'babel',
'webpack',
];

export const State = Record({
reasons: null,
isFetching: false,
});

export const ACTION_TYPES = keymirror({
CLEAR_REASONS_DATA: null,
FETCH_REASONS_DATA: null,
SET_REASONS_DATA: null,
});

export const actionHandlers = {
[ACTION_TYPES.CLEAR_REASONS_DATA](state) {
return state.set('reasons', null);
},
[ACTION_TYPES.SET_REASONS_DATA](state, { payload }) {
return state.set('reasons', payload);
},
[ACTION_TYPES.FETCH_REASONS_DATA](state) {
return state.set('isFetching', !state.isFetching);
},
};

export const actions = {
clearReasonsData: () => ({ type: ACTION_TYPES.CLEAR_REASONS_DATA }),
fetchReasonsData: () => ({ type: ACTION_TYPES.FETCH_REASONS_DATA }),
generateRandomData() {
return (dispatch) => {
dispatch(actions.clearReasonsData());
const payload = reasonsSample.slice(0, Math.floor(Math.random() * reasonsSample.length) + 1);
dispatch({
type: ACTION_TYPES.SET_REASONS_DATA,
payload: payload,
});
dispatch(actions.fetchReasonsData());
};
},
allReasons() {
return (dispatch) => {
dispatch(actions.clearReasonsData());
dispatch({
type: ACTION_TYPES.SET_REASONS_DATA,
payload: reasonsSample,
});
dispatch(actions.fetchReasonsData());
};
},
clearReasons() {
return (dispatch) => {
dispatch(actions.clearReasonsData());
};
},
};

export const appState = state => state;
export const reasons = createSelector([ appState ], state => state.reasons);
export const isFetching = createSelector([ appState ], state => state.isFetching);

export function reducer(state = new State(), action) {
const { type } = action;
if (type in actionHandlers) {
return actionHandlers[type](state, action);
} else {
return state;
}
}

export function createAppStore() {
const finalCreateStore = compose(
applyMiddleware(thunk),
window && window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);

return finalCreateStore(reducer);
}

export { createSelector } from 'reselect';
export { connect, Provider } from 'react-redux';
5 changes: 3 additions & 2 deletions client/routes.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';

import { App, About, Home, NotFound } from './components/App.js';
import { About, Home, NotFound } from './components/App.js';
import AppWrapper from './components/AppWrapper';

export const routes = (
<Route path='/' title='App' component={App}>
<Route path='/' title='App' component={AppWrapper}>
<IndexRoute component={Home} />
<Route path='about' title='App - About' component={About} />
<Route path='*' title='404: Not Found' component={NotFound} />
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"clean": "rimraf public",
"lint": "eslint client",
"lint:fix": "npm run lint -- --fix",
"conf": "babel-node ./scripts/generate-nginx-conf.js",
"test": "echo 'No tests specified.'",
"start:dev": "babel-node ./server.js",
Expand Down Expand Up @@ -52,14 +53,21 @@
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"history": "^2.1.1",
"immutable": "^3.8.1",
"keymirror": "^0.1.1",
"normalize.css": "^4.1.1",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"react-redux": "^4.4.5",
"react-router": "^2.4.1",
"react-static-webpack-plugin": "^0.5.0",
"react-transform-catch-errors": "^1.0.2",
"react-transform-hmr": "^1.0.4",
"redbox-react": "^1.2.5",
"redux": "^3.5.2",
"redux-mock-store": "^1.0.3",
"redux-thunk": "^2.1.0",
"reselect": "^2.5.1",
"rimraf": "^2.5.2",
"rupture": "^0.6.1",
"style-loader": "^0.13.1",
Expand Down