Skip to content
This repository has been archived by the owner on Jun 3, 2019. It is now read-only.

Commit

Permalink
Use Found for React Routing
Browse files Browse the repository at this point in the history
Implements Cliend & Server Side Rendering using Found
  • Loading branch information
LorbusChris committed Dec 8, 2016
1 parent 7564071 commit 3de0d0a
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 66 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -39,7 +39,7 @@
"Carson Perrotti <carsonperrotti@gmail.com>",
"Steven Truesdell <steven@strues.io>",
"Benjamin Kniffler <bkniffler@me.com>",
"Christian Glombek",
"Christian Glombek <christian.glombek@rwth-aachen.de>",
"Joe Kohlmann <kohlmannj@mac.com>",
"Evgeny Boxer",
"Alin Porumb",
Expand All @@ -57,14 +57,14 @@
"compression": "1.6.2",
"dotenv": "2.0.0",
"express": "4.14.0",
"found": "0.2.0",
"helmet": "3.1.0",
"hpp": "0.2.1",
"normalize.css": "5.0.0",
"offline-plugin": "4.5.3",
"react": "15.4.1",
"react-dom": "15.4.1",
"react-helmet": "3.2.3",
"react-router": "4.0.0-alpha.6",
"serialize-javascript": "1.3.0",
"source-map-support": "0.4.6",
"user-home": "2.0.0",
Expand Down
19 changes: 11 additions & 8 deletions src/client/index.js
Expand Up @@ -3,15 +3,20 @@

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router';
import createInitialBrowserRouter from 'found/lib/createInitialBrowserRouter';
import { CodeSplitProvider, rehydrateState } from 'code-split-component';
import ReactHotLoader from './components/ReactHotLoader';
import App from '../shared/components/App';
import { routeConfig, renderConfig } from '../shared/components/App';

// Get the DOM Element that will host our React application.
const container = document.querySelector('#app');

function renderApp(TheApp) {
async function renderApp() {
const BrowserRouter = await createInitialBrowserRouter({
routeConfig,
render: renderConfig,
});

// We use the code-split-component library to provide us with code splitting
// within our application. This library supports server rendered applications,
// but for server rendered applications it requires that we rehydrate any
Expand All @@ -25,9 +30,7 @@ function renderApp(TheApp) {
render(
<ReactHotLoader>
<CodeSplitProvider state={codeSplitState}>
<BrowserRouter>
<TheApp />
</BrowserRouter>
<BrowserRouter />
</CodeSplitProvider>
</ReactHotLoader>,
container,
Expand All @@ -42,12 +45,12 @@ if (process.env.NODE_ENV === 'development' && module.hot) {
// Any changes to our App will cause a hotload re-render.
module.hot.accept(
'../shared/components/App',
() => renderApp(require('../shared/components/App').default),
() => renderApp(),
);
}

// Execute the first render of our app.
renderApp(App);
renderApp();

// This registers our service worker for asset caching and offline support.
// Keep this as the last item, just in case the code execution failed (thanks
Expand Down
43 changes: 15 additions & 28 deletions src/server/middleware/reactApplication/index.js
Expand Up @@ -3,18 +3,18 @@
import type { $Request, $Response, Middleware } from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerRouter, createServerRenderContext } from 'react-router';
import { getFarceResult } from 'found/lib/server';
import { CodeSplitProvider, createRenderContext } from 'code-split-component';
import Helmet from 'react-helmet';
import generateHTML from './generateHTML';
import App from '../../../shared/components/App';
import { routeConfig, renderConfig } from '../../../shared/components/App';
import envConfig from '../../../../config/private/environment';

/**
* An express middleware that is capabable of service our React application,
* supporting server side rendering of the application.
*/
function reactApplicationMiddleware(request: $Request, response: $Response) {
async function reactApplicationMiddleware(request: $Request, response: $Response) {
// We should have had a nonce provided to us. See the server/index.js for
// more information on what this is.
if (typeof response.locals.nonce !== 'string') {
Expand All @@ -38,9 +38,16 @@ function reactApplicationMiddleware(request: $Request, response: $Response) {
return;
}

// First create a context for <ServerRouter>, which will allow us to
// query for the results of the render.
const reactRouterContext = createServerRenderContext();
const { redirect, status, element } = await getFarceResult({
url: request.url,
routeConfig,
render: renderConfig,
});

if (redirect) {
response.redirect(302, redirect.url);
return;
}

// We also create a context for our <CodeSplitProvider> which will allow us
// to query which chunks/modules were used during the render process.
Expand All @@ -49,9 +56,7 @@ function reactApplicationMiddleware(request: $Request, response: $Response) {
// Create our application and render it into a string.
const app = renderToString(
<CodeSplitProvider context={codeSplitContext}>
<ServerRouter location={request.url} context={reactRouterContext}>
<App />
</ServerRouter>
{element}
</CodeSplitProvider>,
);

Expand All @@ -71,26 +76,8 @@ function reactApplicationMiddleware(request: $Request, response: $Response) {
codeSplitState: codeSplitContext.getState(),
});

// Get the render result from the server render context.
const renderResult = reactRouterContext.getResult();

// Check if the render result contains a redirect, if so we need to set
// the specific status and redirect header and end the response.
if (renderResult.redirect) {
response.status(301).setHeader('Location', renderResult.redirect.pathname);
response.end();
return;
}

response
.status(
renderResult.missed
// If the renderResult contains a "missed" match then we set a 404 code.
// Our App component will handle the rendering of an Error404 view.
? 404
// Otherwise everything is all good and we send a 200 OK status.
: 200,
)
.status(status)
.send(html);
}

Expand Down
27 changes: 3 additions & 24 deletions src/shared/components/App/App.js
@@ -1,16 +1,14 @@
/* @flow */

import React from 'react';
import { Match, Miss } from 'react-router';
import Helmet from 'react-helmet';
import { CodeSplit } from 'code-split-component';
import 'normalize.css/normalize.css';
import './globals.css';
import Error404 from './Error404';
import type { ReactChildren } from '../../types/react';
import Header from './Header';
import htmlPageConfig from '../../../../config/public/htmlPage';

function App() {
function App({ children }: { children?: ReactChildren }) {
return (
<div style={{ padding: '10px' }}>
{/*
Expand All @@ -28,26 +26,7 @@ function App() {

<Header />

<Match
exactly
pattern="/"
render={routerProps =>
<CodeSplit chunkName="home" modules={{ Home: require('./Home') }}>
{ ({ Home }) => Home && <Home {...routerProps} /> }
</CodeSplit>
}
/>

<Match
pattern="/about"
render={routerProps =>
<CodeSplit chunkName="about" modules={{ About: require('./About') }}>
{ ({ About }) => About && <About {...routerProps} /> }
</CodeSplit>
}
/>

<Miss component={Error404} />
{children}
</div>
);
}
Expand Down
18 changes: 15 additions & 3 deletions src/shared/components/App/Header/Menu/Menu.js
@@ -1,13 +1,25 @@
/* @flow */

import React from 'react';
import { Link } from 'react-router';
import { Link } from 'found';

function LinkItem(props) {
// TODO: Remove the pragma once evcohen/eslint-plugin-jsx-a11y#81 ships.
return (
<li>
<Link // eslint-disable-line jsx-a11y/anchor-has-content
{...props}
activeStyle={{ fontWeight: 'bold' }}
/>
</li>
);
}

function Menu() {
return (
<ul style={{ marginTop: '1rem' }}>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<LinkItem to="/" exact>Home</LinkItem>
<LinkItem to="/about" exact>About</LinkItem>
</ul>
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/shared/components/App/index.js
@@ -1,3 +1,5 @@
/* @flow */

export { default } from './App';
export { default as App } from './App';
export { default as routeConfig } from './routeConfig';
export { default as renderConfig } from './renderConfig';
15 changes: 15 additions & 0 deletions src/shared/components/App/renderConfig.js
@@ -0,0 +1,15 @@
/* flow */

import React from 'react';
import createRender from 'found/lib/createRender';
import App from './App';
import Error404 from './Error404';

// createRender({ renderPending, readyReady, renderError })
export default createRender({
renderError: ({ error }) => ( // eslint-disable-line react/prop-types
<div>
{error.status === 404 ? <App><Error404 /></App> : 'Error'}
</div>
),
});
49 changes: 49 additions & 0 deletions src/shared/components/App/routeConfig.js
@@ -0,0 +1,49 @@
/* @flow */

import React from 'react';
import makeRouteConfig from 'found/lib/jsx/makeRouteConfig';
import Route from 'found/lib/jsx/Route';
import { CodeSplit } from 'code-split-component';
import type { ReactElement } from '../../types/react';
import App from './App';

function routeRender({ Component, props }: {Component: ReactElement, props?: any}) {
if (!Component || !props) {
return <div><small>Loading&hellip;</small></div>;
}

return <Component {...props} />;
}

function CodeSplitHome() {
return (
<CodeSplit chunkName="home" modules={{ Home: require('./Home') }}>
{ ({ Home }) => Home && <Home /> }
</CodeSplit>
);
}

function CodeSplitAbout() {
return (
<CodeSplit chunkName="about" modules={{ About: require('./About') }}>
{ ({ About }) => About && <About /> }
</CodeSplit>
);
}

export default makeRouteConfig(
<Route
path="/"
Component={App}
>
<Route
Component={CodeSplitHome}
render={routeRender}
/>
<Route
path="about"
Component={CodeSplitAbout}
render={routeRender}
/>
</Route>,
);

0 comments on commit 3de0d0a

Please sign in to comment.