Add second pass rendering for async initial data
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
.post {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
import Post from "./Post";
|
||||
export default Post;
|
||||
@@ -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;
|
||||
@@ -13,7 +13,7 @@ class CommonPage extends React.Component<CommonPageProps, CommonPageState> {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Header />
|
||||
<Page />
|
||||
<Page {...this.props} />
|
||||
<Footer />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface StaticContext {
|
||||
resolutions: { [key: string]: any };
|
||||
promises: { [key: string]: Promise<any> };
|
||||
}
|
||||
+33
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user