Skip to content

Commit f262f19

Browse files
committed
initial commit
0 parents  commit f262f19

17 files changed

+713
-0
lines changed

.babelrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"presets": [ "react", "stage-0" ],
3+
"plugins": [ "transform-es2015-destructuring", "transform-es2015-modules-commonjs", "transform-strict-mode" ]
4+
}

.eslintrc

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "airbnb",
3+
"parser": "babel-eslint",
4+
"plugins": [
5+
"react"
6+
]
7+
}

.gitignore

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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

.npmignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
example
2+
src
3+
.git
4+
.gitignore

README.md

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
<Provider store={store}>
91+
<RoutingContext {...renderProps} />
92+
</Provider>
93+
);
94+
const state = store.getState();
95+
const exposed = 'window.__APP_STATE__=' + serialize(state) + ';';
96+
const html = renderToStaticMarkup(<HTML meta={DocumentMeta.renderAsHTML()} markup={page} version="0.14.3" state={exposed} />);
97+
98+
res.setHeader('content-type', 'text/html');
99+
res.send(200, '<!DOCTYPE html>' + 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(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+
<div>
144+
<DocumentMeta {...this.props.meta.app} />
145+
<h1>Hello world: {this.props.user.result}</h1>
146+
<div>{this.props.children && React.cloneElement(this.props.children, {
147+
userId: this.props.user.result,
148+
})}</div>
149+
</div>
150+
);
151+
}
152+
}
153+
```

example/HTML.jsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
<html>
15+
<head dangerouslySetInnerHTML={{ __html: this.props.meta }} />
16+
<body>
17+
<div id="app" dangerouslySetInnerHTML={{ __html: this.props.markup }} />
18+
<script dangerouslySetInnerHTML={{ __html: this.props.state }} />
19+
<script src={'//cdnjs.cloudflare.com/ajax/libs/react/' + this.props.version + '/react-with-addons.min.js'} defer />
20+
<script src={'//cdnjs.cloudflare.com/ajax/libs/react/' + this.props.version + '/react-dom.min.js'} defer />
21+
<script src="/public/js/browser.js" defer />
22+
</body>
23+
</html>
24+
);
25+
}
26+
}
27+
28+
module.exports = HtmlComponent;

example/createStore.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
}

example/modules/reducers.jsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
});

example/modules/user/index.js

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
});

example/root.jsx

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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(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+
<div>
27+
<DocumentMeta {...this.props.meta.app} />
28+
<h1>Hello world: {this.props.user.result}</h1>
29+
<div>{this.props.children && React.cloneElement(this.props.children, {
30+
userId: this.props.user.result,
31+
})}</div>
32+
</div>
33+
);
34+
}
35+
}

0 commit comments

Comments
 (0)