Add second pass rendering for async initial data

This commit is contained in:
Jan Tuomi
2018-08-19 17:07:41 +03:00
parent 62b2cd2f4a
commit 0c8318be76
10 changed files with 118 additions and 63 deletions
-6
View File
@@ -1,6 +0,0 @@
.post {
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
-20
View File
@@ -1,20 +0,0 @@
import * as React from "react";
import "./Post.scss";
import { Post as PostInterface } from "../../models/Post";
export interface PostProps {
post: PostInterface;
}
class Post extends React.Component<PostProps, undefined> {
render() {
const { title, author, id } = this.props.post;
return (
<div className="post">
#{ id }: <strong>{ title }</strong> from <i>{ author }</i>
</div>
);
}
}
export default Post;
-2
View File
@@ -1,2 +0,0 @@
import Post from "./Post";
export default Post;
+4 -4
View File
@@ -1,14 +1,14 @@
import axios from "axios";
const url = `${process.env.API_URL}/posts`;
const url = `${process.env.API_URL}/events`;
export interface Post {
export interface Event {
id: number;
title: string;
author: string;
description: string;
}
export async function getPosts(): Promise<Post[]> {
export async function getEvents(): Promise<Event[]> {
try {
const resp = await axios.get(url);
return resp.data;
+1 -1
View File
@@ -13,7 +13,7 @@ class CommonPage extends React.Component<CommonPageProps, CommonPageState> {
return (
<React.Fragment>
<Header />
<Page />
<Page {...this.props} />
<Footer />
</React.Fragment>
);
+73 -25
View File
@@ -2,38 +2,86 @@ import * as React from "react";
import Helmet from "react-helmet";
import "./FrontPage.scss";
import appStore from "../../stores/AppStore";
import { getPosts, Post as PostInterface } from "../../models/Post";
import Button from "../../components/Button";
import Post from "../../components/Post";
import JsonLD from "../../components/JsonLD";
import Card from "../../components/Card";
import { Event, getEvents } from "../../models/Event";
import { StaticContext } from "../../server/StaticContext";
// @ts-ignore
import * as BeerImage from "../../assets/img/beer.jpeg";
class FrontPage extends React.Component<undefined, undefined> {
interface FrontPageProps {
staticContext: StaticContext;
}
interface FrontPageState {
events: Event[];
}
class FrontPage extends React.Component<FrontPageProps, FrontPageState> {
constructor(props: FrontPageProps) {
super(props);
const { staticContext } = props;
if (staticContext) {
/* The static context is an object that manages promises when
rendering on the server. If staticContext exists, that means
we have to store all promises in it. Otherwise, operate
normally. See server/index.ts. */
if (staticContext.resolutions.getEvents) {
const events = staticContext.resolutions.getEvents as Event[];
this.state = {
events,
};
} else {
this.state = {
events: [],
};
const promise = this.fetchEvents();
staticContext.promises.getEvents = promise;
}
} else {
this.state = {
events: [],
};
this.fetchEvents();
}
}
fetchEvents = () => {
const getEventsPromise = getEvents();
getEventsPromise.then(events => {
this.setState({
events,
});
});
return getEventsPromise;
}
render() {
return <div className="front-page">
<Helmet>
<link rel="canonical" href="https://sik.ayy.fi" />
</Helmet>
<JsonLD data={{
"@context": "http://schema.org",
"@type": "WebSite",
"url": "https://sik.ayy.fi",
}} />
<Card
title="Øhlhäfv 15.2.2019"
text="Selittelyjen aika on ohi, nyt heevataan!"
image={BeerImage} />
<Card
title="Øhlhäfv 15.2.2019"
text="Selittelyjen aika on ohi, nyt heevataan! Selittelyjen aika on ohi, nyt heevataan! Selittelyjen aika on ohi, nyt heevataan!"
image={BeerImage} />
<Card
title="Øhlhäfv 15.2.2019"
text="Selittelyjen aika on ohi, nyt heevataan!"
image={BeerImage} />
</div>;
const { events } = this.state;
return (
<div className="front-page">
<Helmet>
<link rel="canonical" href="https://sik.ayy.fi" />
</Helmet>
<JsonLD data={{
"@context": "http://schema.org",
"@type": "WebSite",
"url": "https://sik.ayy.fi",
}} />
{ events.map(event => (
<Card
title={event.title}
text={event.description}
key={event.id}
image={BeerImage}
/>
))}
</div>
);
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ const Routes = () => (
<meta name="description" content="Aalto-yliopiston Sähköinsinoorikillan verkkosivut" />
</Helmet>
<Switch>
<Route exact path="/" render={() => <CommonPage page={FrontPage} />} />
<Route exact path="/" render={(props) => <CommonPage page={FrontPage} {...props} />} />
<Route component={NotFoundPage} />
</Switch>
</Fragment>
+2 -2
View File
@@ -2,8 +2,8 @@ import * as React from "react";
import { StaticRouter } from "react-router-dom";
import Routes from "../routes";
export default ({ url }) => (
<StaticRouter context={{}} location={url}>
export default ({ url, context }) => (
<StaticRouter context={context} location={url}>
<Routes />
</StaticRouter>
);
+4
View File
@@ -0,0 +1,4 @@
export interface StaticContext {
resolutions: { [key: string]: any };
promises: { [key: string]: Promise<any> };
}
+33 -2
View File
@@ -9,6 +9,7 @@ import * as compression from "compression";
import App from "./App";
import * as fs from "fs";
import * as path from "path";
import { StaticContext } from "./StaticContext";
const port = 3000;
const server = express();
@@ -27,8 +28,38 @@ server.use(compression());
server.use("/assets", express.static("dist/assets"));
server.use("/js", express.static("dist/js"));
server.get("*", (req, res) => {
const result = renderToString(React.createElement(App, { url: req.url }));
server.get("*", async (req, res) => {
const context: StaticContext = {
resolutions: {}, promises: {},
};
const firstPassRenderResult = renderToString(React.createElement(App, { url: req.url, context }));
const promiseKeys = Object.keys(context.promises);
let result: string;
if (promiseKeys.length === 0) {
/* No promises to resolve on the first pass. Render html normally. */
result = firstPassRenderResult;
} else {
/* Some promises have to be resolved before rendering the page. */
const promiseEntries = promiseKeys.map(async key => {
const promise = context.promises[key];
return { key, value: await promise };
});
/* Resolve all promises. */
const awaitedEntries = await Promise.all(promiseEntries);
/* Store all resolutions in the context. */
awaitedEntries.forEach(entry => {
context.resolutions[entry.key] = entry.value;
});
/* Render a second time with all resolved data. */
const secondPassRenderResult = renderToString(React.createElement(App, { url: req.url, context }));
result = secondPassRenderResult;
}
const head = Helmet.rewind();
res.send(
html(result, head.title, head.meta)