Skip to content

Instantly share code, notes, and snippets.

@Dattaya
Last active January 31, 2016 12:20
Show Gist options
  • Save Dattaya/d4d09eb3449c3f0e146e to your computer and use it in GitHub Desktop.
Save Dattaya/d4d09eb3449c3f0e146e to your computer and use it in GitHub Desktop.
Universal match (draft)
// ...
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__));
// ...
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;
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);
}
/**
*
* @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]);
}
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;
}
}
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