-
-
Notifications
You must be signed in to change notification settings - Fork 381
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
Example of how to fetch API data for SSR #30
Comments
@adardesign this is not the responsibility of loadable-components, you should try to use Apollo for GraphQL or another alternative for REST API. |
I think I'm looking for the same thing as the OP If I use loadable to load a component which loads data from an API (in this example using apollo client) then I end up with the Is there a way to tell loadable to load fully in this situation? (I had figured this would be what I'm set up like this Home content loaded with loadable
Home component returns: <Query
query={query}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const items = data.listContent.map((item, index) =>
<li key={index}>
<Card
title={item.title}
path={item.path}
image={item.image}
/>
</li>
);
return (
<ul>
{items}
</ul>
)
}}
</Query> On the server side: const App = extractor.collectChunks(
<ApolloProvider client={client}>
<StaticRouter context={context} location={req.url}>
<Layout />
</StaticRouter>
</ApolloProvider>
);
renderToStringWithData(App)
.then((content) => {
const initialState = client.extract();
const helmetData = Helmet.renderStatic();
const html = <Html
content={content}
state={initialState}
helmetData={helmetData}
linkTags={extractor.getLinkElements()}
scripts={extractor.getScriptElements()}
/>;
res.status(200);
res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`);
res.end();
})
.catch(e => {
console.error('RENDERING ERROR:', e); // eslint-disable-line no-console
res.status(500);
res.end(
`An error occurred:\n\n${
e.stack
}`
);
});
}); |
Hello @bigwillch, I use Apollo too, you are lucky! I figured out that react-apollo v2.2.4 si not compatible with "forwardRef" (used in "@loadable/component"). You should try to use react-apollo v2.3.0, it should solve your problem. So use Please tell me if it solves your problem. |
This is my SSR middleware: import path from 'path'
import React from 'react'
import { ServerStyleSheet } from 'styled-components'
import { renderToString, renderToNodeStream } from 'react-dom/server'
import { StaticRouter } from 'react-router'
import { HelmetProvider } from 'react-helmet-async'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import { ChunkExtractor } from '@loadable/server'
import config, { getClientConfig } from 'server/config'
import Head from 'server/components/Head'
import Body from 'server/components/Body'
import { asyncMiddleware } from 'server/utils/express'
import { createApolloClient } from 'server/graphql/apolloClient'
const nodeStats = path.resolve(
config.get('server.publicPath'),
'dist/node/loadable-stats.json',
)
const webStats = path.resolve(
config.get('server.publicPath'),
'dist/web/loadable-stats.json',
)
const ssr = asyncMiddleware(async (req, res) => {
const nodeExtractor = new ChunkExtractor({
statsFile: nodeStats,
outputPath: path.join(config.get('server.publicPath'), 'dist/node'),
})
const { default: App } = nodeExtractor.requireEntrypoint()
const webExtractor = new ChunkExtractor({ statsFile: webStats })
const apolloClient = createApolloClient()
const routerContext = {}
const helmetContext = {}
const app = (
<ApolloProvider client={apolloClient}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={req.url} context={routerContext}>
<App />
</StaticRouter>
</HelmetProvider>
</ApolloProvider>
)
// Styled components
const sheet = new ServerStyleSheet()
let jsx = sheet.collectStyles(app)
jsx = webExtractor.collectChunks(app)
// Apollo
await getDataFromTree(jsx)
const apolloState = apolloClient.extract()
// Handle React router status
if (routerContext.status) {
res.status(routerContext.status)
}
// Handle React Router redirection
if (routerContext.url) {
const status = routerContext.status === 301 ? 301 : 302
res.redirect(status, routerContext.url)
return
}
const { helmet } = helmetContext
const stream = sheet.interleaveWithNodeStream(renderToNodeStream(jsx))
const head = renderToString(<Head helmet={helmet} extractor={webExtractor} />)
res.set('content-type', 'text/html')
res.write(
`<!DOCTYPE html><html ${helmet.htmlAttributes}><head>${head}</head><body ${
helmet.bodyAttributes
}><div id="main">`,
)
stream.pipe(
res,
{ end: false },
)
stream.on('end', () => {
const body = renderToString(
<Body
config={getClientConfig()}
helmet={helmet}
extractor={webExtractor}
apolloState={apolloState}
/>,
)
res.end(`</div>${body}</body></html>`)
})
})
export default ssr |
Fantastic! That's got it working. I certainly am lucky!! :) Thanks so much - and thanks for sharing the middleware. I'll definitely be cribbing from it. Really amazing work on the module and documentation, thanks again |
@neoziro |
@xFloooo you are right, I edited it! Thanks! |
@neoziro also in this example there is a memory leak solution const nodeExtractor = new ChunkExtractor({
statsFile: nodeStats,
outputPath: path.join(config.get('server.publicPath'), 'dist/node'),
})
const { default: App } = nodeExtractor.requireEntrypoint()
const webExtractor = new ChunkExtractor({ statsFile: webStats })
const ssr = asyncMiddleware(async (req, res) => {
...
}) |
Hello @xFloooo, if there is a memory leak it is a problem. Where do you see a memory leak? |
@neoziro, I'm not completely sure what it is I launched my application in debug mode // package.json
"debug": "cross-env NODE_ENV=development npm run set-locale && node --harmony --inspect lib/server/index.js" // rederMiddleware.js
import path from "path";
import { ChunkExtractor } from "@loadable/server";
import { jss, JssProvider, SheetsRegistry, ThemeProvider } from "react-jss";
import { ApolloProvider, getDataFromTree } from "react-apollo";
import { StaticRouter } from "react-router-dom";
import { Provider } from "mobx-react";
import theme from "../../../application/styles/themes/base";
import React from "react";
import { renderToNodeStream } from "react-dom/server";
import normalize from "normalize-jss";
import { Helmet } from "react-helmet";
let configs = require("../configs/index");
// let configRoutes = require("../../../configs/routes");
const ZipkinJavascriptOpentracing = require("zipkin-javascript-opentracing");
const tracer = require("../../../tracer/tracer").tracer;
const texts = require("../../../configs/text.config.json");
const Cookies = require("cookies");
const nodeStats = path.resolve(
__dirname,
"../../../public/dist/node/loadable-stats.json"
);
const webStats = path.resolve(
__dirname,
"../../../public/dist/web/loadable-stats.json"
);
const render = async function(req, res, next) {
// start render tracer
const child = tracer.startSpan("react render middleware", {
childOf: req.span
});
tracer.inject(
child,
ZipkinJavascriptOpentracing.FORMAT_HTTP_HEADERS,
req.traceHeaders
);
const apolloClient = req.apolloClient;
/*****************************/
try {
if (req.is404) {
throw new Error("Error application");
}
const nodeExtractor = new ChunkExtractor({
statsFile: nodeStats,
entrypoints: "app"
});
const {
default: App,
ApplicationModel
} = nodeExtractor.requireEntrypoint();
const webExtractor = new ChunkExtractor({
statsFile: webStats,
entrypoints: "app"
});
const cookies = new Cookies(req, res);
const preloadStateMobx = preloadStateData(req);
let applicationModel = new ApplicationModel(null, {});
applicationModel.preloadData(preloadStateMobx);
applicationModel.preloadTexts(texts);
const sheets = new SheetsRegistry();
const createGenerateClassName = () => {
let counter = 0;
return (rule, sheet) => `app${counter++}`;
};
const stores = {
routing: {},
ApplicationModel: applicationModel
};
const app = (
<ApolloProvider client={apolloClient}>
<StaticRouter location={req.url} context={{}}>
<Provider {...stores}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</Provider>
</StaticRouter>
</ApolloProvider>
);
const jsx = webExtractor.collectChunks(app);
// Apollo
await getDataFromTree(jsx);
const apolloState = apolloClient.extract();
const helmet = Helmet.renderStatic();
const stream = renderToNodeStream(
<JssProvider
jss={jss}
registry={sheets}
generateClassName={createGenerateClassName()}
>
{jsx}
</JssProvider>
);
res.set("content-type", "text/html");
res.write(`<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<meta charSet="utf-8" />
<meta
name="google-site-verification"
content="6BurPKJZGSClBM5QuL_myv0xHsorOa44i6RMpZsgPVc"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<meta name="theme-color" content="#5812fe" />
${webExtractor.getLinkTags()}
${webExtractor.getStyleTags()}
</head>
<body>
<div id="root">`);
stream.pipe(
res,
{ end: false }
);
stream.on("end", async () => {
const cleanSheets_jss_app = sheets
.toString()
.replace(/\s{2,}|\r+|\n+/gm, "");
const cleanSheets_jss_normalize = jss
.createStyleSheet(normalize)
.toString()
.replace(/\s{2,}|\r+|\n+/gm, "");
res.end(`</div>
<style type="text/css" id="server-side-styles">
${cleanSheets_jss_normalize}
${cleanSheets_jss_app}
</style>
<script>window.texts=${JSON.stringify(texts)};</script>
<script>window.__MOBX_STATE__=${JSON.stringify(
preloadStateMobx
)};</script>
<script>window.__APOLLO_STATE__=${JSON.stringify(
apolloState
).replace(/</g, "\\\u003c")};</script>
${webExtractor.getScriptTags()}
</body>
</html>`);
});
} catch (e) {
console.log(e);
res.status(404);
res.send("error");
} finally {
/* finish render tracer **/
child.finish();
}
};
/**
* @param {RequestExstend} req
* @param res
* @param next
*/
function preloadStateData(req) {
let MetrikaId = configs.get("YANDEX_METRICA_ID");
let googleMetrikaId = configs.get("GOOGLE_METRIKA_ID");
let slackChannelId = configs.get("SLACK_CHANNEL_ID");
let slackChannelErrorsId = "CCABR59B7";
return {
location: req.url,
context: {},
data: {
currentRegion: req.site.currentRegion
? req.site.currentRegion
: null,
defaultRegion: req.site.regions.default
? req.site.regions.default
: null
},
MetrikaId: MetrikaId,
googleMetrikaId: googleMetrikaId,
slackChannelId: slackChannelId,
slackChannelErrorsId: slackChannelErrorsId,
siteCode: req.siteCode
};
}
module.exports = render; after fix // renderMiddleware
const nodeExtractor = new ChunkExtractor({
statsFile: nodeStats,
entrypoints: "app"
});
const { default: App, ApplicationModel } = nodeExtractor.requireEntrypoint();
const webExtractor = new ChunkExtractor({
statsFile: webStats,
entrypoints: "app"
});
const render = async function(req, res, next) {
...
}) |
@xFloooo Node has a special way to manage memory, garbage collector runs only when needed. I think there is no memory leak, I run it in production and my memory is stable. |
Hello @neoziro! I use Apollo too. Got it working with your example of middleware! But could you explain a bit how exactly it works? )) I can't figure out what this part do: const nodeExtractor = new ChunkExtractor({
statsFile: nodeStats,
outputPath: path.join(config.get('server.publicPath'), 'dist/node'),
})
const { default: App } = nodeExtractor.requireEntrypoint()
const webExtractor = new ChunkExtractor({ statsFile: webStats }) Why do we need both nodeExtractor and webExtractor? And what nodeExtractor actually do? Thank you! |
Hello @evgeniysolodkov, I use |
I'm also confused about this.It would be great if there were examples to tell me what to do. |
I am looking for a example of how I can achieve SSR with
loadable-components
that would also fetch dynamic data needed for he component of the route and have that rendered server side..The text was updated successfully, but these errors were encountered: