├── .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 | 16 | 17 |
18 |