Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use react-router; client- and server side rendering #422

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -33,7 +33,7 @@
"pretty-error": "2.0.0",
"react": "0.14.7",
"react-dom": "0.14.7",
"react-routing": "0.0.7",
"react-router": "2.0.0-rc5",
"source-map-support": "0.4.0",
"whatwg-fetch": "0.11.0"
},
Expand Down
84 changes: 43 additions & 41 deletions src/client.js
Expand Up @@ -8,10 +8,13 @@
*/

import 'babel-polyfill';
import ReactDOM from 'react-dom';
import React from 'react';
import { match, Router } from 'react-router';
import { render } from 'react-dom';
import FastClick from 'fastclick';
import Router from './routes';
import routes from './routes';
import Location from './core/Location';
import ContextHolder from './core/ContextHolder';
import { addEventListener, removeEventListener } from './core/DOMUtils';

let cssContainer = document.getElementById('css');
Expand Down Expand Up @@ -41,59 +44,58 @@ const context = {
// rendering, as it was already sent by the Html component.
let trackPageview = () => (trackPageview = () => window.ga('send', 'pageview'));

function render(state) {
Router.dispatch(state, (newState, component) => {
ReactDOM.render(component, appContainer, () => {
// Restore the scroll position if it was saved into the state
if (state.scrollY !== undefined) {
window.scrollTo(state.scrollX, state.scrollY);
} else {
window.scrollTo(0, 0);
}

trackPageview();

// Remove the pre-rendered CSS because it's no longer used
// after the React app is launched
if (cssContainer) {
cssContainer.parentNode.removeChild(cssContainer);
cssContainer = null;
}
});
});
}

function run() {
let currentLocation = null;
let currentState = null;
const scrollOffsets = new Map();
let currentScrollOffset = null;

// Make taps on links and buttons work fast on mobiles
FastClick.attach(document.body);

// Re-render the app when window.location changes
const unlisten = Location.listen(location => {
currentLocation = location;
currentState = Object.assign({}, location.state, {
path: location.pathname,
query: location.query,
state: location.state,
context,
});
render(currentState);
const locationId = location.pathname + location.search;
if (!scrollOffsets.get(locationId)) {
scrollOffsets.set(locationId, Object.create(null));
}
currentScrollOffset = scrollOffsets.get(locationId);
// Restore the scroll position if it was saved
if (currentScrollOffset.scrollY !== undefined) {
window.scrollTo(currentScrollOffset.scrollX, currentScrollOffset.scrollY);
} else {
window.scrollTo(0, 0);
}

trackPageview();
});

const { pathname, search, hash } = window.location;
const location = `${pathname}${search}${hash}`;

match({ routes, location }, (error, redirectLocation, renderProps) => {
render(
<ContextHolder context={context}>
<Router {...renderProps} children={routes} history={Location} />
</ContextHolder>,
appContainer
);
// Remove the pre-rendered CSS because it's no longer used
// after the React app is launched
if (cssContainer) {
cssContainer.parentNode.removeChild(cssContainer);
cssContainer = null;
}
});

// Save the page scroll position into the current location's state
// Save the page scroll position
const supportPageOffset = window.pageXOffset !== undefined;
const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat');
const setPageOffset = () => {
currentLocation.state = currentLocation.state || Object.create(null);
if (supportPageOffset) {
currentLocation.state.scrollX = window.pageXOffset;
currentLocation.state.scrollY = window.pageYOffset;
currentScrollOffset.scrollX = window.pageXOffset;
currentScrollOffset.scrollY = window.pageYOffset;
} else {
currentLocation.state.scrollX = isCSS1Compat ?
currentScrollOffset.scrollX = isCSS1Compat ?
document.documentElement.scrollLeft : document.body.scrollLeft;
currentLocation.state.scrollY = isCSS1Compat ?
currentScrollOffset.scrollY = isCSS1Compat ?
document.documentElement.scrollTop : document.body.scrollTop;
}
};
Expand Down
27 changes: 3 additions & 24 deletions src/components/App/App.js
Expand Up @@ -8,7 +8,6 @@
*/

import React, { Component, PropTypes } from 'react';
import emptyFunction from 'fbjs/lib/emptyFunction';
import s from './App.scss';
import Header from '../Header';
import Feedback from '../Feedback';
Expand All @@ -17,36 +16,16 @@ import Footer from '../Footer';
class App extends Component {

static propTypes = {
context: PropTypes.shape({
insertCss: PropTypes.func,
onSetTitle: PropTypes.func,
onSetMeta: PropTypes.func,
onPageNotFound: PropTypes.func,
}),
children: PropTypes.element.isRequired,
error: PropTypes.object,
};

static childContextTypes = {
insertCss: PropTypes.func.isRequired,
onSetTitle: PropTypes.func.isRequired,
onSetMeta: PropTypes.func.isRequired,
onPageNotFound: PropTypes.func.isRequired,
static contextTypes = {
insertCss: PropTypes.func,
};

getChildContext() {
const context = this.props.context;
return {
insertCss: context.insertCss || emptyFunction,
onSetTitle: context.onSetTitle || emptyFunction,
onSetMeta: context.onSetMeta || emptyFunction,
onPageNotFound: context.onPageNotFound || emptyFunction,
};
}

componentWillMount() {
const { insertCss } = this.props.context;
this.removeCss = insertCss(s);
this.removeCss = this.context.insertCss(s);
}

componentWillUnmount() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Footer/Footer.js
Expand Up @@ -10,7 +10,7 @@
import React, { Component } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Footer.scss';
import Link from '../Link';
import { Link } from 'react-router';

class Footer extends Component {

Expand Down
6 changes: 3 additions & 3 deletions src/components/Header/Header.js
Expand Up @@ -10,7 +10,7 @@
import React, { Component } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Header.scss';
import Link from '../Link';
import { IndexLink } from 'react-router';
import Navigation from '../Navigation';

class Header extends Component {
Expand All @@ -20,10 +20,10 @@ class Header extends Component {
<div className={s.root}>
<div className={s.container}>
<Navigation className={s.nav} />
<Link className={s.brand} to="/">
<IndexLink className={s.brand} to="/">
<img src={require('./logo-small.png')} width="38" height="38" alt="React" />
<span className={s.brandTxt}>Your Company</span>
</Link>
</IndexLink>
<div className={s.banner}>
<h1 className={s.bannerTitle}>React</h1>
<p className={s.bannerDesc}>Complex web apps made easy</p>
Expand Down
63 changes: 0 additions & 63 deletions src/components/Link/Link.js

This file was deleted.

6 changes: 0 additions & 6 deletions src/components/Link/package.json

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/Navigation/Navigation.js
Expand Up @@ -11,7 +11,7 @@ import React, { Component, PropTypes } from 'react';
import cx from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Navigation.scss';
import Link from '../Link';
import { Link } from 'react-router';

class Navigation extends Component {

Expand Down
38 changes: 38 additions & 0 deletions src/core/ContextHolder.js
@@ -0,0 +1,38 @@
import React, { PropTypes } from 'react';
import emptyFunction from 'fbjs/lib/emptyFunction';

class ContextHolder extends React.Component {

static propTypes = {
context: PropTypes.shape({
insertCss: PropTypes.func,
onSetMeta: PropTypes.func,
onPageNotFound: PropTypes.func,
}),
children: PropTypes.element.isRequired,
};

static childContextTypes = {
insertCss: PropTypes.func,
onSetTitle: PropTypes.func,
onSetMeta: PropTypes.func,
onPageNotFound: PropTypes.func,
};

getChildContext() {
const context = this.props.context;
return {
insertCss: context.insertCss || emptyFunction,
onSetTitle: context.onSetTitle || emptyFunction,
onSetMeta: context.onSetMeta || emptyFunction,
onPageNotFound: context.onPageNotFound || emptyFunction,
};
}

render() {
const { children } = this.props;
return React.Children.only(children);
}
}

export default ContextHolder;
6 changes: 2 additions & 4 deletions src/core/Location.js
Expand Up @@ -7,10 +7,8 @@
* LICENSE.txt file in the root directory of this source tree.
*/

import createHistory from 'history/lib/createBrowserHistory';
import createMemoryHistory from 'history/lib/createMemoryHistory';
import useQueries from 'history/lib/useQueries';
import { browserHistory, createMemoryHistory } from 'react-router';

const location = useQueries(process.env.BROWSER ? createHistory : createMemoryHistory)();
const location = process.env.BROWSER ? browserHistory : createMemoryHistory();

export default location;
51 changes: 23 additions & 28 deletions src/routes.js
Expand Up @@ -8,39 +8,34 @@
*/

import React from 'react';
import Router from 'react-routing/src/Router';
import { IndexRoute, Route } from 'react-router';
import fetch from './core/fetch';
import App from './components/App';
import ContentPage from './components/ContentPage';
import ContactPage from './components/ContactPage';
import LoginPage from './components/LoginPage';
import RegisterPage from './components/RegisterPage';
import NotFoundPage from './components/NotFoundPage';
import ErrorPage from './components/ErrorPage';

const router = new Router(on => {
on('*', async (state, next) => {
const component = await next();
return component && <App context={state.context}>{component}</App>;
});

on('/contact', async () => <ContactPage />);

on('/login', async () => <LoginPage />);

on('/register', async () => <RegisterPage />);

on('*', async (state) => {
const query = `/graphql?query={content(path:"${state.path}"){path,title,content,component}}`;
const response = await fetch(query);
const { data } = await response.json();
return data && data.content && <ContentPage {...data.content} />;
});

on('error', (state, error) => state.statusCode === 404 ?
<App context={state.context} error={error}><NotFoundPage /></App> :
<App context={state.context} error={error}><ErrorPage /></App>
);
});

export default router;
async function getContextComponent(location, callback) {
const query = '/graphql?' +
`query={content(path:"${location.pathname}"){path,title,content,component}}`;
const response = await fetch(query);
const { data } = await response.json();
// using an arrow to pass page instance instead of page class; cb accepts class by default
callback(null, () => <ContentPage {...data.content} />);
}

export default (
<Route>
<Route path="/" component={App}>
<IndexRoute getComponent={getContextComponent} />
<Route path="contact" component={ContactPage} />
<Route path="login" component={LoginPage} />
<Route path="register" component={RegisterPage} />
<Route path="about" getComponent={getContextComponent} />
<Route path="privacy" getComponent={getContextComponent} />
</Route>
<Route path="*" component={NotFoundPage} />
</Route>
);