Add redux. Shared actions can be found from _src/ducks/_ folder and route specific are inside container folder.

This commit is contained in:
Vesa Luusua 2017-01-18 17:55:36 +02:00
parent 826fe88dd6
commit a5e051a7ee
12 changed files with 215 additions and 23 deletions

View file

@ -11,8 +11,10 @@
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-helmet": "^4.0.0",
"react-redux": "^5.0.2",
"react-router": "4.0.0-alpha.6",
"react-test-renderer": "^15.4.2",
"redux": "^3.6.0",
"sharetribe-scripts": "0.8.6",
"source-map-support": "^0.4.10"
},

View file

@ -2,30 +2,45 @@ import React, { PropTypes } from 'react';
import ReactDOMServer from 'react-dom/server';
import Helmet from 'react-helmet';
import { BrowserRouter, ServerRouter } from 'react-router';
import { Provider } from 'react-redux';
import configureStore from './store';
import Routes from './Routes';
const RoutesWithRouterProp = ({ router }) => <Routes router={router} />;
export const ClientApp = props => {
const { store } = props;
return (
<BrowserRouter>
{
({router}) => (
<Provider store={store}>
<Routes router={router} />
</Provider>
)
}
</BrowserRouter>
);
};
const { any, string } = PropTypes;
RoutesWithRouterProp.propTypes = { router: any.isRequired };
export const ClientApp = () => (
<BrowserRouter>
{RoutesWithRouterProp}
</BrowserRouter>
);
ClientApp.propTypes = { store: any.isRequired };
export const ServerApp = props => {
const { url, context } = props;
return (
<ServerRouter location={url} context={context}>
{RoutesWithRouterProp}
{
({router}) => (
<Provider store={store}>
<Routes router={router} />
</Provider>
)
}
</ServerRouter>
);
};
ServerApp.propTypes = { url: string.isRequired, context: any.isRequired };
ServerApp.propTypes = { url: string.isRequired, context: any.isRequired, store: any.isRequired };
/**
* Render the given route.

View file

@ -4,14 +4,17 @@ import ReactDOMServer from 'react-dom/server';
import { forEach } from 'lodash';
import { createServerRenderContext } from 'react-router';
import { ClientApp, ServerApp } from './app';
import configureStore from './store';
const store = configureStore({});
const render = (url, context) =>
ReactDOMServer.renderToString(<ServerApp url={url} context={context} />);
ReactDOMServer.renderToString(<ServerApp url={url} context={context} store={store} />);
describe('Application', () => {
it('renders in the client without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<ClientApp />, div);
ReactDOM.render(<ClientApp store={store} />, div);
});
it('renders in the server without crashing', () => {

View file

@ -1,9 +1,28 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { Page } from '../../components';
import { addFlashNotification } from '../../ducks/FlashNotification';
import { addFilter } from './SearchPageDucks';
export default () => (
<Page title="Search page">
export const SearchPage = props => (
<PageLayout title="Search page">
<Link to="/l/Nice+studio-in-Helsinki-345">Nice studio in Helsinki</Link>
</Page>
)
</PageLayout>
);
/**
* Container functions.
*/
const mapStateToProps = function mapStateToProps(state) {
return state;
};
const mapDispatchToProps = function mapDispatchToProps(dispatch) {
return {
addNotice: msg => dispatch(addFlashNotification('notice', msg)),
addFilter: (k, v) => dispatch(addFilter(k, v)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchPage);

View file

@ -1,16 +1,14 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import SearchPage from './SearchPage';
import { SearchPage } from './SearchPage';
describe('SearchPage', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<SearchPage />
</BrowserRouter>
),
<BrowserRouter>
<SearchPage />
</BrowserRouter>,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();

View file

@ -0,0 +1,26 @@
/**
* This file contains Action constants, Action creators, and reducer of a page
* container. We are following Ducks module proposition:
* https://github.com/erikras/ducks-modular-redux
*/
import unionBy from 'lodash/unionBy';
// Actions
const ADD_FILTER = 'ADD_FILTER';
// Reducer
export default function reducer(state = {}, action = {}) {
const { type, payload } = action;
switch (type) {
case ADD_FILTER:
const stateFilters = state.filters || [];
return Object.assign({}, state, { filters: unionBy(stateFilters, [ payload ]) });
default:
return state;
}
}
// Action Creators
export const addFilter = (key, value) => {
return { type: ADD_FILTER, payload: { [key]: value } };
};

View file

@ -0,0 +1,9 @@
/**
* Export reducers from ducks modules of different containers (i.e. default export)
* We are following Ducks module proposition:
* https://github.com/erikras/ducks-modular-redux
*/
import SearchPage from './SearchPage/SearchPageDucks';
export { SearchPage };

View file

@ -0,0 +1,52 @@
/**
* This file contains Action constants, Action creators, and reducer of global
* FlashMessages. Global actions can be used in multiple pages.
* We are following Ducks module proposition:
* https://github.com/erikras/ducks-modular-redux
*/
import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
// Actions: system notifications
const ADD_FLASH_NOTIFICATION = 'FLASH::ADD_NOTIFICATION';
const REMOVE_FLASH_NOTIFICATION = 'FLASH::REMOVE_NOTIFICATION';
const initialState = [];
// Reducer
export default (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case ADD_FLASH_NOTIFICATION:
if (!find(state, n => n.content === payload.content && !n.isRead)) {
return state.concat([
{ id: payload.id, type: payload.type, content: payload.content, isRead: false },
]);
}
return state;
case REMOVE_FLASH_NOTIFICATION:
return state.map(
findIndex(state, msg => msg.id === payload.id),
msg => msg.set('isRead', true),
);
default:
return state;
}
};
// Action Creators
let nextMessageId = 1;
export const addFlashNotification = (type, content) => {
const id = nextMessageId;
nextMessageId += 1;
return {
type: ADD_FLASH_NOTIFICATION,
payload: { id: `note_${id}`, type, content, isRead: false },
};
};
export const removeFlashNotification = id => ({ type: REMOVE_FLASH_NOTIFICATION, payload: { id } });

9
src/ducks/index.js Normal file
View file

@ -0,0 +1,9 @@
/**
* Import reducers from shared ducks modules (default export)
* We are following Ducks module proposition:
* https://github.com/erikras/ducks-modular-redux
*/
import FlashNotification from './FlashNotification';
export { FlashNotification };

14
src/reducers.js Normal file
View file

@ -0,0 +1,14 @@
import { combineReducers } from 'redux';
import * as globalReducers from './ducks';
import * as pageReducers from './containers/reducers';
/**
* Function _createReducer_ combines global reducers (reducers that are used in
* multiple pages) and reducers that are handling actions happening inside one page container.
* Future: this structure could take in asyncReducers, which are changed when you navigate pages.
*/
const createReducer = function createReducer() {
return combineReducers({ ...globalReducers, ...pageReducers });
};
export default createReducer;

10
src/store.js Normal file
View file

@ -0,0 +1,10 @@
import { createStore } from 'redux';
import createReducer from './reducers';
/**
* configureStore creates a new store with initial state and possible enhancers
* (like redux-saga or redux-thunk middleware)
*/
export default function configureStore(initialState) {
return createStore(createReducer(), initialState);
}

View file

@ -2608,6 +2608,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoist-non-react-statics@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -2801,7 +2805,7 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
invariant@^2.2.0, invariant@^2.2.1:
invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
@ -3455,6 +3459,14 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.
json5 "^0.5.0"
object-assign "^4.0.1"
lodash, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash-es@^4.2.0, lodash-es@^4.2.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
lodash._arraycopy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
@ -4869,6 +4881,16 @@ react-helmet@^4.0.0:
object-assign "^4.0.1"
react-side-effect "^1.1.0"
react-redux@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.2.tgz#3d9878f5f71c6fafcd45de1fbb162ea31f389814"
dependencies:
hoist-non-react-statics "^1.0.3"
invariant "^2.0.0"
lodash "^4.2.0"
lodash-es "^4.2.0"
loose-envify "^1.1.0"
react-router@4.0.0-alpha.6:
version "4.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.0.0-alpha.6.tgz#239fcf9a6ba7997021022c9b51d72d370f7b6bf4"
@ -5010,6 +5032,15 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
loose-envify "^1.1.0"
symbol-observable "^1.0.2"
referrer-policy@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79"
@ -5598,6 +5629,10 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
symbol-observable@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
"symbol-tree@>= 3.1.0 < 4.0.0":
version "3.2.1"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.1.tgz#8549dd1d01fa9f893c18cc9ab0b106b4d9b168cb"