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.
Related
I have created an app using create-react-app with a basename '/admin' and it is working just fine in development mode. All routes working properly both on localhost and behind nginx proxy.
When I build the app using npm run build I get a blank screen on the '/admin' url, with the following errors in the console:
The script from “https://192.168.1.2/admin/static/js/main.49bb4878.js”
was loaded even though its MIME type (“text/html”) is not a valid
JavaScript MIME type.
The stylesheet https://192.168.1.2/admin/static/css/main.4efb37a3.css
was not loaded because its MIME type, “text/html”, is not “text/css”.
Uncaught SyntaxError: expected expression, got '<' main.49bb4878.js:1
I have tried both <BrowserRouter pathname="/admin">...</BrowserRouter> and the one I have in the following index.js file.
It seems like the server sends the index.html file no matter what the client requests...
This is my index.js file:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import { configureStore } from '#reduxjs/toolkit';
import storeToConfigure from './configureStore';
import CustomRouter from './utils/CustomRouter';
import * as buffer from 'buffer';
import { BrowserRouter } from 'react-router-dom';
console.log(window);
window.Buffer = buffer;
window.process = {}
export const store = configureStore(storeToConfigure);
export const history = createBrowserHistory({ basename: '/admin' });
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<CustomRouter history={history} basename="/thug-app">
<ScrollToTop>
<App />
</ScrollToTop>
</CustomRouter>
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint.
reportWebVitals(console.log);
This is the CustomRouter I'm using, in order to be able to directly access the history anywhere without a hook:
import React, { useLayoutEffect, useState } from 'react';
import { Router } from 'react-router-dom';
const CustomRouter = ({
basename,
children,
history,
}) => {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
};
export default CustomRouter;
Again, everything works just fine in development. The problem is when I build the app for production. I have tried both pm2 and serve packages (same names on npmjs). The serve package returns 404, while pm2 returns the errors I mention above.
Thank you for taking the time to help!
I managed to make it work with this workaround:
I ditched both pm2 and serve and used a custom express.js server.
The setup:
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
// app.use(express.static(path.join(__dirname, 'build')));
app.get('/*', (req, res) => {
let theUrl = req.originalUrl.replace('/admin', '');
if (!!!theUrl) {
theUrl = 'index.html';
}
try {
const target = path.join(__dirname, 'build', theUrl);
console.log(target);
if (fs.statSync(target)) {
res.sendFile(target);
} else {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
}
} catch (error) {
console.log(error);
res.sendFile(path.join(__dirname, 'build', 'index.html'));
}
});
app.listen(4000, () => console.log('admin app listening on 4000'));
It also seems to work fine without the following line:
app.use(express.static(path.join(__dirname, 'build')));
Like I had noticed, both pm2 and serve sent the index.html file no matter what the client requested.
Seems to be working fine behind nginx proxy as well.
Whenever the requested file does not exist, simply serve the index.js file.
I have attempted to make a react SSR website using create-React basics through the "razzle" helper tool.
the app originally worked with this:
'use strict'
const functions = require('firebase-functions');
const app = require('./server/build/server.bundle.js').default;
exports.app = functions.https.onRequest(app)
but when I changed it to this:
'use strict'
const functions = require('firebase-functions');
const App = require('./server/build/server.bundle.js').default;
const React = require('react')
const express = require('express');
const fs = require('fs');
const app = express();
const {RenderToString} = require('react-dom/server');
const index = fs.readFileSync(__dirname +'/index.html', 'utf8');
app.get ('**', (req, res) => {
const html = RenderToString(App);
const finalHtml = index.replace('<!-- ::APP:: -->', html)
res.set('Cache-Control', 'public, max-age=600, sMax-age=1200');
res.send(finalHtml);
})
exports.shell = functions.https.onRequest(app);
instead of the website i got
An application is requesting permission to access your Google Account.
Please select an account that you would like to use.
and upon attempting entry with the firebase admin gmail account:
Error: Forbidden
Your client does not have permission to get URL /app/ from this server.
What have I done wrong, and how do I fix this?
The original function which worked for SSR was removed, and the new one exports.shell was added sucessfully.
IF the function has problems within it that are not adressed through processes such as eslint, then it will cause the permission webpage instead of your natural webpage.
The problem here is that using the Razzle package you have the "renderTOSTring" placed in another file.
If the thing worked with no "renderTOString" there using react it will not when it is placed in there.
To adjust the razzle config so that you can both add in "firebase caching" and adjust the routing. ou need to go to the "src/server.js file generated by razzle.
TO add caching and routing it should look like this:
import App from './App';
import React from 'react';
import express from 'express';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const server = express();
server
.disable('x-powered-by')
.use(express.static(process.env.RAZZLE_PUBLIC_DIR))
.get('/*', (req, res) => {
const markup = renderToString( <StaticRouter
location={req.url}
context={context}
>
<App /></StaticRouter>);
res.set('Cache-Control', 'public, max-age=300, s-maxage=1600');
res.send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charSet='utf-8' />
<title>Welcome to Razzle</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
${assets.client.css
? `<link rel="stylesheet" href="${assets.client.css}">`
: ''}
${process.env.NODE_ENV === 'production'
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${assets.client.js}" defer crossorigin></script>`}
</head>
<body>
<div id="root">${markup}</div>
</body>
</html>`
);
});
export default server;
with the changes being here :
<StaticRouter
location={req.url}
context={context}
>
<App /></StaticRouter>);
res.set('Cache-Control', 'public, max-age=300, s-maxage=1600');
You can be forwarded to the same page with message "App requesting permission to access your google account" when your Firebase Function is deployed to another region than "us-central1", because:
Important: Firebase Hosting supports Cloud Functions in us-central1
only.
The App was initially made with CRA and converted into SSR. The app works fine but the problem arises only when I refresh the app or manually type in the URL to a certain page. The middle div's classname is then overridden to the classname of the middle div of the base route.
It changes for example from this:
<div id="root">
<div class="header">..</div>
<div class="newlist">..</div>
<div class="footer">..</div>
</div>
to this:
<div id="root">
<div class="header">..</div>
<div class="main-flex-container">..</div>
<div class="footer">..</div>
</div>
For extra information here is my src/index.js:
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import configureStore, { history } from './redux/store/index';
import AppRouter from './routers/AppRouter';
import './styles/styles.scss';
const store = configureStore();
require('dotenv').config();
const isServer = typeof window !== 'undefined';
if (isServer) {
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
}
and here is my server.js:
import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';
import configureStore from '../src/redux/store/index';
import AppRouter from '../src/routers/AppRouter';
import { StaticRouter } from 'react-router-dom';
const app = express();
const store = configureStore();
app.use(express.static(path.resolve(__dirname, '..', 'build')));
app.use('/*', (req, res, next) => {
fs.readFile(path.resolve('./build/index.html'), 'utf-8', (err, data) => {
if (err) {
console.log(err);
return res.send(500).send('Error happened!');
}
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={{}}>
<AppRouter />
</StaticRouter>
</Provider>,
)}</div>`,
),
);
});
});
app.listen(3000, () => {
console.log('Listening...');
});
In case this isn't enough information, here is the link to the project on GitHub.
So the crux of your problem here seems to be that when rendered on the server, the router isn't rendering the correct component based on the url as we would expect.
What is happening is not that the class name is changed, instead we are actually always rendering the root page and then react replaces the content with the "new task" page on the client before you get a chance to have a look at it in the inspector.
Summary: When using app.use with a path in express, we are mounting an express middleware at the specified path, and express removes the "mount point" from the req.url provided to the middleware. Among other solutions, we can use req.originalUrl to get the actual url. req.originalUrl is a property provided by express that gives the url as it was before any middleware changed it.
Let's investigate!
If we extract the code that renders your app to a separate variable and log that, we will see that the logged HTML string is the page with the "New List" button on it.
const renderedApp = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={{}}>
<AppRouter />
</StaticRouter>
</Provider>,
);
console.log('Rendered app', renderedApp);
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${renderedApp}</div>`,
),
);
Visiting localhost:3000/l/new and looking in the log:
Rendered app <div class="header"><a class="header__button" id="header__colabico" href="/">COLABI.CO</a><div class="header__right"><button class="header__button" id="header__tweet">TWEET</button><button class="header__button" id="header__logout">LOGOUT</button></div></div><div class="main-flex-container"><a class="home__button" href="/l/new"> <!-- -->NEW LIST<!-- --> </a><p class="home__infotext"> <!-- -->Start by pressing that big button up there!<!-- --> </p></div><div class="footer"><div class="footer__left"><a class="footer__button" href="/privacy">PRIVACY</a><a class="footer__button" href="/terms">TERMS</a></div><p class="footer__colabico"> © colabi.co</p></div>
This looks very much like the root page instead of the new list page.
Aside: Putting the rendering on a separate line instead of inline in the template string also helps with syntax highlighting, as a nice bonus.
Aside 2: By adding this logging you'll also see that your code is not run when hitting the root url. The reason for this is probably because of the static middleware at the start, which will match on the index.html page in your build folder.
Now that we are fairly certain what the problem is, let's try to see why it might be.
The StaticRouter accepts the location as a prop, and we are passing in req.url for that. Let's verify that this is what we think it is.
console.log('URL:', req.url);
When visiting localhost:3000/l/new, this is logged:
URL: /
That is certainly not what we expected!
I went to look up the documentation for req.url to see what the problem might be, and found this method:
req.originalUrl
I couldn't see the documentation for req.url, but there's a helpful box here explaining why that is (req.url comes from Node's http module).
Logging req.originalUrl too just to see what that is for us, and sure enough:
original URL: /l/new
URL: /
req.originalUrl has what we need!
At this point I searched around for references to explain why req.url might be / in our case, and it turns out it's because of this:
When we are using app.use, we are mounting an express middleware at the specified path, rather than setting up a route handler (that would be app.get or similar), and middlewares remove the path they are mounted at from their own url, because they consider that their root. When we specify the middleware to be mounted at /*, the wildcard matches all routes and the whole url is removed.
In other words, we can fix this in a few different ways:
Keep it as it is, but use req.originalUrl instead
Remove the path from app.use (app.use((req, res, next) => {/* your code */}))
Change from app.use to app.get
Hope that helps!
I want to implement react-helmet on Meteor. The client side works fine and I can see the tags on the Inspect Element - but how to render it on server side for SEO? I do not understand the documentation.
ReactDOMServer.renderToString(<Handler />);
const helmet = Helmet.renderStatic();
My Code is as follows
index.html (client)
<head>
<body>
<div id="target" />
</body>
</head>
main.js (client)
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import { renderRoutes } from '../imports/startup/client/routes.jsx'
Meteor.startup(() => {
render(renderRoutes(), document.getElementById('target'));
});
I'm using React-Router to render the element to the "target"
main.js (server)
How to get the tags from the server.
import { onPageLoad } from 'meteor/server-render';
Meteor.startup(() => {
onPageLoad((sink) => {
const helmet = Helmet.renderStatic();
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
});
});
On the about code helmet.meta.toString() returns empty. I know we need to pass something to let the helmet know the meta tags. But how to do that
Can someone help me understand what I need to write on the server to get this working? Everything except that is working fine.
Finally I found the solution :
The code on the main.js (server) should be the following
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import {renderRoutes} from '../imports/startup/both/routes.jsx'
// ...
onPageLoad((sink) => {
const context = {};
const app = ReactDOMServer.renderToString(
<StaticRouter location={sink.request.url} context={context}>
{renderRoutes()}
</StaticRouter>
);
sink.renderIntoElementById('react-root', app);
const helmet = Helmet.renderStatic();
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
});
Hope it helps someone who might run into the same issue in the future. :)
I have an app where I configured server side rendering. Everything is working nice and my component is rendered on the server. The problem is that I get my component rendered twice on the screen. One comes from <div id="content"><%- content %></div>that I am using for server rendering and one comes from <script src="http://localhost:3001/bundle.js"></script>. I use webpack to make two bundles for my server and client. Why is this happening and how can I fix this?
views/index.ejs
<body>
<div id="app"></div>
<div id="content"><%- content %></div>
<script src="http://localhost:3001/bundle.js"></script>
</body>
index.js
app.use(Express.static(path.join(__dirname, '../', 'dist')))
app.use(serverRenderer)
app.get('*', (req: Object, res: Object) => {
res.render('index', {content: req.body})
})
serverRender
import React from 'react'
import ReactDOM from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from '../client/routes.js'
async function render (component) {
const content = ReactDOM.renderToString(component)
return content
}
async function getMatchParams (routes, currentUrl) {
return new Promise((resolve, reject) => {
match({routes: routes, location: currentUrl}, (err, redirect, props) => {
if (err) {
return reject(err)
}
return resolve(props)
})
})
}
export default async(req, res, next) => {
const renderProps = await getMatchParams(routes, req.url)
if (renderProps) {
const component = (
<RouterContext {...renderProps} />
)
req.body = await render(component)
next()
}
}
Ok. I have found a problem. I was referring to the bundle and server rendered string with two separate <div>. Inside my app.js I was doing this
render(
<Router history={browserHistory}>
{routes}
</Router>,
document.getElementById('app')
)
Thats why I should have been sending the string to the template like this.
app.use(Express.static(path.join(__dirname, '../', 'dist')))
app.use(serverRenderer)
app.get('*', (req: Object, res: Object) => {
res.render('index', {app: req.body})
})
And finally my views/index.js should look like this
<body>
<div id="app"><%- app %></div>
<script src="http://localhost:3001/bundle.js"></script>
</body>
I also faced that problem and found a solution.
On package.json,
"start": "npm-run-all --parallel dev:*",
It will run webpack and node build/bundle.js.
Then 2 things happened simultaneously, webpack build project, node build/bundle.js
After webpack built project, then node build/bundle.js runs again since bundle.js is changed.
So there was twice calls on both server and client side. I solved this problem very easily.
First run npm run build, then run node build/bunde.js . Then it will run everything once :)