Skip to content

Instantly share code, notes, and snippets.

@Dattaya
Last active January 28, 2016 11:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Dattaya/191bc290ed1bc7855e33 to your computer and use it in GitHub Desktop.
Save Dattaya/191bc290ed1bc7855e33 to your computer and use it in GitHub Desktop.
Universal router (draft)
...
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);
/**
*
* @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]);
}
...
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>
);
...
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;
}
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