diff --git a/README.md b/README.md index d551d1945..f86d31a75 100755 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This is a starter boilerplate app I've put together using the following technolo * [Redux](https://github.com/rackt/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux.html) implementation * [Redux Dev Tools](https://github.com/gaearon/redux-devtools) for next generation DX (developer experience). Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs). * [Redux Router](https://github.com/rackt/redux-router) Keep your router state in your Redux store +* [react-fetcher](https://github.com/markdalgleish/react-fetcher) for route-based universal data fetching * [ESLint](http://eslint.org) to maintain a consistent code style * [redux-form](https://github.com/erikras/redux-form) to manage form state in Redux * [lru-memoize](https://github.com/erikras/lru-memoize) to speed up form validation @@ -86,7 +87,7 @@ We also spit out the `redux` state into a global `window.__data` variable in the #### Server-side Data Fetching -We ask `react-router` for a list of all the routes that match the current request and we check to see if any of the matched routes has a static `fetchData()` function. If it does, we pass the redux dispatcher to it and collect the promises returned. Those promises will be resolved when each matching route has loaded its necessary data from the API server. +We ask `react-router` for a list of all the routes that match the current request and pass them to `react-fetcher` to check to see if any of the matched routes have a `@prefetch` or `@defer` decorator function. If it does, we pass the redux dispatcher to it and `react-fetcher` collects the promises returned. Those promises will be resolved when each matching route has loaded its necessary data from the API server. #### Client Side diff --git a/package.json b/package.json index ba9c6c328..322786463 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "express-session": "^1.12.1", "file-loader": "^0.8.4", "history": "^1.13.0", - "hoist-non-react-statics": "^1.0.3", "http-proxy": "^1.12.0", "less": "^2.5.3", "less-loader": "^2.2.1", @@ -103,6 +102,7 @@ "react-bootstrap": "^0.27.3", "react-document-meta": "^2.0.0", "react-dom": "^0.14.1", + "react-fetcher": "^0.1.0", "react-inline-css": "^2.0.0", "react-redux": "^4.0.0", "react-router": "^1.0.0-rc3", diff --git a/src/containers/App/App.js b/src/containers/App/App.js index c67790bc6..b34256ef7 100755 --- a/src/containers/App/App.js +++ b/src/containers/App/App.js @@ -8,10 +8,10 @@ import { isLoaded as isInfoLoaded, load as loadInfo } from 'redux/modules/info'; import { isLoaded as isAuthLoaded, load as loadAuth, logout } from 'redux/modules/auth'; import { InfoBar } from 'components'; import { pushState } from 'redux-router'; -import connectData from 'helpers/connectData'; +import { prefetch } from 'react-fetcher'; import config from '../../config'; -function fetchData(getState, dispatch) { +@prefetch(({ getState, dispatch }) => { const promises = []; if (!isInfoLoaded(getState())) { promises.push(dispatch(loadInfo())); @@ -20,9 +20,7 @@ function fetchData(getState, dispatch) { promises.push(dispatch(loadAuth())); } return Promise.all(promises); -} - -@connectData(fetchData) +}) @connect( state => ({user: state.auth.user}), {logout, pushState}) diff --git a/src/containers/Home/Home.js b/src/containers/Home/Home.js index 44499b7dd..4c03c233d 100755 --- a/src/containers/Home/Home.js +++ b/src/containers/Home/Home.js @@ -79,6 +79,7 @@ export default class Home extends Component {
  • Redux Router Keep your router state in your Redux store
  • +
  • react-fetcher for route-based universal data fetching
  • ESLint to maintain a consistent code style
  • redux-form to manage form state in Redux @@ -108,9 +109,11 @@ export default class Home extends Component {
    Server-side data loading
    The Widgets page demonstrates how to fetch data asynchronously from - some source that is needed to complete the server-side rendering. Widgets.js's - fetchData() function is called before the widgets page is loaded, on either the server - or the client, allowing all the widget data to be loaded and ready for the page to render. + some source that is needed to complete the server-side rendering. + Widgets.js's @defer decorator is passed a function that is called + by react-fetcher + before the widgets page is loaded, on either the server or the client, allowing all the widget + data to be loaded and ready for the page to render.
    Data loading errors
    diff --git a/src/containers/Widgets/Widgets.js b/src/containers/Widgets/Widgets.js index 2e5da043c..6f02b3cb4 100755 --- a/src/containers/Widgets/Widgets.js +++ b/src/containers/Widgets/Widgets.js @@ -4,16 +4,14 @@ import {connect} from 'react-redux'; import * as widgetActions from 'redux/modules/widgets'; import {isLoaded, load as loadWidgets} from 'redux/modules/widgets'; import {initializeWithKey} from 'redux-form'; -import connectData from 'helpers/connectData'; +import {defer} from 'react-fetcher'; import { WidgetForm } from 'components'; -function fetchDataDeferred(getState, dispatch) { +@defer(({ getState, dispatch }) => { if (!isLoaded(getState())) { return dispatch(loadWidgets()); } -} - -@connectData(null, fetchDataDeferred) +}) @connect( state => ({ widgets: state.widgets.data, @@ -60,8 +58,10 @@ class Widgets extends Component {

    If you hit refresh on your browser, the data loading will take place on the server before the page is returned. If you navigated here from another page, the data was fetched from the client after the route transition. - This uses the static method fetchDataDeferred. To block a route transition until some data is loaded, use fetchData. - To always render before loading data, even on the server, use componentDidMount. + This uses the @defer decorator + from react-fetcher. To block a + route transition until some data is loaded, use @prefetch. To always render before loading data, + even on the server, use componentDidMount.

    This widgets are stored in your session, so feel free to edit it and refresh. diff --git a/src/helpers/__tests__/connectData-test.js b/src/helpers/__tests__/connectData-test.js deleted file mode 100644 index d61d73bf4..000000000 --- a/src/helpers/__tests__/connectData-test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import { div } from 'react-dom'; -import connectData from '../connectData'; - -describe('connectData', () => { - let fetchData; - let fetchDataDeferred; - let WrappedComponent; - let DataComponent; - - beforeEach(() => { - fetchData = 'fetchDataFunction'; - fetchDataDeferred = 'fetchDataDeferredFunction'; - - WrappedComponent = () => -

    ; - - DataComponent = connectData(fetchData, fetchDataDeferred)(WrappedComponent); - }); - - it('should set fetchData as a static property of the final component', () => { - expect(DataComponent.fetchData).to.equal(fetchData); - }); - - it('should set fetchDataDeferred as a static property of the final component', () => { - expect(DataComponent.fetchDataDeferred).to.equal(fetchDataDeferred); - }); -}); diff --git a/src/helpers/__tests__/getDataDependencies-test.js b/src/helpers/__tests__/getDataDependencies-test.js deleted file mode 100644 index 70ee2b2c1..000000000 --- a/src/helpers/__tests__/getDataDependencies-test.js +++ /dev/null @@ -1,61 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import { div } from 'react-dom'; -import getDataDependencies from '../getDataDependencies'; - -describe('getDataDependencies', () => { - let getState; - let dispatch; - let location; - let params; - let CompWithFetchData; - let CompWithNoData; - let CompWithFetchDataDeferred; - - beforeEach(() => { - getState = 'getState'; - dispatch = 'dispatch'; - location = 'location'; - params = 'params'; - - CompWithNoData = () => -
    ; - - CompWithFetchData = () => -
    ; - - CompWithFetchData.fetchData = (_getState, _dispatch, _location, _params) => { - return `fetchData ${_getState} ${_dispatch} ${_location} ${_params}`; - }; - CompWithFetchDataDeferred = () => -
    ; - - CompWithFetchDataDeferred.fetchDataDeferred = (_getState, _dispatch, _location, _params) => { - return `fetchDataDeferred ${_getState} ${_dispatch} ${_location} ${_params}`; - }; - }); - - it('should get fetchDatas', () => { - const deps = getDataDependencies([ - CompWithFetchData, - CompWithNoData, - CompWithFetchDataDeferred - ], getState, dispatch, location, params); - - expect(deps).to.deep.equal([ - 'fetchData getState dispatch location params' - ]); - }); - - it('should get fetchDataDeferreds', () => { - const deps = getDataDependencies([ - CompWithFetchData, - CompWithNoData, - CompWithFetchDataDeferred - ], getState, dispatch, location, params, true); - - expect(deps).to.deep.equal([ - 'fetchDataDeferred getState dispatch location params' - ]); - }); -}); diff --git a/src/helpers/connectData.js b/src/helpers/connectData.js deleted file mode 100644 index 4ad672720..000000000 --- a/src/helpers/connectData.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Component } from 'react'; -import hoistStatics from 'hoist-non-react-statics'; - -/* - Note: - When this decorator is used, it MUST be the first (outermost) decorator. - Otherwise, we cannot find and call the fetchData and fetchDataDeffered methods. -*/ - -export default function connectData(fetchData, fetchDataDeferred) { - - return function wrapWithFetchData(WrappedComponent) { - class ConnectData extends Component { - render() { - return ; - } - } - - ConnectData.fetchData = fetchData; - ConnectData.fetchDataDeferred = fetchDataDeferred; - - return hoistStatics(ConnectData, WrappedComponent); - }; -} diff --git a/src/helpers/getDataDependencies.js b/src/helpers/getDataDependencies.js deleted file mode 100755 index c7e4569af..000000000 --- a/src/helpers/getDataDependencies.js +++ /dev/null @@ -1,9 +0,0 @@ -export default (components, getState, dispatch, location, params, deferred) => { - const methodName = deferred ? 'fetchDataDeferred' : 'fetchData'; - - return components - .filter((component) => component[methodName]) // only look at ones with a static fetchData() - .map((component) => component[methodName]) // pull out fetch data methods - .map(fetchData => - fetchData(getState, dispatch, location, params)); // call fetch data methods and save promises -}; diff --git a/src/redux/middleware/transitionMiddleware.js b/src/redux/middleware/transitionMiddleware.js index 6b25b0887..acb8a48e1 100644 --- a/src/redux/middleware/transitionMiddleware.js +++ b/src/redux/middleware/transitionMiddleware.js @@ -1,5 +1,5 @@ import {ROUTER_DID_CHANGE} from 'redux-router/lib/constants'; -import getDataDependencies from '../../helpers/getDataDependencies'; +import {getPrefetchedData, getDeferredData} from 'react-fetcher'; const locationsAreEqual = (locA, locB) => (locA.pathname === locB.pathname) && (locA.search === locB.search); @@ -10,24 +10,25 @@ export default ({getState, dispatch}) => next => action => { } const {components, location, params} = action.payload; + const locals = {getState, dispatch, location, params}; const promise = new Promise((resolve) => { const doTransition = () => { next(action); - Promise.all(getDataDependencies(components, getState, dispatch, location, params, true)) + getDeferredData(components, locals) .then(resolve) .catch(error => { - // TODO: You may want to handle errors for fetchDataDeferred here - console.warn('Warning: Error in fetchDataDeferred', error); + // TODO: You may want to handle errors for @defer here + console.warn('Warning: Error in "defer" decorator function', error); return resolve(); }); }; - Promise.all(getDataDependencies(components, getState, dispatch, location, params)) + getPrefetchedData(components, locals) .then(doTransition) .catch(error => { - // TODO: You may want to handle errors for fetchData here - console.warn('Warning: Error in fetchData', error); + // TODO: You may want to handle errors for @prefetch here + console.warn('Warning: Error in "prefetch" decorator function', error); return doTransition(); }); });