mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Fetch data with saga (both server and client)
This commit is contained in:
parent
3c597ea2cf
commit
2a9c9e103b
10 changed files with 189 additions and 352 deletions
|
|
@ -27,6 +27,7 @@ const _ = require('lodash');
|
|||
const React = require('react');
|
||||
const { createServerRenderContext } = require('react-router');
|
||||
const auth = require('./auth');
|
||||
const sagaEffects = require('redux-saga/effects');
|
||||
|
||||
// Construct the bundle path where the server side rendering function
|
||||
// can be imported.
|
||||
|
|
@ -37,6 +38,7 @@ const mainJsPath = path.join(buildPath, manifest['main.js']);
|
|||
const mainJs = require(mainJsPath);
|
||||
const renderApp = mainJs.default;
|
||||
const matchPathname = mainJs.matchPathname;
|
||||
const configureStore = mainJs.configureStore;
|
||||
|
||||
// The HTML build file is generated from the `public/index.html` file
|
||||
// and used as a template for server side rendering. The application
|
||||
|
|
@ -69,28 +71,46 @@ const template = _.template(indexHtml, {
|
|||
escape: reNoMatch,
|
||||
});
|
||||
|
||||
function render(requestUrl, context, preloadedState) {
|
||||
const { head, body } = renderApp(requestUrl, context, preloadedState);
|
||||
|
||||
function fetchInitialState(requestUrl) {
|
||||
const pathname = url.parse(requestUrl).pathname;
|
||||
const { matchedRoutes, params } = matchPathname(pathname);
|
||||
const component = matchedRoutes[0].component;
|
||||
const initialFetches = _
|
||||
.chain(matchedRoutes)
|
||||
.filter(r => r.loadData && r.loadData.fetchData)
|
||||
.map(r => sagaEffects.fork(r.loadData.fetchData))
|
||||
.value();
|
||||
|
||||
if (component.loadData) {
|
||||
component
|
||||
.loadData()
|
||||
.then((val) => {
|
||||
console.log('Data fetch resolved', val);
|
||||
});
|
||||
const fetchInitialData = function* fetchInitialData() {
|
||||
yield initialFetches;
|
||||
};
|
||||
|
||||
if (initialFetches.length > 0) {
|
||||
const fetchPromise = new Promise((resolve, reject) => {
|
||||
const store = configureStore({});
|
||||
store.runSaga(fetchInitialData).done
|
||||
.then(() => {
|
||||
resolve(store.getState());
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e);
|
||||
});
|
||||
store.close();
|
||||
});
|
||||
return fetchPromise;
|
||||
}
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
function render(requestUrl, context, preloadedState) {
|
||||
const { head, body } = renderApp(requestUrl, 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 serializedState = JSON.stringify(preloadedState).replace(/</g, '\\u003c');
|
||||
const preloadedStateScript = `
|
||||
<script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')};</script>
|
||||
<script>window.__PRELOADED_STATE__ = ${serializedState};</script>
|
||||
`;
|
||||
|
||||
return template({ title: head.title.toString(), preloadedStateScript, body });
|
||||
|
|
@ -120,21 +140,26 @@ app.get('*', (req, res) => {
|
|||
const filters = qs.parse(req.query);
|
||||
|
||||
// TODO fetch this asynchronously
|
||||
const preloadedState = { search: { filters } };
|
||||
fetchInitialState(req.url)
|
||||
.then(preloadedState => {
|
||||
const html = render(req.url, context, preloadedState);
|
||||
const result = context.getResult();
|
||||
|
||||
const html = render(req.url, context, preloadedState);
|
||||
const result = context.getResult();
|
||||
|
||||
if (result.redirect) {
|
||||
res.redirect(result.redirect.pathname);
|
||||
} else if (result.missed) {
|
||||
// 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, preloadedState));
|
||||
} else {
|
||||
res.send(html);
|
||||
}
|
||||
if (result.redirect) {
|
||||
res.redirect(result.redirect.pathname);
|
||||
} else if (result.missed) {
|
||||
// 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, preloadedState));
|
||||
} else {
|
||||
res.send(html);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e.message);
|
||||
res.status(500).send(e.message);
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
|
|
|||
|
|
@ -4,22 +4,78 @@
|
|||
* https://github.com/erikras/ducks-modular-redux
|
||||
*/
|
||||
import { unionWith, isEqual } from 'lodash';
|
||||
import { delay } from 'redux-saga';
|
||||
import { call, take, put } from 'redux-saga/effects';
|
||||
import { fakeListings } from './fakeData';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
// Actions
|
||||
export const ADD_FILTER = 'app/SearchPage/ADD_FILTER';
|
||||
export const LOAD_LISTINGS = 'app/SearchPage/LOAD_LISTINGS';
|
||||
export const LOAD_LISTINGS_SUCCESS = 'app/SearchPage/LOAD_LISTINGS_SUCCESS';
|
||||
export const LOAD_LISTINGS_FAILURE = 'app/SearchPage/LOAD_LISTINGS_FAILURE';
|
||||
|
||||
// Reducer
|
||||
export default function reducer(state = {}, action = {}) {
|
||||
// ================ Reducer ================ //
|
||||
|
||||
export const initialState = {
|
||||
filters: [],
|
||||
initialListingsLoaded: false,
|
||||
listings: [],
|
||||
loadingListings: false,
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case ADD_FILTER: {
|
||||
const stateFilters = state.filters || [];
|
||||
return { ...state, ...{ filters: unionWith(stateFilters, [payload], isEqual) } };
|
||||
}
|
||||
case LOAD_LISTINGS:
|
||||
return { ...state, loadingListings: true, initialListingsLoaded: false };
|
||||
case LOAD_LISTINGS_SUCCESS:
|
||||
return { ...state, loadingListings: false, initialListingsLoaded: true, listings: payload };
|
||||
case LOAD_LISTINGS_FAILURE:
|
||||
return { ...state, loadingListings: false, loadListingError: payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Action Creators
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const addFilter = (key, value) => ({ type: ADD_FILTER, payload: { [key]: value } });
|
||||
export const loadListings = () => ({ type: LOAD_LISTINGS, payload: {} });
|
||||
export const loadListingsSuccess = listings => ({ type: LOAD_LISTINGS_SUCCESS, payload: listings });
|
||||
export const loadListingsFailure = error => ({
|
||||
type: LOAD_LISTINGS_FAILURE,
|
||||
payload: error,
|
||||
error: true,
|
||||
});
|
||||
|
||||
// ================ Worker sagas ================ //
|
||||
|
||||
export function fetchListings() {
|
||||
const randomTime = Math.random() * 2000;
|
||||
const fakeResponseTime = 1000 + randomTime;
|
||||
return delay(fakeResponseTime, { response: fakeListings });
|
||||
}
|
||||
|
||||
export function* callFetchListings() {
|
||||
const { response, error } = yield call(fetchListings);
|
||||
if (response) {
|
||||
yield put(loadListingsSuccess(response));
|
||||
} else {
|
||||
yield put(loadListingsFailure(error));
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Watcher sagas ================ //
|
||||
|
||||
export function* watchLoadListings() {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
yield take(LOAD_LISTINGS);
|
||||
yield call(callFetchListings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||
import { connect } from 'react-redux';
|
||||
import { includes } from 'lodash';
|
||||
import { addFlashNotification } from '../../ducks/FlashNotification.ducks';
|
||||
import { addFilter } from './SearchPage.ducks';
|
||||
import { addFilter, callFetchListings, loadListings } from './SearchPage.ducks';
|
||||
import css from './SearchPage.css';
|
||||
import {
|
||||
FilterPanel,
|
||||
|
|
@ -14,44 +14,16 @@ import {
|
|||
SearchResultsPanel,
|
||||
} from '../../components';
|
||||
|
||||
const fakeListings = [
|
||||
{
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1234,
|
||||
title: 'Pienix Studio',
|
||||
price: '80\u20AC day',
|
||||
description: 'Pienix Studio specializes in music mixing and mastering production.',
|
||||
location: 'New York, NY \u2022 6mi away',
|
||||
review: { count: '7 reviews', rating: '4' },
|
||||
author: { name: 'Juhan', avatar: 'http://placehold.it/44x44', review: { rating: '4' } },
|
||||
},
|
||||
];
|
||||
|
||||
export class SearchPageComponent extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
/* eslint-disable no-console */
|
||||
console.log('Client loads data');
|
||||
SearchPageComponent.loadData().then((val) => {
|
||||
console.log('Client fetched data', val);
|
||||
});
|
||||
/* eslint-enable no-console */
|
||||
componentDidMount() {
|
||||
// TODO: This should be moved to Router
|
||||
this.props.onLoadListings();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tab } = this.props;
|
||||
const { tab, SearchPage } = this.props;
|
||||
const { listings } = SearchPage;
|
||||
const fakeListings = listings || [];
|
||||
const selectedTab = includes(['filters','listings', 'map'], tab)
|
||||
? tab
|
||||
: 'listings';
|
||||
|
|
@ -86,23 +58,25 @@ export class SearchPageComponent extends Component {
|
|||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchPageComponent.loadData = { fetchData: callFetchListings, fetchDataAction: loadListings };
|
||||
|
||||
SearchPageComponent.defaultProps = { SearchPage: {}, tab: 'listings' };
|
||||
|
||||
const { array, bool, func, shape, string } = PropTypes;
|
||||
|
||||
SearchPageComponent.propTypes = {
|
||||
onLoadListings: func.isRequired,
|
||||
SearchPage: shape({
|
||||
filters: array,
|
||||
initialListingsLoaded: bool,
|
||||
listings: array,
|
||||
loadingListings: bool,
|
||||
}),
|
||||
tab: string,
|
||||
};
|
||||
|
||||
SearchPageComponent.defaultProps = { tab: 'listings' };
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
SearchPageComponent.propTypes = { tab: string };
|
||||
|
||||
SearchPageComponent.loadData = () => (
|
||||
new Promise((resolve) => (
|
||||
// also node has a 'setTimeout' -> it's good for testing async call.
|
||||
setTimeout(() => {
|
||||
resolve(fakeListings);
|
||||
}, (Math.random() * 2000) + 1000)
|
||||
))
|
||||
);
|
||||
|
||||
/**
|
||||
* Container functions.
|
||||
* Since we add this to global store state with combineReducers, this will only get partial state
|
||||
|
|
@ -116,6 +90,7 @@ const mapDispatchToProps = function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
onAddNotice: msg => dispatch(addFlashNotification('notice', msg)),
|
||||
onAddFilter: (k, v) => dispatch(addFilter(k, v)),
|
||||
onLoadListings: () => dispatch(loadListings()),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { SearchPageComponent } from './SearchPage';
|
||||
import reducer, { ADD_FILTER, addFilter } from './SearchPage.ducks';
|
||||
import reducer, { ADD_FILTER, addFilter, initialState } from './SearchPage.ducks';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ describe('SearchPageComponent', () => {
|
|||
const component = renderer.create(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<SearchPageComponent />
|
||||
<SearchPageComponent onLoadListings={v => v} />
|
||||
</RoutesProvider>
|
||||
</TestProvider>,
|
||||
);
|
||||
|
|
@ -31,7 +31,7 @@ describe('SearchPageDucs', () => {
|
|||
describe('reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const initial = reducer(undefined, {});
|
||||
expect(initial).toEqual({});
|
||||
expect(initial).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should handle ADD_FILTER', () => {
|
||||
|
|
|
|||
|
|
@ -155,174 +155,6 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
▼
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/400x300" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Organic Music Production in a Sustainable, Ethical and Professional Studio.
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, NY • 40mi away
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="The Stardust Collective"
|
||||
className={undefined}
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
The Stardust Collective
|
||||
</span>
|
||||
<div
|
||||
className={undefined}>
|
||||
review:
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/400x300" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Pienix-Studio/1234"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Pienix Studio
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
80€ day
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Pienix Studio specializes in music mixing and mastering production.
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, NY • 6mi away
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
7 reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Juhan"
|
||||
className={undefined}
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
Juhan
|
||||
</span>
|
||||
<div
|
||||
className={undefined}>
|
||||
review:
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
|
|
@ -350,92 +182,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
Map
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/200x150" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/200x150" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Pienix-Studio/1234"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Pienix Studio
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
7 reviews
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
80€ day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
className={undefined} />
|
||||
<a
|
||||
className=""
|
||||
href="/s/filters"
|
||||
|
|
|
|||
26
src/containers/SearchPage/fakeData.js
Normal file
26
src/containers/SearchPage/fakeData.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
export const fakeListings = [
|
||||
{
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1234,
|
||||
title: 'Pienix Studio',
|
||||
price: '80\u20AC / day',
|
||||
description: 'Pienix Studio specializes in music mixing and mastering production.',
|
||||
location: 'New York, NY \u2022 6mi away',
|
||||
review: { count: '7 reviews', rating: '4' },
|
||||
author: { name: 'Juhan', avatar: 'http://placehold.it/44x44', review: { rating: '4' } },
|
||||
},
|
||||
];
|
||||
/* eslint-enable import/prefer-default-export */
|
||||
|
|
@ -15,7 +15,8 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { ClientApp, renderApp } from './app';
|
||||
import configureStore from './store';
|
||||
import { matchLocation } from './routesConfiguration';
|
||||
import { matchPathname } from './routesConfiguration';
|
||||
import rootSaga from './sagas';
|
||||
|
||||
import './index.css';
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ if (typeof window !== 'undefined') {
|
|||
// eslint-disable-next-line no-underscore-dangle
|
||||
const preloadedState = window.__PRELOADED_STATE__ || {};
|
||||
const store = configureStore(preloadedState);
|
||||
store.runSaga(rootSaga);
|
||||
|
||||
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
|
||||
}
|
||||
|
|
@ -31,4 +33,4 @@ if (typeof window !== 'undefined') {
|
|||
// Export the function for server side rendering.
|
||||
export default renderApp;
|
||||
|
||||
export { matchLocation };
|
||||
export { matchPathname, configureStore };
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { find } from 'lodash';
|
|||
import { Redirect } from 'react-router';
|
||||
|
||||
// This will change to `matchPath` soonish
|
||||
import matchPattern from 'react-router/matchPattern'
|
||||
import matchPattern from 'react-router/matchPattern';
|
||||
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import {
|
||||
|
|
@ -37,24 +37,28 @@ const routesConfiguration = [
|
|||
exactly: true,
|
||||
name: 'SearchPage',
|
||||
component: SearchPage,
|
||||
loadData: SearchPage.loadData,
|
||||
routes: [
|
||||
{
|
||||
pattern: '/s/filters',
|
||||
exactly: true,
|
||||
name: 'SearchFiltersPage',
|
||||
component: props => <SearchPage {...props} tab="filters" />,
|
||||
loadData: SearchPage.loadData,
|
||||
},
|
||||
{
|
||||
pattern: '/s/listings',
|
||||
exactly: true,
|
||||
name: 'SearchListingsPage',
|
||||
component: props => <SearchPage {...props} tab="listings" />,
|
||||
loadData: SearchPage.loadData,
|
||||
},
|
||||
{
|
||||
pattern: '/s/map',
|
||||
exactly: true,
|
||||
name: 'SearchMapPage',
|
||||
component: props => <SearchPage {...props} tab="map" />,
|
||||
loadData: SearchPage.loadData,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -276,9 +280,9 @@ const pathByRouteName = (nameToFind, routes, params = {}) =>
|
|||
/**
|
||||
* matchRoutesToLocation helps to figure out which routes are related to given location.
|
||||
*/
|
||||
const matchRoutesToLocation = (routes, location, matchedRoutes=[], params={}) => {
|
||||
const matchRoutesToLocation = (routes, location, matchedRoutes = [], params = {}) => {
|
||||
const parameters = { ...params };
|
||||
routes.forEach((route) => {
|
||||
routes.forEach(route => {
|
||||
const { exactly = false } = route;
|
||||
const match = !route.pattern ? true : matchPattern(route.pattern, location, exactly);
|
||||
|
||||
|
|
@ -286,7 +290,9 @@ const matchRoutesToLocation = (routes, location, matchedRoutes=[], params={}) =>
|
|||
matchedRoutes.push(route);
|
||||
|
||||
if (match.params) {
|
||||
Object.keys(match.params).forEach(key => { parameters[key] = match.params[key]; });
|
||||
Object.keys(match.params).forEach(key => {
|
||||
parameters[key] = match.params[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,12 +302,11 @@ const matchRoutesToLocation = (routes, location, matchedRoutes=[], params={}) =>
|
|||
});
|
||||
|
||||
return { matchedRoutes, params: parameters };
|
||||
}
|
||||
};
|
||||
|
||||
const matchPathnameCreator = routes => (
|
||||
(location, matchedRoutes=[], params={}) =>
|
||||
matchRoutesToLocation(routes, { pathname: location }, matchedRoutes, params)
|
||||
);
|
||||
const matchPathnameCreator = routes =>
|
||||
(location, matchedRoutes = [], params = {}) =>
|
||||
matchRoutesToLocation(routes, { pathname: location }, matchedRoutes, params);
|
||||
|
||||
/**
|
||||
* matchRoutesToLocation helps to figure out which routes are related to given location.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { watchLogin, watchLogout } from './ducks/Auth.ducks';
|
||||
import { watchLoadListings } from './containers/SearchPage/SearchPage.ducks';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield [watchLogin(), watchLogout()];
|
||||
yield [watchLogin(), watchLogout(), watchLoadListings()];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import createSagaMiddleware, { END } from 'redux-saga';
|
||||
import createReducer from './reducers';
|
||||
import rootSaga from './sagas';
|
||||
|
||||
/**
|
||||
* Create a new store with the given initial state. Adds Redux
|
||||
|
|
@ -21,9 +20,10 @@ export default function configureStore(initialState = {}) {
|
|||
: compose;
|
||||
|
||||
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
|
||||
const store = createStore(createReducer(), initialState, enhancer);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
const store = createStore(createReducer(), initialState, enhancer);
|
||||
store.runSaga = sagaMiddleware.run;
|
||||
store.close = () => store.dispatch(END);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue