Merge pull request #12 from sharetribe/add-redux

Add redux
This commit is contained in:
Vesa Luusua 2017-01-20 15:16:55 +02:00 committed by GitHub
commit 24e44787c2
41 changed files with 433 additions and 119 deletions

View file

@ -8,11 +8,14 @@
"express": "^4.14.0",
"helmet": "^3.3.0",
"lodash": "^4.17.4",
"qs": "^6.3.0",
"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

@ -5,6 +5,7 @@
<!--!title-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<script><!--!preloadedStateScript--></script>
</head>
<body>
<div id="root"><!--!body--></div>

View file

@ -21,12 +21,13 @@ const helmet = require('helmet');
const compression = require('compression');
const path = require('path');
const fs = require('fs');
const auth = require('./auth');
const qs = require('qs');
const _ = require('lodash');
const React = require('react');
const { createServerRenderContext } = require('react-router');
const auth = require('./auth');
// Construct the bundle path where the server side renering function
// Construct the bundle path where the server side rendering function
// can be imported.
const buildPath = path.resolve(__dirname, '..', 'build');
const manifestPath = path.join(buildPath, 'asset-manifest.json');
@ -65,9 +66,18 @@ const template = _.template(indexHtml, {
escape: reNoMatch,
});
function render(url, context) {
const { head, body } = renderApp(url, context);
return template({ title: head.title.toString(), body });
function render(url, context, preloadedState) {
const { head, body } = renderApp(url, context, preloadedState);
// Preloaded state needs to be passed for client side too.
// For security reasons we ensure that preloaded state is considered as a string
// by replacing '<' character with its unicode equivalent.
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
const preloadedStateScript = `
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')};
`;
return template({ title: head.title.toString(), preloadedStateScript, body });
}
const env = process.env.NODE_ENV;
@ -91,7 +101,12 @@ app.use('/static', express.static(path.join(buildPath, 'static')));
app.get('*', (req, res) => {
const context = createServerRenderContext();
const html = render(req.url, context);
const filters = qs.parse(req.query);
// TODO fetch this asynchronously
const preloadedState = { search: { filters } };
const html = render(req.url, context, preloadedState);
const result = context.getResult();
if (result.redirect) {
@ -100,7 +115,7 @@ app.get('*', (req, res) => {
// Do a second render pass with the context to clue <Miss>
// components into rendering this time.
// See: https://react-router.now.sh/ServerRouter
res.status(404).send(render(req.url, context));
res.status(404).send(render(req.url, context, preloadedState));
} else {
res.send(html);
}

View file

@ -2,30 +2,41 @@ 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;
const { url, context, store } = 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.
@ -37,8 +48,11 @@ ServerApp.propTypes = { url: string.isRequired, context: any.isRequired };
* - {String} body: Rendered application body of the given route
* - {Object} head: Application head metadata from react-helmet
*/
export const renderApp = (url, serverContext) => {
const body = ReactDOMServer.renderToString(<ServerApp url={url} context={serverContext} />);
export const renderApp = (url, serverContext, preloadedState) => {
const store = configureStore(preloadedState);
const body = ReactDOMServer.renderToString(
<ServerApp url={url} context={serverContext} store={store} />,
);
const head = Helmet.rewind();
return { head, body };
};

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,23 +0,0 @@
import React, { PropTypes } from 'react';
import Helmet from 'react-helmet';
import { Topbar } from '../../containers';
const Page = props => {
const { className, title, children } = props;
return (
<div className={className}>
<Helmet title={title} />
<Topbar />
<h1>{title}</h1>
{children}
</div>
);
};
const { string, any } = PropTypes;
Page.defaultProps = { className: '', children: null };
Page.propTypes = { className: string, title: string.isRequired, children: any };
export default Page;

View file

@ -0,0 +1,40 @@
import React, { Component, PropTypes } from 'react';
import Helmet from 'react-helmet';
import { Topbar } from '../../containers';
const scrollToTop = () => {
// Todo: this might need fine tuning later
window.scrollTo(0, 0);
};
class PageLayout extends Component {
componentDidMount() {
this.historyUnlisten = this.context.history.listen(() => scrollToTop());
}
componentWillUnmount() {
this.historyUnlisten();
}
render() {
const { className, title, children } = this.props;
return (
<div className={className}>
<Helmet title={title} />
<Topbar />
<h1>{title}</h1>
{children}
</div>
);
}
}
const { any, object, string } = PropTypes;
PageLayout.contextTypes = { history: object };
PageLayout.defaultProps = { className: '', children: null };
PageLayout.propTypes = { className: string, title: string.isRequired, children: any };
export default PageLayout;

View file

@ -1,4 +1,4 @@
/* eslint-disable import/prefer-default-export */
import Page from './Page/Page';
import PageLayout from './PageLayout/PageLayout';
export { Page };
export { PageLayout };

View file

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link, Redirect } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
import { fakeAuth } from '../../Routes';
class AuthenticationPage extends Component {
@ -38,22 +38,22 @@ class AuthenticationPage extends Component {
) : null;
return (
<Page title={`Authentication page: ${this.props.tab} tab`}>
<PageLayout title={`Authentication page: ${this.props.tab} tab`}>
{redirectToReferrer ? <Redirect to={from || '/'} /> : null}
{fromLoginMsg}
<button onClick={this.login}>{currentMethod}</button>
<p>or {alternativeMethod}</p>
</Page>
</PageLayout>
);
}
}
AuthenticationPage.defaultProps = { location: {}, tab: 'signup' };
const { shape, string, oneOf } = PropTypes;
const { any, oneOf, shape } = PropTypes;
AuthenticationPage.propTypes = {
location: shape({ state: shape({ from: string }) }),
location: shape({ state: shape({ from: any }) }),
tab: oneOf([ 'login', 'signup' ]),
};

View file

@ -1,11 +1,11 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const CheckoutPage = ({ params }) => (
<Page title={`Checkout page: ${params.listingId}`}>
<PageLayout title={`Checkout page: ${params.listingId}`}>
<Link to={`/order/12345`}><button>Pay</button></Link>
</Page>
</PageLayout>
);
const { shape, string } = PropTypes;

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Contact details" />
export default () => <PageLayout title="Contact details" />

View file

@ -1,12 +1,12 @@
import React, { PropTypes } from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const ConversationPage = props => {
const { params } = props;
return (
<Page title="Conversation page">
<PageLayout title="Conversation page">
<p>Conversation id: {params.id}</p>
</Page>
</PageLayout>
);
};

View file

@ -1,9 +1,9 @@
import React, { PropTypes } from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const EditProfilePage = props => {
const { params } = props;
return <Page title={`Edit profile page with display name: ${params.displayName}`} />;
return <PageLayout title={`Edit profile page with display name: ${params.displayName}`} />;
};
const { shape, string } = PropTypes;

View file

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const toPath = (filter, id) => {
switch (filter) {
@ -16,11 +16,11 @@ const toPath = (filter, id) => {
const InboxPage = props => {
const { filter } = props;
return (
<Page title={`${filter} page`}>
<PageLayout title={`${filter} page`}>
<ul>
<li><Link to={toPath(filter, 1234)}>Single thread</Link></li>
</ul>
</Page>
</PageLayout>
);
};
@ -28,6 +28,6 @@ InboxPage.defaultProps = { filter: 'conversation' };
const { oneOf } = PropTypes;
InboxPage.propTypes = { filter: oneOf([ 'orders', 'sales', 'conversation' ]) };
InboxPage.propTypes = { filter: oneOf([ 'orders', 'sales', 'inbox' ]) };
export default InboxPage;

View file

@ -8,7 +8,7 @@ describe('InboxPage', () => {
const component = renderer.create(
(
<BrowserRouter>
<InboxPage />
<InboxPage filter="inbox" />
</BrowserRouter>
),
);

View file

@ -135,7 +135,7 @@ exports[`InboxPage matches snapshot 1`] = `
</button>
</div>
<h1>
conversation page
inbox page
</h1>
<ul>
<li>

View file

@ -1,9 +1,9 @@
import React from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => (
<Page title="Landing page">
<PageLayout title="Landing page">
<Link to="/s?location=helsinki">Find studios</Link>
</Page>
</PageLayout>
)

View file

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const ListingPage = ({ params }) => {
// Listing id should be located either in the end of slug
@ -11,10 +11,10 @@ const ListingPage = ({ params }) => {
// TODO: Fetch data from SDK if no data is passed through props
return (
<Page title={`Listing page with listing id: #${id}`}>
<PageLayout title={`Listing page with listing id: #${id}`}>
<p>Slug: {params.slug}</p>
<Link to={`/checkout/${id}`}><button>Book</button></Link>
</Page>
</PageLayout>
);
};

View file

@ -1,11 +1,11 @@
import React from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => (
<Page title="Manage listings">
<PageLayout title="Manage listings">
<ul>
<li><Link to="/l/1234">Listing 1234</Link></li>
</ul>
</Page>
</PageLayout>
)

View file

@ -1,9 +1,9 @@
import React from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => (
<Page title="Page not found">
<PageLayout title="Page not found">
<Link to="/">Index page</Link>
</Page>
</PageLayout>
)

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Notification settings" />
export default () => <PageLayout title="Notification settings" />

View file

@ -1,12 +1,12 @@
/* eslint-disable react/no-unescaped-entities */
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const OrderPage = props => {
const { params } = props;
return (
<Page title="Order page">
<PageLayout title="Order page">
<p>Order id: {params.id}</p>
<Link to={`/order/${params.id}/discussion`}>Discussion tab</Link>
<br />
@ -17,12 +17,14 @@ const OrderPage = props => {
/order/1234
</i>)
</p>
</Page>
</PageLayout>
);
};
const { shape, number } = PropTypes;
const { number, oneOfType, shape, string } = PropTypes;
OrderPage.propTypes = { params: shape({ id: number.isRequired }).isRequired };
OrderPage.propTypes = {
params: shape({ id: oneOfType([ number, string ]).isRequired }).isRequired,
};
export default OrderPage;

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Type new password" />
export default () => <PageLayout title="Type new password" />

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Request new password" />
export default () => <PageLayout title="Request new password" />

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Payment methods" />
export default () => <PageLayout title="Payment methods" />

View file

@ -1,4 +1,4 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => <Page title="Payout preferences" />
export default () => <PageLayout title="Payout preferences" />

View file

@ -1,8 +1,8 @@
import React, { PropTypes } from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const ProfilePage = ({ params }) => (
<Page title={`Profile page with display name: ${params.displayName}`} />
<PageLayout title={`Profile page with display name: ${params.displayName}`} />
);
const { shape, string } = PropTypes;

View file

@ -1,12 +1,12 @@
/* eslint-disable react/no-unescaped-entities */
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { PageLayout } from '../../components';
const SalesConversationPage = props => {
const { params } = props;
return (
<Page title="Sales conversation page">
<PageLayout title="Sales conversation page">
<p>Sale id: {params.id}</p>
<Link to={`/sale/${params.id}/discussion`}>Discussion tab</Link>
<br />
@ -17,7 +17,7 @@ const SalesConversationPage = props => {
/order/1234
</i>)
</p>
</Page>
</PageLayout>
);
};

View file

@ -0,0 +1,25 @@
/**
* 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 { unionWith, isEqual } from 'lodash';
// Actions
export const ADD_FILTER = 'app/SearchPage/ADD_FILTER';
// Reducer
export default function reducer(state = {}, action = {}) {
const { type, payload } = action;
switch (type) {
case ADD_FILTER: {
const stateFilters = state.filters || [];
return { ...state, ...{ filters: unionWith(stateFilters, [ payload ], isEqual) } };
}
default:
return state;
}
}
// Action Creators
export const addFilter = (key, value) => ({ type: ADD_FILTER, payload: { [key]: value } });

View file

@ -1,9 +1,30 @@
import React from 'react';
import { Link } from 'react-router';
import { Page } from '../../components';
import { connect } from 'react-redux';
import { PageLayout } from '../../components';
import { addFlashNotification } from '../../ducks/FlashNotification.ducks';
import { addFilter } from './SearchPage.ducks';
export default () => (
<Page title="Search page">
export const SearchPageComponent = () => (
<PageLayout title="Search page">
<Link to="/l/Nice+studio-in-Helsinki-345">Nice studio in Helsinki</Link>
</Page>
)
</PageLayout>
);
/**
* Container functions.
* Since we add this to global store state with combineReducers, this will only get partial state
* which is page specific.
*/
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)(SearchPageComponent)

View file

@ -1,14 +1,15 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import SearchPage from './SearchPage';
import { SearchPageComponent } from './SearchPage';
import reducer, { ADD_FILTER, addFilter } from './SearchPage.ducks';
describe('SearchPage', () => {
describe('SearchPageComponent', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<SearchPage />
<SearchPageComponent />
</BrowserRouter>
),
);
@ -16,3 +17,35 @@ describe('SearchPage', () => {
expect(tree).toMatchSnapshot();
});
});
describe('SearchPageDucs', () => {
describe('actions', () => {
it('should create an action to add a filter', () => {
const expectedAction = { type: ADD_FILTER, payload: { location: 'helsinki' } };
expect(addFilter('location', 'helsinki')).toEqual(expectedAction);
});
});
describe('reducer', () => {
it('should return the initial state', () => {
const initial = reducer(undefined, {});
expect(initial).toEqual({});
});
it('should handle ADD_FILTER', () => {
const addFilter1 = addFilter('location', 'helsinki');
const addFilter2 = addFilter('gears', 3);
const reduced = reducer([], addFilter1);
const reducedWithInitialContent = reducer({ filters: [ addFilter1.payload ] }, addFilter2);
expect(reduced).toEqual({ filters: [ addFilter1.payload ] });
expect(reducedWithInitialContent).toEqual({ filters: [ addFilter1.payload, addFilter2.payload ] });
});
it('should handle duplicates ADD_FILTER', () => {
const filter = { location: 'helsinki' };
const addFilter = { type: ADD_FILTER, payload: filter };
const reducedWithInitialContent = reducer({ filters: [ filter ] }, addFilter);
expect(reducedWithInitialContent).toEqual({ filters: [ filter ] });
});
});
});

View file

@ -1,4 +1,4 @@
exports[`SearchPage matches snapshot 1`] = `
exports[`SearchPageComponent matches snapshot 1`] = `
<div
className="">
<div

View file

@ -1,8 +1,8 @@
import React from 'react';
import { Page } from '../../components';
import { PageLayout } from '../../components';
export default () => (
<Page title="Security">
<PageLayout title="Security">
<p>Change password, delete account</p>
</Page>
</PageLayout>
)

View file

@ -0,0 +1,10 @@
/**
* 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
*/
/* eslint-disable import/prefer-default-export */
import SearchPage from './SearchPage/SearchPage.ducks';
export { SearchPage };

View file

@ -0,0 +1,51 @@
/**
* 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, findIndex } from 'lodash';
// Actions: system notifications
export const ADD_FLASH_NOTIFICATION = 'app/FlashNotification/ADD_NOTIFICATION';
export const REMOVE_FLASH_NOTIFICATION = 'app/FlashNotification/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 } });

View file

@ -0,0 +1,49 @@
import reducer, {
ADD_FLASH_NOTIFICATION,
REMOVE_FLASH_NOTIFICATION,
addFlashNotification,
removeFlashNotification,
} from './FlashNotification.ducks';
describe('FlashNotification', () => {
describe('actions', () => {
it('should create an action to add a filter', () => {
const content = 'Error message';
const type = 'error';
const expectedAction = {
type: ADD_FLASH_NOTIFICATION,
payload: { id: 'note_1', type, content, isRead: false },
};
const received = addFlashNotification(type, content);
expect(received).toEqual(expectedAction);
});
it('should create an action to remove a notification', () => {
const expectedAction = { type: REMOVE_FLASH_NOTIFICATION, payload: { id: 1 } };
expect(removeFlashNotification(1)).toEqual(expectedAction);
});
});
describe('reducer', () => {
it('should return the initial state', () => {
const initial = reducer(undefined, {});
expect(initial).toEqual([]);
});
it('should handle ADD_FLASH_NOTIFICATION', () => {
const addFlashNote1 = addFlashNotification('error', 'Run the tests');
const addFlashNote2 = addFlashNotification('error', 'Run the tests again');
const reduced = reducer([], addFlashNote1);
const reducedWithInitialContent = reducer([ addFlashNote1.payload ], addFlashNote2);
expect(reduced).toEqual([ addFlashNote1.payload ]);
expect(reducedWithInitialContent).toEqual([ addFlashNote1.payload, addFlashNote2.payload ]);
});
it('should handle duplicates ADD_FILTER', () => {
const addFlashNote = addFlashNotification('error', 'Run the tests');
const reducedWithInitialContent = reducer([ addFlashNote.payload ], addFlashNote);
expect(reducedWithInitialContent).toEqual([ addFlashNote.payload ]);
});
});
});

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.ducks';
export { FlashNotification }; // eslint-disable-line import/prefer-default-export

View file

@ -14,12 +14,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ClientApp, renderApp } from './app';
import configureStore from './store';
import './index.css';
// If we're in a browser already, render the client application.
if (typeof window !== 'undefined') {
ReactDOM.render(<ClientApp />, document.getElementById('root'));
const preloadedState = window.__PRELOADED_STATE__ || {}; // eslint-disable-line no-underscore-dangle
const store = configureStore(preloadedState);
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
}
// Export the function for server side rendering.

16
src/reducers.js Normal file
View file

@ -0,0 +1,16 @@
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.
* Since we combineReducers, pageReducers will get page specific key (e.g. SearchPage)
* which is page specific.
* 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.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"
@ -3600,10 +3612,6 @@ lodash.uniq@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@ -4792,7 +4800,7 @@ qs@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
qs@~6.3.0:
qs@^6.3.0, qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
@ -4869,6 +4877,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 +5028,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 +5625,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"