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?

Limbo
  • 2,123
  • 21
  • 41
  • are `FirstView` and `SecondView` on different routes? – Gaurav Saraswat Feb 18 '19 at 13:08
  • @GauravSaraswat yes, of course – Limbo Feb 18 '19 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 – Gaurav Saraswat Feb 19 '19 at 09: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`) – Limbo Feb 19 '19 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` – Gaurav Saraswat Feb 19 '19 at 19:02
  • does that help? – Gaurav Saraswat Feb 21 '19 at 18:00
  • @GauravSaraswat I'm sorry, I'm unable to test it now. I will tell you when it will be checked :) but my 6th sense tells me that you are right – Limbo Feb 22 '19 at 00:29
  • sure. I just updated my answer below. lemme know if it worked :) – Gaurav Saraswat Feb 22 '19 at 10:50

1 Answers1

6

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
            })
        })....
Gaurav Saraswat
  • 1,353
  • 8
  • 11
  • 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` – Limbo May 10 '19 at 15:58
  • And, yes, [`react-helmet` is thread-unsafe](https://github.com/nfl/react-helmet/issues/216) – Limbo May 10 '19 at 16:10