Refactor saga code in ducks files and add sdk as parameter.

This commit is contained in:
Vesa Luusua 2017-02-03 15:51:01 +02:00
parent 2a9c9e103b
commit b937f3fd57
7 changed files with 127 additions and 38 deletions

View file

@ -1,5 +1,5 @@
/* eslint-disable import/prefer-default-export */
export const fakeListings = [
/* eslint-disable import/prefer-default-export, no-unused-vars */
const fakeListings = [
{
id: 123,
title: 'Banyan Studios',
@ -24,3 +24,19 @@ export const fakeListings = [
},
];
/* eslint-enable import/prefer-default-export */
exports.fetchListings = () => {
const randomTime = Math.random() * 2000;
const fakeResponseTime = 1000 + randomTime;
const fakeFetch = new Promise((resolve, reject) => {
setTimeout(
() => {
resolve({ response: fakeListings });
// or reject({ error: new Error('FetchListings errored') );
},
fakeResponseTime,
);
});
return fakeFetch;
};

View file

@ -26,8 +26,9 @@ const url = require('url');
const _ = require('lodash');
const React = require('react');
const { createServerRenderContext } = require('react-router');
const auth = require('./auth');
const sagaEffects = require('redux-saga/effects');
const auth = require('./auth');
const sdk = require('./fakeSDK');
// Construct the bundle path where the server side rendering function
// can be imported.
@ -74,16 +75,21 @@ const template = _.template(indexHtml, {
function fetchInitialState(requestUrl) {
const pathname = url.parse(requestUrl).pathname;
const { matchedRoutes, params } = matchPathname(pathname);
// pathname may match with several routes (if they don't have exactly=true)
// We filter all the components form matched routes that have `loadData`
const initialFetches = _
.chain(matchedRoutes)
.filter(r => r.loadData && r.loadData.fetchData)
.map(r => sagaEffects.fork(r.loadData.fetchData))
.map(r => sagaEffects.fork(r.loadData.fetchData, sdk))
.value();
// We need to combine different onload sagas under one yield
const fetchInitialData = function* fetchInitialData() {
yield initialFetches;
};
// runSaga (if necessary) and return initial store state after loadData fetches.
if (initialFetches.length > 0) {
const fetchPromise = new Promise((resolve, reject) => {
const store = configureStore({});
@ -94,6 +100,7 @@ function fetchInitialState(requestUrl) {
.catch(e => {
reject(e);
});
// Close temporary store (which was used only for fetching initial store state)
store.close();
});
return fetchPromise;

View file

@ -4,16 +4,15 @@
* 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';
import { createRequestTypes } from '../../util/sagaHelpers';
// ================ Action types ================ //
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';
// async actions use request - success / failure pattern
export const LOAD_LISTINGS = createRequestTypes('app/SearchPage/LOAD_LISTINGS');
// ================ Reducer ================ //
@ -26,17 +25,16 @@ export const initialState = {
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 };
case LOAD_LISTINGS.REQUEST:
case LOAD_LISTINGS.SUCCESS:
case LOAD_LISTINGS.FAILURE:
return { ...state, ...payload };
default:
return state;
}
@ -45,37 +43,42 @@ export default function reducer(state = initialState, action = {}) {
// ================ 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,
});
// async actions use request - success - failure pattern
const action = (type, payload = {}) => ({ type, ...payload });
export const loadListings = {
request: () =>
action(LOAD_LISTINGS.REQUEST, {
payload: { loadingListings: true, initialListingsLoaded: false },
}),
success: listings =>
action(LOAD_LISTINGS.SUCCESS, {
payload: { loadingListings: false, initialListingsLoaded: true, listings },
}),
failure: error =>
action(LOAD_LISTINGS.FAILURE, {
payload: { loadingListings: false, loadListingError: 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);
export function* callFetchListings(sdk) {
const { response, error } = yield call(sdk.fetchListings);
if (response) {
yield put(loadListingsSuccess(response));
yield put(loadListings.success(response));
} else {
yield put(loadListingsFailure(error));
yield put(loadListings.failure(error));
}
}
// ================ Watcher sagas ================ //
export function* watchLoadListings() {
export function* watchLoadListings(sdk) {
// eslint-disable-next-line no-constant-condition
while (true) {
yield take(LOAD_LISTINGS);
yield call(callFetchListings);
yield take(LOAD_LISTINGS.REQUEST);
yield call(callFetchListings, sdk);
}
}

View file

@ -17,7 +17,10 @@ import {
export class SearchPageComponent extends Component {
componentDidMount() {
// TODO: This should be moved to Router
this.props.onLoadListings();
const { SearchPage } = this.props;
if (!SearchPage.initialListingsLoaded) {
this.props.onLoadListings();
}
}
render() {
@ -90,7 +93,7 @@ const mapDispatchToProps = function mapDispatchToProps(dispatch) {
return {
onAddNotice: msg => dispatch(addFlashNotification('notice', msg)),
onAddFilter: (k, v) => dispatch(addFilter(k, v)),
onLoadListings: () => dispatch(loadListings()),
onLoadListings: () => dispatch(loadListings.request()),
};
};

View file

@ -1,6 +1,7 @@
import { watchLogin, watchLogout } from './ducks/Auth.ducks';
import { watchLoadListings } from './containers/SearchPage/SearchPage.ducks';
import sdk from './util/fakeSDK';
export default function* rootSaga() {
yield [watchLogin(), watchLogout(), watchLoadListings()];
yield [watchLogin(), watchLogout(), watchLoadListings(sdk)];
}

45
src/util/fakeSDK.js Normal file
View file

@ -0,0 +1,45 @@
/* eslint-disable import/prefer-default-export, no-unused-vars */
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 */
const fakeSDK = {
fetchListings: () => {
const randomTime = Math.random() * 2000;
const fakeResponseTime = 1000 + randomTime;
const fakeFetch = new Promise((resolve, reject) => {
setTimeout(
() => {
resolve({ response: fakeListings });
// or reject({ error: new Error('FetchListings errored') );
},
fakeResponseTime,
);
});
return fakeFetch;
},
};
export default fakeSDK;

14
src/util/sagaHelpers.js Normal file
View file

@ -0,0 +1,14 @@
/* eslint-disable import/prefer-default-export */
// async actions use request - success / failure pattern
const REQUEST = 'REQUEST';
const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE';
// response format: { REQUEST: 'baseStr.REQUEST', SUCCESS: 'baseStr.SUCCESS', FAILURE: 'baseStr.FAILURE' }
export const createRequestTypes = base => [REQUEST, SUCCESS, FAILURE].reduce((acc, type) => {
// eslint-disable-next-line no-param-reassign
acc[type] = `${base}.${type}`;
return acc;
}, {});
/* eslint-enable import/prefer-default-export */