React SSR blinks when starting client
Asked Answered
K

0

6

simplifying my post:

my ssr webpage blinks when starting client which means page renders server side rendered html then goes blank and then it starts loading everything all over again.

going through the details:

i'm working on a react project which we decided to change it from being rendered in client to render in server. the project includes react-router-dom ,redux and react-redux ,material-ui which comes with react-jss ,loadable/component ,also handling the head elements by react-helmet-async ,and in ssr it's using express.js which seems to be a must.

  • for react-router-dom i did everything that is on the docs. using BrowserRouter in client and StaticRouter in ssr and passing a context object to it.
  • for redux and react-redux i saved preloaded_state in a variable in window and fetched it client side then pass it to store.also fetched some external data to get the content on the source of the page.so i have some requests and data fetching in ssr.
  • for material-ui i created a new serverSideStyleSheet and collected all the styles from all over the project.
  • for react-helmet-async i've got different Helmet tags for each page that collects different title ,description and ... individualy.there is also a helmetProvider wrapper for csr and ssr.
  • at first i used react-helmet but it wasn't compatible with renderToNodeStream.i didn't change react-helmet-async although i'm not using renderToNodeStream but kept it to migrate to renderToNodeStream one day hopefully.
  • about express.js i'm using it as the docs say but after i added loadable/component i wasn't able to get a successful server side rendering by just adding app.get('*' , ServerSideRender) .so i had to add every url i wanted to render in server in app.get(url ,ServerSideRender).
  • the other thing about the project is that i didn't eject and created it with create-react-app and there is no webpack config or babelrc but instead i'm using craco.config.js
  • the last thing is that i've excluded index.html from ssr and instead i've made the tags myself in SSR.js file so index.html gets rendered just in client. and i was so careful about writing tags in ssr exactlly like they are in index.html

solution but not the solution:

this problem is occuring because i use loadable component in my Router.js. so when i import components the normal way there is no blink and everything is fine but unused js decreases my page's perfomance score. i need loadable component stop making the page blink.

diving into the code:

just the client

index.html : rendered just in client

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
    <meta name="robots" content="noindex, nofollow" />
    <meta data-rh="true" name="viewport"
        content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
    <link href="%PUBLIC_URL%/fonts.css" rel="stylesheet" />
</head>
<body>
    <div id="root"></div>
    <script src="%PUBLIC_URL%/main.js"></script>
</body>
</html>

index.js : rendered just in client

import React from 'react'
import ReactDOM from 'react-dom'
import {loadableReady} from '@loadable/component'
import App from './App'
import {BrowserRouter} from 'react-router-dom'
import {HelmetProvider} from 'react-helmet-async'
import { Provider } from 'react-redux'

loadableReady(() => {
    const preloadedState = window.__PRELOADED_STATE__
    delete window.__PRELOADED_STATE__

    ReactDOM.hydrate(
      <BrowserRouter>
        <HelmetProvider>
          <Provider store={store(preloadedState)}>
            <App />
          </Provider>{" "}
        </HelmetProvider>
      </BrowserRouter>,
      document.getElementById("root")
    );
})

just the server

ssrIndex.js

require('ignore-styles')

require('@babel/register')({
    ignore: [/(node_modules)/],
    presets: ['@babel/preset-env', '@babel/preset-react'],
    plugins: [
        '@babel/plugin-transform-runtime',
        '@babel/plugin-proposal-class-properties',
        'babel-plugin-dynamic-import-node',
        '@babel/plugin-transform-modules-commonjs',
        '@loadable/babel-plugin',
    ],
})

// Import the rest of our application.
require('./SSR.js')

SSR.js

import React from 'react'
import express from 'express'
import ReactDOMServer from 'react-dom/server'
import {StaticRouter} from 'react-router-dom'
import {Provider} from 'react-redux'
import ServerStyleSheets from '@material-ui/styles/ServerStyleSheets'
import {HelmetProvider} from 'react-helmet-async'
import {ChunkExtractor, ChunkExtractorManager} from '@loadable/server'
import path from 'path'
import App from './App'
import store from './redux/store'
import template from './utils/template'

const PORT = 8080
const app = express()

const renderPage = (req, res, preload) => {
    const staticRouterContext = {}
    const helmetContext = {}

    const statsFile = path.resolve(__dirname, '../build', 'loadable-component.json')
    const extractor = new ChunkExtractor({statsFile})
    const sheets = new ServerStyleSheets()

    const html = ReactDOMServer.renderToString(
        sheets.collect(
            <ChunkExtractorManager extractor={extractor}>
                <HelmetProvider context={helmetContext}>
                    <StaticRouter location={req.url} context={staticRouterContext}>
                        <Provider store={store(preload)}>
                            <App />
                        </Provider>
                    </StaticRouter>
                </HelmetProvider>
            </ChunkExtractorManager>,
        ),
    )
    const {helmet} = helmetContext

    const wholeData = template('scripts', {
        chunks: html,
        helmet,
        extractor,
        sheets,
        preload,
    })

    res.send(wholeData)
}

const serverRenderer = (req, res, next) => {
    fetchSomeExternalData()
        .then(response => {
            // response.data is used as preloaded data and passed to the store of redux
            // also stored in a variable called __PRELOADED_STATE__ in window to use in client side
            // to populate store of redux
            renderPage(req, response, response.data)
        })
        .catch(err => {
            // start server side rendering without preloaded data
            renderPage(req, res)
        })
}

// each url that i want to render on the server side i should add here individually
// which is not so convenient
app.get('/', serverRenderer)
app.get('/my-url-1/', serverRenderer)
app.get('/my-url-2/', serverRenderer)

app.use(express.static(path.join(__dirname, '/../build/')))

// the * doesnt seem to work
app.get('*', serverRenderer)

app.listen(PORT, () => {
    if (process.send) {
        process.send('ready')
    }
})

for both client and server

App.js

<div>
    <Header/>
    <Router/>
    <Footer/>
</div>

i'd be happy to hear any suggestions or solutions. thank you for your time.

Keystone answered 4/2, 2021 at 16:44 Comment(3)
I don't think there is a solution to this problem at this moment. I've tried the same but ended up with same result. Because these external tools which we are trying to use to render it stream way but their internal implementation doest work in stream way at the moment.Fuscous
@zahra were you able to solve this? or at least find the root cause?Comprehension
@Calinortan unfortunately any solution i tried failed. the root cause seems to be the hydration that did not happen properly. in react 18 there are some changes specifically for rendering server-side which i haven't tried yet. what i did was to migrate to next.js which solved the blink by hiding the flash of unstyled content. maybe trying to hide the flash would be helpful but i didn't risk getting the source of the page later for that matter.Keystone

© 2022 - 2024 — McMap. All rights reserved.