├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── example
├── HTML.jsx
├── createStore.js
├── modules
│ ├── reducers.jsx
│ └── user
│ │ └── index.js
├── root.jsx
└── server.js
├── package.json
└── src
├── constants.js
├── fetch.js
├── index.js
├── middleware.js
└── reducer.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "react", "es2015", "stage-0" ],
3 | "plugins": [
4 | "transform-strict-mode"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "plugins": [
5 | "react"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.xml
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
19 | .grunt
20 |
21 | # node-waf configuration
22 | .lock-wscript
23 |
24 | # Compiled binary addons (http://nodejs.org/api/addons.html)
25 | build/Release
26 |
27 | # Dependency directory
28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
29 | node_modules
30 |
31 | # compiled source code
32 | lib
33 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 | src
3 | .git
4 | .gitignore
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Makeomatic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redux prefetch
2 |
3 | Allows universal server-side rendering to be performed without much hassle.
4 | Exposes `@fetch` decorator and `storeEnchancer`, which keeps track of unresolved promises.
5 | Add `.resolve` function to store
6 |
7 | ## Install
8 |
9 | `npm i redux-prefetch -S`
10 |
11 | ## Usage
12 |
13 | The most important files are listed here, but look in the example for some extra stuff.
14 |
15 | ```js
16 | // createStore.js
17 | import { createStore, applyMiddleware, compose } from 'redux';
18 | import promiseMiddleware from 'redux-promise-middleware';
19 | import reducers from '../modules/reducers';
20 | import { syncReduxAndRouter } from 'redux-simple-router';
21 | import { canUseDOM as isBrowser } from 'fbjs/lib/ExecutionEnvironment';
22 | import { reduxPrefetch } from 'redux-prefetch';
23 |
24 | export default function returnStore(history, initialState) {
25 | const middleware = [promiseMiddleware()];
26 |
27 | let finalCreateStore;
28 | if (isBrowser) {
29 | finalCreateStore = applyMiddleware(...middleware);
30 | } else {
31 | finalCreateStore = compose(reduxPrefetch, applyMiddleware(...middleware));
32 | }
33 |
34 | const store = finalCreateStore(createStore)(reducers, initialState);
35 | syncReduxAndRouter(history, store);
36 |
37 | return store;
38 | }
39 | ```
40 |
41 | ```js
42 | // server.js
43 | import React from 'react';
44 | import merge from 'lodash/object/merge';
45 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
46 | import { match, RoutingContext } from 'react-router';
47 | import routes from './routes';
48 | import createStore from './store/create';
49 | import metaState from './constants/config.js';
50 | import HTML from './components/HTML';
51 | import { Provider } from 'react-redux';
52 | import serialize from 'serialize-javascript';
53 | import DocumentMeta from 'react-document-meta';
54 |
55 | export default function middleware(config = {}) {
56 | const meta = merge({}, metaState, config);
57 |
58 | // this is middleware for Restify, but can easily be changed for express or similar
59 | return function serveRoute(req, res, next) {
60 | match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
61 | if (err) {
62 | return next(err);
63 | }
64 |
65 | if (redirectLocation) {
66 | res.setHeader('Location', redirectLocation.pathname + redirectLocation.search);
67 | res.send(302);
68 | return next(false);
69 | }
70 |
71 | if (!renderProps) {
72 | return next('route');
73 | }
74 |
75 | // this is because we don't want to initialize another history store
76 | // but apparently react-router passes (err, state) instead of (state), which
77 | // is expected by redux-simple-router
78 | const { history } = renderProps;
79 | const { listen: _listen } = history;
80 | history.listen = callback => {
81 | return _listen.call(history, (_, nextState) => {
82 | return callback(nextState.location);
83 | });
84 | };
85 | const store = createStore(history, { meta });
86 |
87 | // wait for the async state to resolve
88 | store.resolve(renderProps.components, renderProps.params).then(() => {
89 | const page = renderToString(
90 |
91 |
92 |
93 | );
94 | const state = store.getState();
95 | const exposed = 'window.__APP_STATE__=' + serialize(state) + ';';
96 | const html = renderToStaticMarkup(
);
97 |
98 | res.setHeader('content-type', 'text/html');
99 | res.send(200, '' + html);
100 | return next(false);
101 | });
102 | });
103 | };
104 | }
105 | ```
106 |
107 | ```js
108 | import React, { Component, PropTypes } from 'react';
109 | import { connect } from 'react-redux';
110 | import DocumentMeta from 'react-document-meta';
111 | import { dummy } from './modules/user' ;
112 | import { fetch } from 'redux-prefetch';
113 |
114 | function prefetch({ dispatch, getState }, params) {
115 | const timeout = parseInt(params.id || 30, 10);
116 | if (getState().user.result !== timeout) {
117 | return dispatch(dummy(timeout));
118 | }
119 | }
120 |
121 | // this is the important part
122 | // it wraps the component with 2 handlers: componentDidMount() and static fetch()
123 | // static function is performed on the server for state resolution before rendering
124 | // the data
125 | // componentDidMount() is obviously only performed on the client. Because this state
126 | // will be already resolved on load, you need to make sure that necessary checks are performed
127 | // and async actions are not repeated again
128 | @fetch("root", prefetch)
129 | @connect(state => ({ meta: state.meta, user: state.user }))
130 | export default class App extends Component {
131 | static propTypes = {
132 | children: PropTypes.element,
133 | meta: PropTypes.object.isRequired,
134 | user: PropTypes.object.isRequired,
135 | };
136 |
137 | static contextTypes = {
138 | store: PropTypes.object.isRequired,
139 | };
140 |
141 | render() {
142 | return (
143 |
144 |
145 |
Hello world: {this.props.user.result}
146 |
{this.props.children && React.cloneElement(this.props.children, {
147 | userId: this.props.user.result,
148 | })}
149 |
150 | );
151 | }
152 | }
153 | ```
154 |
--------------------------------------------------------------------------------
/example/HTML.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | class HtmlComponent extends Component {
4 | static propTypes = {
5 | markup: PropTypes.string.isRequired,
6 | state: PropTypes.string.isRequired,
7 | client: PropTypes.string.isRequired,
8 | version: PropTypes.string.isRequired,
9 | meta: PropTypes.string.isRequired,
10 | };
11 |
12 | render() {
13 | return (
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | module.exports = HtmlComponent;
29 |
--------------------------------------------------------------------------------
/example/createStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import promiseMiddleware from 'redux-promise-middleware';
3 | import reducers from '../modules/reducers';
4 | import { syncReduxAndRouter } from 'redux-simple-router';
5 | import { canUseDOM as isBrowser } from 'fbjs/lib/ExecutionEnvironment';
6 | import { reduxPrefetch } from '../redux-prefetch';
7 |
8 | export default function returnStore(history, initialState) {
9 | const middleware = [promiseMiddleware()];
10 |
11 | let finalCreateStore;
12 | if (isBrowser) {
13 | finalCreateStore = applyMiddleware(...middleware);
14 | } else {
15 | finalCreateStore = compose(reduxPrefetch, applyMiddleware(...middleware));
16 | }
17 |
18 | const store = finalCreateStore(createStore)(reducers, initialState);
19 | syncReduxAndRouter(history, store);
20 |
21 | return store;
22 | }
23 |
--------------------------------------------------------------------------------
/example/modules/reducers.jsx:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routeReducer } from 'redux-simple-router';
3 | import userReducer from './user';
4 |
5 | export default combineReducers({
6 | user: userReducer,
7 | routing: routeReducer,
8 | meta: (meta) => ({ ...meta }),
9 | });
10 |
--------------------------------------------------------------------------------
/example/modules/user/index.js:
--------------------------------------------------------------------------------
1 | import Promise from 'bluebird';
2 | import { USER_DUMMY, USER_MULTIPLY } from '../../constants/actions.js';
3 | import { handleActions, createAction } from 'redux-actions';
4 |
5 | const initialState = {
6 | loaded: false,
7 | loading: false,
8 | error: null,
9 | result: null,
10 | promises: null,
11 | multiply: 0,
12 | };
13 |
14 | // REDUCERS
15 | export default handleActions({
16 | [`${USER_DUMMY}_PENDING`]: (state) => ({
17 | ...state,
18 | loading: true,
19 | }),
20 |
21 | [`${USER_DUMMY}_PROMISE`]: (state, action) => ({
22 | ...state,
23 | promises: Promise.all(action.payload.promises),
24 | }),
25 |
26 | [`${USER_DUMMY}_FULFILLED`]: (state, action) => ({
27 | ...state,
28 | loading: false,
29 | loaded: true,
30 | promises: null,
31 | result: action.payload,
32 | }),
33 |
34 | [`${USER_MULTIPLY}_FULFILLED`]: (state, action) => ({
35 | ...state,
36 | promises: null,
37 | multiply: action.payload,
38 | }),
39 | }, initialState);
40 |
41 | // ACTIONS
42 | export const dummy = createAction(USER_DUMMY, timeout => {
43 | return {
44 | data: timeout,
45 | promise: Promise.delay(timeout).then(() => {
46 | return timeout;
47 | }),
48 | };
49 | });
50 |
51 | export const multiply = createAction(USER_MULTIPLY, ({ getState }, multiplicator) => {
52 | const userState = getState().user;
53 |
54 | function calculate(state) {
55 | return state.result * multiplicator;
56 | }
57 |
58 | if (userState.loaded) {
59 | return {
60 | promise: Promise.resolve(calculate(userState)),
61 | };
62 | }
63 |
64 | return {
65 | promise: userState.promises.then(() => {
66 | return calculate(getState().user);
67 | }),
68 | };
69 | });
70 |
--------------------------------------------------------------------------------
/example/root.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import DocumentMeta from 'react-document-meta';
4 | import { dummy } from './modules/user' ;
5 | import { fetch } from './redux-prefetch';
6 |
7 | function prefetch({ dispatch, getState }, params) {
8 | return dispatch(dummy(parseInt(params.id || 30, 10)));
9 | }
10 |
11 | @fetch('root', prefetch)
12 | @connect(state => ({ meta: state.meta, user: state.user }))
13 | export default class App extends Component {
14 | static propTypes = {
15 | children: PropTypes.element,
16 | meta: PropTypes.object.isRequired,
17 | user: PropTypes.object.isRequired,
18 | };
19 |
20 | static contextTypes = {
21 | store: PropTypes.object.isRequired,
22 | };
23 |
24 | render() {
25 | return (
26 |
27 |
28 |
Hello world: {this.props.user.result}
29 |
{this.props.children && React.cloneElement(this.props.children, {
30 | userId: this.props.user.result,
31 | })}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/example/server.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import merge from 'lodash/object/merge';
3 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
4 | import { match, RoutingContext } from 'react-router';
5 | import routes from './routes';
6 | import createStore from './store/create';
7 | import metaState from './constants/config.js';
8 | import HTML from './components/HTML';
9 | import { Provider } from 'react-redux';
10 | import serialize from 'serialize-javascript';
11 | import DocumentMeta from 'react-document-meta';
12 |
13 | export default function middleware(config = {}) {
14 | const meta = merge({}, metaState, config);
15 |
16 | // this is middleware for restify, but can easily be changed for express or similar
17 | return function serveRoute(req, res, next) {
18 | match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
19 | if (err) {
20 | return next(err);
21 | }
22 |
23 | if (redirectLocation) {
24 | res.setHeader('Location', redirectLocation.pathname + redirectLocation.search);
25 | res.send(302);
26 | return next(false);
27 | }
28 |
29 | if (!renderProps) {
30 | return next('route');
31 | }
32 |
33 | // this is because we don't want to initialize another history store
34 | // but apparently react-router passes (err, state) instead of (state), which
35 | // is expected by redux-simple-router
36 | const { history } = renderProps;
37 | const { listen: _listen } = history;
38 | history.listen = callback => {
39 | return _listen.call(history, (_, nextState) => {
40 | return callback(nextState.location);
41 | });
42 | };
43 | const store = createStore(history, { meta });
44 |
45 | // wait for the async state to resolve
46 | store.resolve(renderProps.components, renderProps.params).then(() => {
47 | const page = renderToString(
48 |
49 |
50 |
51 | );
52 | const state = store.getState();
53 | const exposed = 'window.__APP_STATE__=' + serialize(state) + ';';
54 | const html = renderToStaticMarkup(