Skip to content

Instantly share code, notes, and snippets.

@Superilya
Created February 12, 2018 16:34
Show Gist options
  • Save Superilya/f182963b4bb6b30cb1d1a96ca205e922 to your computer and use it in GitHub Desktop.
Save Superilya/f182963b4bb6b30cb1d1a96ca205e922 to your computer and use it in GitHub Desktop.
// load-route-data.js
import { matchPath } from 'react-router-dom';
export default (dispatch, pathname, routes, query) => {
const promises = [];
routes.some(route => {
const match = matchPath(pathname, route);
if (match && match.isExact && route.loadData) {
promises.push(route.loadData({
dispatch,
match,
query
}));
}
return match && match.isExact;
});
return Promise.all(promises);
}
// page.js (container)
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Meta from 'src/components/meta';
import ArticleView from 'src/components/article';
import { fetchArticle } from 'src/actions';
export const loadData = ({ dispatch, match, query }) => new Promise(res => {
const ta = bindActionCreators({ fetchArticle }, dispatch);
const cb = article => {
if (article) {
res(prepareMeta(article));
return;
}
res();
};
ta.fetchArticle({ cb, alias: match.params.alias, hidden: query.hidden });
});
const prepareMeta = ({ header, preview, image }) => ({
title: header,
description: preview,
image: image
});
class ArticleContainer extends Component {
render() {
const {page, article} = this.props;
const content = article ? article.content : null;
const header = article ? article.header : null;
return (
<Fragment>
{article && <Meta {...prepareMeta(article)} />}
<ArticleView
isLoading={page.isLoading}
isError={page.isError}
content={content}
header={header}
/>
</Fragment>
);
}
}
const mapStateToProps = ({ articles, pages }, props) => ({
article: articles[props.match.params.alias],
page: pages.article
});
export default connect(mapStateToProps)(ArticleContainer);
// server-side-render.js
const path = require('path');
const fs = require('fs');
import React from 'react'
import { renderToNodeStream, renderToStaticMarkup } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import { routes } from 'src/routes'
import configureStore from 'src/store'
import { Provider } from 'react-redux'
import App from 'src/routes';
import { staticMeta } from 'src/components/meta';
import loadRouteData from 'src/utils/load-route-data';
// Prevent XSS
const printWindowData = data => {
const dataSource = JSON.stringify(data);
if (dataSource) {
return dataSource.replace(/([<>/\u2028\u2029])/g, '\\$1');
}
return dataSource;
};
const filePath = path.resolve(__dirname, '..', 'build', 'index.html');
const htmlData = fs.readFileSync(filePath, 'utf8');
const delimetr = '<div id="root">';
const head = htmlData.substr(0, htmlData.indexOf(delimetr) + delimetr.length);
const tail = htmlData.substr(htmlData.indexOf(delimetr) + delimetr.length, htmlData.length);
module.exports = function universalLoader(req, res) {
const cspNonce = req.headers['x-csp-nonce'];
const store = configureStore();
loadRouteData(store.dispatch, req.path, routes, req.query)
.then(([meta]) => {
const context = {};
const state = store.getState();
const currentHead = meta ? head.replace('</head>', `${renderToStaticMarkup(staticMeta(meta))}</head>`) : head;
res.header('Content-Type', 'text/html');
res.write(currentHead);
const stream = renderToNodeStream(
<Provider store={store}>
<StaticRouter
location={req.url}
routerContext={{}}
context={context}
>
<App />
</StaticRouter>
</Provider>
);
stream.pipe(res, { end: false });
stream.on('end', () => {
const currentTail = tail
.replace('initialState={}', 'initialState=' + printWindowData(state))
.replace('ssr=!1', 'ssr=1')
.replace('%NONCE%', cspNonce);
res.write(currentTail);
res.end();
});
})
.catch((errors) => {
console.log('err', errors);
});
};
// client-render.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import history from 'src/history';
import configureStore from 'src/store'
import { Router } from 'react-router-dom';
import queryString from 'query-string';
import Routes, { routes } from 'src/routes';
import loadRouteData from 'src/utils/load-route-data';
const store = configureStore(window.initialState);
const renderParams = [
(
<Provider store={store}>
<Router basename={''} history={ history }>
<Routes />
</Router>
</Provider>
),
document.getElementById('root')
];
if (window.ssr) {
ReactDOM.hydrate(...renderParams);
} else {
loadRouteData(store.dispatch, window.location.pathname, routes, queryString.parse(window.location.search))
.then(() => {
ReactDOM.render(...renderParams)
});
}
// componen-meta.js
import React, { Component } from 'react';
import { Helmet } from "react-helmet";
const renderStyles = styles => styles.map(({ path }) => <link key={path} type="text/css" rel="stylesheet" href={path} />);
const renderScripts = scripts => scripts.map(({ defer, path }) => <script defer={defer} src={path} />);
export const staticMeta = ({ title, image, description, url, styles, scripts }) => ([
title && <title key="titls">{title}</title>,
title && <meta key="og-title" property="og:title" content={title} data-react-helmet="true" />,
title && <meta key="twitter-title" property="twitter:title" content={title} data-react-helmet="true" />,
image && <meta key="og-image" property="og:image" content={image} data-react-helmet="true" />,
image && <meta key="twitter-image" property="twitter:image" content={image} data-react-helmet="true" />,
description && <meta key="og-description" property="og:description" content={description} data-react-helmet="true" />,
description && <meta key="twitter-description" property="twitter:description" content={description} data-react-helmet="true" />,
description && <meta key="description" name="description" content={description} data-react-helmet="true" />,
url && <meta key="og-url" property="og:url" content={url} />,
styles && renderStyles(styles),
scripts && renderScripts(scripts)
]);
export default class Meta extends Component {
render() {
return (
<Helmet>
{staticMeta(this.props)}
</Helmet>
)
}
}
// router.js
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Layout from 'src/components/layout';
import Article, { loadData as articleLoadData } from 'src/pages/article';
import Root, { loadData as rootLoadData } from 'src/pages/root';
import NoMatch from 'src/components/no-match';
export const routes = [
{
path: '/',
key: 'index',
exact: true,
component: Root,
loadData: (...args) => rootLoadData(...args),
},
{
path: '/articles/:alias',
key: 'article',
exact: true,
component: Article,
loadData: (...args) => articleLoadData(...args),
},
{
path: '*',
exact: true,
key: 'no found',
component: NoMatch,
}
];
const Routes = props => {
return (
<Layout>
<Switch>
{routes.map(route => (
<Route {...route} />
))}
</Switch>
</Layout>
);
};
export default Routes;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment