Last active
January 31, 2016 12:20
-
-
Save Dattaya/d4d09eb3449c3f0e146e to your computer and use it in GitHub Desktop.
Universal match (draft)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
const renderApp = (location, initialStatus) => { | |
return universalMatch({routes, location, store, history, deferred: true, initialStatus}) | |
.then(({component, redirectLocation}) => { | |
if (redirectLocation) { | |
history.replace(redirectLocation) | |
} else { | |
render(component, document.getElementById('react-view')); | |
} | |
}) | |
.catch(console.error.bind(console)); | |
}; | |
history.listenBefore((location, callback) => { | |
renderApp(location) | |
.then(callback); | |
}); | |
renderApp(pathname + search, Number(window.__INITIAL_STATUS__)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
return universalMatch({routes, location: req.url, store}) | |
.then(({component, redirectLocation, status}) => { | |
if (redirectLocation) { | |
return res.redirect(302, redirectLocation.pathname + redirectLocation.search); | |
} | |
const componentHTML = renderToString(component); | |
const initialState = store.getState(); | |
const html = ` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="shortcut icon" href="/favicon.ico"> | |
<title>Redux Demo</title> | |
<script> | |
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}; | |
window.__INITIAL_STATUS__ = ${status}; | |
</script> | |
</head> | |
<body> | |
<div id="react-view">${componentHTML}</div> | |
<script type="application/javascript" src="/dist/bundle.js"></script> | |
</body> | |
</html> | |
`; | |
res.status(status).send(html); | |
}) | |
.catch((error) => { | |
res.status(500).end('Internal server error'); | |
console.error(error); | |
}); | |
}); | |
export default app; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { | |
match, | |
RouterContext, | |
} from 'react-router'; | |
import { Provider } from 'react-redux'; | |
import ErrorHandler from 'lib/ErrorHandler'; | |
import fetchComponentData from 'lib/fetchComponentData'; | |
/** | |
* | |
* @param routes | |
* @param location | |
* @param store | |
* @param history | |
* @param deferred If `true`, fetchDataDeferred is fetched without blocking (we want this behavior on the client). | |
* @param initialStatus If present, fetchComponentData will NOT be called, instead, the page will be loaded that matches the initial status. | |
* @returns {Promise} | |
*/ | |
export default function universalMatch({routes, location, store, history, deferred = false, initialStatus}) { | |
return new Promise((resolve, reject) => { | |
match({routes, location, history}, (error, redirectLocation, renderProps) => { | |
if (error) { | |
return reject(error); | |
} | |
if (redirectLocation) { | |
return resolve({ | |
redirectLocation | |
}); | |
} | |
if (initialStatus) { | |
resolveWithComponent(initialStatus); | |
} else { | |
fetchComponentData(store, renderProps.components, renderProps.params, renderProps.location.query, deferred) | |
.then(() => resolveWithComponent(getRouteStatus(renderProps.routes))) | |
.catch((error) => { | |
if (error && error.status && generateStatusFromApi(error.status.toString())) { | |
resolveWithComponent(generateStatusFromApi(error.status.toString())); | |
} else { | |
reject(error) | |
} | |
}); | |
} | |
function resolveWithComponent(status) { | |
const component = ( | |
<Provider store={store}> | |
<ErrorHandler status={status}> | |
<RouterContext {...renderProps}/> | |
</ErrorHandler> | |
</Provider> | |
); | |
resolve({component, status}) | |
} | |
}); | |
}); | |
} | |
const statusTable = { | |
'': null, | |
'4': 404, | |
'5': 500 | |
}; | |
function generateStatusFromApi(status) { | |
return statusTable[status] ? statusTable[status] : generateStatusFromApi(status.slice(0, -1)); | |
} | |
function getRouteStatus(routes) { | |
return routes.reduce((prev, curr) => curr.status || prev, 200); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* @param store | |
* @param components | |
* @param params | |
* @param queries | |
* @param deferred Does not mean that all of the data will be deferred, only fetchDataDeferred. Do we need a better name like `holdUntilAllDataIsLoaded`? | |
* @returns {Promise} | |
*/ | |
export default function fetchComponentData(store, components, params, queries, deferred) { | |
const deferredData = () => Promise.all(getDataDeps(components, true).map(fetchDataDeferred => | |
fetchDataDeferred(store.state, store.dispatch, params, queries) | |
// for deferred data we don't want to exit Promise.all if something goes wrong in a component. | |
.catch(()=> {}) | |
)); | |
return Promise.all(getDataDeps(components).map((fetchData) => fetchData(store.state, store.dispatch, params, queries))) | |
.then(() => { | |
if (!deferred) { | |
// wait until everything is loaded | |
return deferredData(); | |
} | |
deferredData(); | |
}); | |
} | |
function getDataDeps(components, deferred = false) { | |
const methodName = deferred ? 'fetchDataDeferred' : 'fetchData'; | |
return components | |
.filter((component) => component && component[methodName]) | |
.map((component) => component[methodName]); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
/** | |
* The only purpose of `ErrorHandler` is to pass `status` into the `context`. | |
* Then `handleApiErrors` decorator can decide if it should return the original page or an error page. | |
*/ | |
export default class ErrorHandler extends React.Component { | |
static propTypes = {status: React.PropTypes.number}; | |
static defaultProps = {status: 200}; | |
static childContextTypes = { | |
status: React.PropTypes.number | |
}; | |
getChildContext() { | |
return {status: this.props.status}; | |
} | |
render() { | |
return this.props.children; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { | |
NotFound, | |
InternalServerError | |
} from 'components'; | |
/** | |
* To be used only with components connected to `Route`s. | |
* | |
* @returns {handleApiErrors} | |
*/ | |
export default function handleApiErrors() { | |
return function wrap(WrappedComponent) { | |
class HandleApiErrors extends Component { | |
static contextTypes = { | |
status: React.PropTypes.number | |
}; | |
render() { | |
switch (this.context.status) { | |
case 404: | |
return <NotFound />; | |
case 500: | |
return <InternalServerError />; | |
default: | |
return <WrappedComponent {...this.props} />; | |
} | |
} | |
} | |
return HandleApiErrors; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment