From b937f3fd57fc17de5563f7d57aea6c580bc71e83 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 3 Feb 2017 15:51:01 +0200 Subject: [PATCH] Refactor saga code in ducks files and add sdk as parameter. --- .../fakeData.js => server/fakeSDK.js | 20 +++++- server/index.js | 11 +++- src/containers/SearchPage/SearchPage.ducks.js | 65 ++++++++++--------- src/containers/SearchPage/SearchPage.js | 7 +- src/sagas.js | 3 +- src/util/fakeSDK.js | 45 +++++++++++++ src/util/sagaHelpers.js | 14 ++++ 7 files changed, 127 insertions(+), 38 deletions(-) rename src/containers/SearchPage/fakeData.js => server/fakeSDK.js (62%) create mode 100644 src/util/fakeSDK.js create mode 100644 src/util/sagaHelpers.js diff --git a/src/containers/SearchPage/fakeData.js b/server/fakeSDK.js similarity index 62% rename from src/containers/SearchPage/fakeData.js rename to server/fakeSDK.js index a543d6f6..b06c6371 100644 --- a/src/containers/SearchPage/fakeData.js +++ b/server/fakeSDK.js @@ -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; +}; diff --git a/server/index.js b/server/index.js index 944d5920..abc391bb 100644 --- a/server/index.js +++ b/server/index.js @@ -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; diff --git a/src/containers/SearchPage/SearchPage.ducks.js b/src/containers/SearchPage/SearchPage.ducks.js index 9f4bfa8d..810f90f7 100644 --- a/src/containers/SearchPage/SearchPage.ducks.js +++ b/src/containers/SearchPage/SearchPage.ducks.js @@ -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); } } diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js index 590e58f5..7dffcfec 100644 --- a/src/containers/SearchPage/SearchPage.js +++ b/src/containers/SearchPage/SearchPage.js @@ -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()), }; }; diff --git a/src/sagas.js b/src/sagas.js index d0a86084..f1df18e3 100644 --- a/src/sagas.js +++ b/src/sagas.js @@ -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)]; } diff --git a/src/util/fakeSDK.js b/src/util/fakeSDK.js new file mode 100644 index 00000000..5affbb92 --- /dev/null +++ b/src/util/fakeSDK.js @@ -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; diff --git a/src/util/sagaHelpers.js b/src/util/sagaHelpers.js new file mode 100644 index 00000000..099ad65a --- /dev/null +++ b/src/util/sagaHelpers.js @@ -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 */