9

I'm running server-side render of the React application. I'm using express for this purposes. The whole server-side render code looks like this:

import * as React from "react"
import * as ReactDOMServer from "react-dom/server"
import * as express from "express"
import { StaticRouter } from "react-router-dom"
import walker = require("react-tree-walker")
import { useStaticRendering } from "mobx-react"
import Helmet from "react-helmet"
import Provider from "../src/Provider"
import { StaticRouterContext } from "react-router"
import version = require("../version")
var _template: string = require(`../dist/${version.v()}/index.html`)
interface IRenderResponse {
 statusCode: number,
 template: string,
 redirect?: string
}
const run = (url: string, locale?: string): Promise<IRenderResponse> => {
 var template: string
 var html: string = ""
 var head: object
 var context: StaticRouterContext = {}
 useStaticRendering(true)
 var routing = (
 <StaticRouter 
 location={url} 
 context={context}
 >
 <Provider defaultLocale={locale} />
 </StaticRouter>
 )
 return new Promise((resolve) => {
 walker(routing, (element, instance) => {
 if (instance && typeof instance._prepare == typeof (() => {}))
 return instance._prepare()
 }).then(() => {
 html = ReactDOMServer.renderToString(routing)
 head = Helmet.renderStatic()
 template = _template.replace(/\${__rh-([a-z]+)}/gi, (match, group) => {
 return head[group].toString()
 })
 template = template.replace(/\${__body}/gi, (match) => {
 return html
 })
 if (context.url)
 context["statusCode"] = 301
 resolve({
 statusCode: context["statusCode"] || 200, 
 template, 
 redirect: context.url
 })
 }).catch((error) => {
 template = _template.replace(/\${__rh-([a-z]+)}/gi, "")
 template = template.replace(/\${__body}/gi, error.stack || error.toString())
 resolve({
 statusCode: 500, 
 template
 })
 })
 })
}
var app = express()
app.get("*", (req, res) => {
 var accepted = req.acceptsLanguages()
 var locale = accepted ? (accepted[0] || "ru").split("-")[0] : "ru"
 run(req.originalUrl, locale).then((data) => {
 if (data.redirect)
 res.redirect(data.redirect)
 else
 res.status(data.statusCode).send(data.template)
 })
})
app.listen(1239)

You can see that the react-tree-walker is used here. But this problem occurs whatever I'm using for the server-side render.

The problem is that if my node-js server is running in one thread, then if two different requests are being done simultaneously, then react-helmet mixes fields. For example, if there are two views:

class FirstView extends React.Component {
 render() {
 return (
 <Helmet>
 <title>This is first title</title>
 <meta name="description" content="My first view description" />
 </Helmet>
 )
 }
}

and

class SecondView extends React.Component {
 render() {
 return (
 <Helmet>
 <title>This is second title</title>
 <meta name="description" content="My second view description" />
 </Helmet>
 )
 }
}

then I can receive the head something like that:

<title>This is first title</title>
<meta name="description" content="My second view description" />

Obviously this happens because of react-helmet uses static fields, I suppose. So, if two requests are being handled in parallel, this fields are being changed chaoticaly.

How can I defeat it? This problem often occurs for high-load projects, and this can crash the SEO, because crawler can receive wrong data.


webpack.config.js:

var webpack = require("webpack")
var config = {
 mode: "production",
 target: "node",
 entry: __dirname + "/index.tsx",
 output: {
 path: __dirname,
 filename: `index.js`
 },
 module: {
 rules: [
 {
 test: require.resolve("phoenix"),
 use: "imports-loader?window=>global"
 },
 {
 test: /\.tsx?$/,
 loader: "awesome-typescript-loader?configFileName=tsconfig.server.json",
 exclude: /node_modules/
 },
 {
 test: /\.js$/,
 enforce: "pre",
 loader: "source-map-loader"
 },
 {
 test: /\.html$/,
 loader: "html-loader"
 },
 {
 test: /\.(svg|woff|woff2|ttf|otf|png|jpg)$/,
 loader: "url-loader"
 },
 {
 test: /\.(css|sass)$/,
 loader: "ignore-loader"
 }
 ]
 },
 resolve: {
 modules: [
 "node_modules",
 `${__dirname}/../src`
 ],
 extensions: [".js", ".jsx", ".sass", ".json", ".css", ".ts", ".tsx"]
 },
 parallelism: 2,
 plugins: [
 new webpack.DefinePlugin({
 "ENV": JSON.stringify("server"),
 "process.env.ENV": JSON.stringify("server"),
 "process.env.VERSION": JSON.stringify("n/a"),
 "process.env.NODE_ENV": JSON.stringify("production"),
 "global.GENTLY": false
 })
 ]
}
module.exports = config

Edit

Seems like, react-helmet is thread-unsafe according to this issue. Is there any possibility to create some workaround based on this knowlenge?

asked Feb 16, 2019 at 10:24
8
  • are FirstView and SecondView on different routes? Commented Feb 18, 2019 at 13:08
  • @GauravSaraswat yes, of course Commented Feb 18, 2019 at 15:29
  • i doubt require(../dist/${version.v()}/index.html) gives you a string object which modifies the content of exported /index.html. can you tell me which loader are you using to require .html files Commented Feb 19, 2019 at 9:16
  • @GauravSaraswat oh, I think I'm starting to understand what the trick. I have updated my question with the webpack.config.js (I use html-loader) Commented Feb 19, 2019 at 15:25
  • I dont think, that should create any problem. can you try to just require the html file only once at top with imports instead of again and again in run function and assign it to template private variable inside the promise returned in run function Commented Feb 19, 2019 at 19:02

1 Answer 1

6
+25

I feels require(../dist/${version.v()}/index.html); imports only one instance of the template.

By requiring the template repeatedly in all network calls you might be updating the same string reference again and again.

so. You can do the following:

  1. import(require) template only once at the top of your file.
  2. assign it to new variable of string type inside the promise returned in the run function and perform replace operations over it.

basic code snippet:

...
import version = require("../version");
import appTemplate = require(`../dist/${version.v()}/index.html`)
interface IRenderResponse {
 statusCode: number,
 template: string,
 redirect?: string
}
const run = (url: string, locale?: string): Promise<IRenderResponse> => {
 var template: string = "";
 var html: string = ""
 var head: object
 var context: StaticRouterContext = {}
 useStaticRendering(true)
 var routing = (
 <StaticRouter 
 location={url} 
 context={context}
 >
 <Provider defaultLocale={locale} />
 </StaticRouter>
 )
 return new Promise((resolve) => {
 walker(routing, (element, instance) => {
 if (instance && typeof instance._prepare == typeof (() => {}))
 return instance._prepare()
 }).then(() => {
 html = ReactDOMServer.renderToString(routing)
 head = Helmet.renderStatic()
 template = appTemplate.replace(/\${__rh-([a-z]+)}/gi, (match, group) => {
 return head[group].toString()
 })
 template = template.replace(/\${__body}/gi, (match) => {
 return html
 })
 if (context.url)
 context["statusCode"] = 301
 resolve({
 statusCode: context["statusCode"] || 200, 
 template, 
 redirect: context.url
 })
 })....

answered Feb 22, 2019 at 10:49
Sign up to request clarification or add additional context in comments.

2 Comments

FInally I have checked this out, and it is not working. And I think, that there is no trick in import or something. appTemplate is being imported, so webpack loads it as a string during compilation. No filesystem operaions are being executed during run(). But fields are still mixing. I think, that problem is exactly in react-helmet

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.