From a5e051a7ee9acc5e01f77cc3c7ee6a9ee7450c3a Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 18 Jan 2017 17:55:36 +0200 Subject: [PATCH] Add redux. Shared actions can be found from _src/ducks/_ folder and route specific are inside container folder. --- package.json | 2 + src/app.js | 35 +++++++++---- src/app.test.js | 7 ++- src/containers/SearchPage/SearchPage.js | 27 ++++++++-- src/containers/SearchPage/SearchPage.test.js | 10 ++-- src/containers/SearchPage/SearchPageDucks.js | 26 ++++++++++ src/containers/reducers.js | 9 ++++ src/ducks/FlashNotification.js | 52 ++++++++++++++++++++ src/ducks/index.js | 9 ++++ src/reducers.js | 14 ++++++ src/store.js | 10 ++++ yarn.lock | 37 +++++++++++++- 12 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 src/containers/SearchPage/SearchPageDucks.js create mode 100644 src/containers/reducers.js create mode 100644 src/ducks/FlashNotification.js create mode 100644 src/ducks/index.js create mode 100644 src/reducers.js create mode 100644 src/store.js diff --git a/package.json b/package.json index 8334977d..bb6b7612 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/app.js b/src/app.js index 33bb6b69..7e24a06f 100644 --- a/src/app.js +++ b/src/app.js @@ -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 }) => ; +export const ClientApp = props => { + const { store } = props; + return ( + + { + ({router}) => ( + + + + ) + } + + ); +}; const { any, string } = PropTypes; -RoutesWithRouterProp.propTypes = { router: any.isRequired }; - -export const ClientApp = () => ( - - {RoutesWithRouterProp} - -); +ClientApp.propTypes = { store: any.isRequired }; export const ServerApp = props => { const { url, context } = props; return ( - {RoutesWithRouterProp} + { + ({router}) => ( + + + + ) + } ); }; -ServerApp.propTypes = { url: string.isRequired, context: any.isRequired }; +ServerApp.propTypes = { url: string.isRequired, context: any.isRequired, store: any.isRequired }; /** * Render the given route. diff --git a/src/app.test.js b/src/app.test.js index ad415995..b5707d7a 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -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(); + ReactDOMServer.renderToString(); describe('Application', () => { it('renders in the client without crashing', () => { const div = document.createElement('div'); - ReactDOM.render(, div); + ReactDOM.render(, div); }); it('renders in the server without crashing', () => { diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js index 8b0ba9dd..51d8daf6 100644 --- a/src/containers/SearchPage/SearchPage.js +++ b/src/containers/SearchPage/SearchPage.js @@ -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 () => ( - +export const SearchPage = props => ( + Nice studio in Helsinki - -) + +); + +/** + * 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); diff --git a/src/containers/SearchPage/SearchPage.test.js b/src/containers/SearchPage/SearchPage.test.js index 893ea3c5..81419d85 100644 --- a/src/containers/SearchPage/SearchPage.test.js +++ b/src/containers/SearchPage/SearchPage.test.js @@ -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( - ( - - - - ), + + + , ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); diff --git a/src/containers/SearchPage/SearchPageDucks.js b/src/containers/SearchPage/SearchPageDucks.js new file mode 100644 index 00000000..8f2a3ecd --- /dev/null +++ b/src/containers/SearchPage/SearchPageDucks.js @@ -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 } }; +}; diff --git a/src/containers/reducers.js b/src/containers/reducers.js new file mode 100644 index 00000000..577a391b --- /dev/null +++ b/src/containers/reducers.js @@ -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 }; diff --git a/src/ducks/FlashNotification.js b/src/ducks/FlashNotification.js new file mode 100644 index 00000000..c5aea686 --- /dev/null +++ b/src/ducks/FlashNotification.js @@ -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 } }); diff --git a/src/ducks/index.js b/src/ducks/index.js new file mode 100644 index 00000000..3f31556a --- /dev/null +++ b/src/ducks/index.js @@ -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 }; diff --git a/src/reducers.js b/src/reducers.js new file mode 100644 index 00000000..a54211b2 --- /dev/null +++ b/src/reducers.js @@ -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; diff --git a/src/store.js b/src/store.js new file mode 100644 index 00000000..2d0f79e3 --- /dev/null +++ b/src/store.js @@ -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); +} diff --git a/yarn.lock b/yarn.lock index 52556dc9..722e55f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"