Last active
January 28, 2016 11:25
-
-
Save Dattaya/191bc290ed1bc7855e33 to your computer and use it in GitHub Desktop.
Universal router (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, preload) => { | |
return universalRouter({routes, location, store, history, deferred: true, preload}) | |
.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, true) | |
.then(callback); | |
}); | |
// can't use `false` for `preload` as this would break symmetry and `fetchComponentData`...`catch` wouldn't be called. | |
// need to find a workaround. | |
renderApp(pathname + search, true); |
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
... | |
return ( | |
<Route component={App} path="/" onEnter={loadAuth}> | |
<IndexRoute component={About}/> | |
<Route path="todos" component={Todos}/> | |
<Route path="todos/:id" component={Todo}/> | |
<Route path="/__500" component={InternalServerError} status={500}/> | |
<Route path="*" component={NotFound} status={404}/> | |
</Route> | |
); |
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 universalRouter({routes, location: req.url, store}) | |
.then(({component, matchedRoutes, redirectLocation}) => { | |
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)}; | |
</script> | |
</head> | |
<body> | |
<div id="react-view">${componentHTML}</div> | |
<script type="application/javascript" src="/dist/bundle.js"></script> | |
</body> | |
</html> | |
`; | |
res.status(getStatus(matchedRoutes)).send(html); | |
}) | |
.catch((error) => { | |
res.status(500).end('Internal server error'); | |
console.error(error); | |
}); | |
}); | |
function getStatus(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
import React from 'react'; | |
import { | |
match, | |
RouterContext, | |
Router | |
} from 'react-router'; | |
import { Provider } from 'react-redux'; | |
import fetchComponentData from './fetchComponentData'; | |
/** | |
* | |
* @param routes | |
* @param location | |
* @param store | |
* @param history | |
* @param deferred If `true`, deferred data is fetched without blocking (we want this behavior on the client). | |
* @param preload If `true`, fetchComponentData will be called. Those two arguments (deferred, preload) were set to match the server defaults | |
* @returns {Promise} | |
*/ | |
export default function universalRouter({routes, location, store, history, deferred = false, preload = true}) { | |
const rematch = (location, resolve, reject, rematched = false) => { | |
const handleError = (error) => { | |
if (__DEVELOPMENT__ || rematched) { | |
return reject(error); | |
} | |
rematch(getErrorPagePath(error.status.toString() || '500'), resolve, reject, true); | |
}; | |
match({routes, location, history}, (error, redirectLocation, renderProps) => { | |
if (error) { | |
// this error shouldn't happen in production, but let's try to handle it anyway | |
return handleError(error); | |
} | |
if (redirectLocation) { | |
return resolve({ | |
redirectLocation | |
}); | |
} | |
if (preload) { | |
fetchComponentData(store, renderProps.components, renderProps.params, renderProps.location.query, deferred) | |
.then(resolveWithComponent) | |
.catch(handleError); | |
} else { | |
resolveWithComponent(); | |
} | |
function resolveWithComponent() { | |
const component = ( | |
<Provider store={store}> | |
<RouterContext {...renderProps}/> | |
</Provider> | |
); | |
resolve({component, matchedRoutes: renderProps.routes}) | |
} | |
}); | |
}; | |
return new Promise((resolve, reject) => { | |
rematch(location, resolve, reject); | |
}); | |
} | |
// empty string '' indicates status by default | |
const statusTable = { | |
'': '/__404', | |
'5': '/__500' | |
}; | |
function getErrorPagePath(status) { | |
return statusTable[status] ? statusTable[status] : getErrorPagePath(status.slice(0, -1)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment