React SSR two pass rendering with renderToString and renderToNodeStream - javascript

I'm trying to do SSR with ReactDOMServer.renderToNodeStream(element) but just wanted to know if there would be any problem with using both ReactDOMServer.renderToString(element) and ReactDOMServer.renderToNodeStream(element) at each request?
What I have in my custom SSR setup is:
* React 16
* react-loadable
* styled-components v4
* react-helmet-async
* Redux
* Express JS
Previously with React, I could easily render a HTML document by first rendering the <head></head> tags that contains markup produced by react-helmet and then using ReactDOMServer.renderToString() to render my React elements.
However, by switching to ReactDOMServer.renderToNodeStream() I had to switch react-helmet for react-helmet-async, which supports renderToNodeStream() function. But then when I try to render the <head></head> tags with the markup by react-helmet-async it'll come back as undefined.
To get around this problem, I've had to use renderToString() first without actually writing that out to Express JS response. That way react-helmet-async can then see what meta tags to render and then proceed to use renderToNodeStream and stream that out to the response.
I've simplified my code as much as possible as I want to understand if this would have a negative impact (for performance, or if anyone can think of anything else)?
Before:
let html = ReactDOMServer.renderToString(stylesheet.collectStyles(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<LocalStoreProvider store={store}>
<HelmetProvider context={helmetContext}>
<RouterContext {...renderProps} />
</HelmetProvider>
</LocalStoreProvider>
</Loadable.Capture>
));
const { helmet } = helmetContext;
response.write(
renderDocumentHead({
css: stylesheet.getStyleTags(),
title: helmet.title.toString(),
link: helmet.link.toString(),
meta: helmet.meta.toString()
})
);
response.write(html);
After:
let html = stylesheet.collectStyles(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<LocalStoreProvider store={store}>
<HelmetProvider context={helmetContext}>
<RouterContext {...renderProps} />
</HelmetProvider>
</LocalStoreProvider>
</Loadable.Capture>
);
// do a first pass render so that react-helmet-async
// can see what meta tags to render
ReactDOMServer.renderToString(html);
const { helmet } = helmetContext;
response.write(
renderDocumentHead({
css: stylesheet.getStyleTags(),
title: helmet.title.toString(),
link: helmet.link.toString(),
meta: helmet.meta.toString()
})
);
const stream = stylesheet.interleaveWithNodeStream(
ReactDOMServer.renderToNodeStream(html)
);
// and then actually stream the react elements out
stream.pipe(response, { end: false });
stream.on('end', () => response.end('</body></html>'));
Unfortunately, the only way I could get react-helmet-async to work correctly, I have to do this two-pass render. My CSS styles, etc. resolves correctly and the client renders/hydrates correctly too. I've seen other examples where react-apollo was used and the getDataFromTree data rehydration method was used which allows react-helmet-async to see what was needed to render the head markup. But hopefully there are no issues with my two-pass rendering approach?

Related

Compile a JSX string to a component on the fly

I've got a React application, in which I need to display an HTML Popup with some dynamic content generated by the server. This content also contains some JSX markup which I want to be rendered into real components.
<MyButton onClick={displayInfo}/>
...
async displayInfo() {
let text = await fetch(...)
console.log(text) // "<SomeComp onClick={foo}><OtherComp..... etc
let component = MAGIC(text)
ReactDom.render(component, '#someDiv')
}
Due to how my server app is structured, ReactServerDOM und hydration is not an option.
Is there a way to implement this on the client side only?
As others mentioned, this has code smell. JSX is an intermediary language intended to be compiled into JavaScript, and it's inefficient to compile it at run time. Also it's generally a bad idea to download and run dynamic executable code. Most would say the better way would be to have components that are driven, not defined, by dynamic data. It should be possible to do this with your use case (even if the logic might be unwieldy), though I'll leave the pros/cons and how to do this to other answers.
Using Babel
But if you trust your dynamically generated code, don't mind the slower user experience, and don't have time/access to rewrite the backend/frontend for a more efficient solution, you can do this with Babel, as mentioned by BENARD Patrick and the linked answer. For this example, we'll use a version of Babel that runs in the client browser called Babel Standalone.
Using Dynamic Modules
There needs to be some way to run the compiled JavaScript. For this example, I'll import it dynamically as a module, but there are other ways. Using eval can be briefer and runs synchronously, but as most know, is generally considered bad practice.
function SomeComponent(props) {
// Simple component as a placeholder; but it can be something more complicated
return React.createElement('div', null, props.name);
}
async function importJSX(input) {
// Since we'll be dynamically importing this; we'll export it as `default`
var moduleInput = 'export default ' + input;
var output = Babel.transform(
moduleInput,
{
presets: [
// `modules: false` creates a module that can be imported
["env", { modules: false }],
"react"
]
}
).code;
// now we'll create a data url of the compiled code to import it
var dataUrl = 'data:text/javascript;base64,' + btoa(output);
return (await import(dataUrl)).default;
}
(async function () {
var element = await importJSX('<SomeComponent name="Hello World"></SomeComponent>');
var root = ReactDOM.createRoot(document.getElementById('root'));
root.render(element);
})();
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Adding a Web Worker
For a better user experience, you'd probably want to move the compilation to a web worker. For example:
worker.js
importScripts('https://unpkg.com/#babel/standalone#7/babel.min.js');
self.addEventListener('message', function ({ data: { input, port }}) {
var moduleInput = 'export default ' + input;
var output = Babel.transform(
moduleInput,
{
presets: [
// `modules: false` creates a module that can be imported
["env", { modules: false }],
"react"
]
}
).code;
port.postMessage({ output });
});
And in your main script:
var worker = new Worker('./worker.js');
async function importJSX(input) {
var compiled = await new Promise((resolve) => {
var channel = new MessageChannel();
channel.port1.onmessage = (e) => resolve(e.data);
worker.postMessage({ input, port: channel.port2 }, [ channel.port2 ]);
});
var dataUrl = 'data:text/javascript;base64,' + btoa(compiled.output);
return (await import(dataUrl)).default;
}
(async function () {
var root = ReactDOM.createRoot(document.getElementById('root'));
var element = await importJSX('<div>Hello World</div>');
root.render(element);
})();
This assumes React and ReactDOM are imported already, and there's a HTML element with id root.
Skipping JSX and Compilation
It's worth mentioning that if you're generating JSX dynamically, it's usually only slightly more complex to instead generate what that JSX would compile to.
For instance:
<SomeComponent onClick={foo}>
<div id="container">Hello World</div>
</SomeComponent>
When compiled for React is something like:
React.createElement(SomeComponent,
{
onClick: foo
},
React.createElement('div',
{
id: 'container'
},
'Hello World'
)
);
(See https://reactjs.org/docs/jsx-in-depth.html for more information on how JSX gets compiled and https://babeljs.io/repl for an interactive website to compile JSX to JS using Babel)
While you could run Babel server-side to do this (which would add additional overhead) or you could find/write a pared-down JSX compiler, you also probably can also just rework the functions that return JSX code to ones that return regular JavaScript code.
This offers significant performance improvements client-side since they wouldn't be downloading and running babel.
To actually then use this native JavaScript code, you can export it by prepending export default to it, e.g.:
export default React.createElement(SomeComponent,
...
);
And then dynamically importing it in your app, with something like:
async function displayInfo() {
let component = (await import(endpointURL)).default;
ReactDom.render(component, '#someDiv');
}
As others have pointed out, Browsers don't understand JSX. Therefore, there's no runtime solution you can apply that will actually work in a production environment.
What you can do instead:
Write a bunch of pre-defined components in the front-end repository. (The JSX with the server).
Write a HOC or even just a component called ComponentBuilder will do.
Your <ComponentBuilder /> is nothing but a switch statement that renders one or more of those pre-defined components from step 1 based on what ever you pass to the switch statement.
Ask the server to send keywords instead of JSX. Use the server sent keyword by passing it to the switch statement. You can see the example below.
Now your server decides what to render. You don't need a runtime solution.
Stick the <ComponentBuilder /> where ever you want to render components decided by the server.
Most importantly, you won't give away a front-end responsibility into the hands of the backend. That's just "very difficult to maintain" code. Especially if you start to bring in styles, state management, API calls, etc.
// Component Builder code
const ComponentBuilder = () => {
const [apiData, setApiData] = useState()
useEffect(() => {
// make api call.
const result = getComponentBuilderDataFromAPI()
if (result.data) {
setApiData(result.data)
}
}, [])
switch (apiData.componentName) {
case 'testimonial_cp':
return <Testimonials someProp={apiData.props.someValue || ''} />
case 'about_us_cp':
return <AboutUs title={apiData.props.title || 'About Us'} />
// as many cases as you'd like
default:
return <Loader />
}
}
This will be a lot more efficient, maintainable and the server ends up deciding which component to render.
Just make sure not to mix up responsibilities. What is to be on the front end must be on the front end.
eg: don't let the server send across HTML or CSS directly. Instead have a bunch of predefined styles on the front-end and let the server just send the className. That way the server is in control of deciding the styles but the styles themselves live on the front-end. Else, it becomes really difficult to make changes and find bugs.
The server sends a bunch of CSS properties. You have a bunch of component styles. There are a bunch of global styles. You also use Tailwind or Bootstrap. You will not know what exactly is setting the style or even who's controlling it. A nightmare to debug and maintain! Saying this from experience.
The above solution should work for you provided you can make the required changes to your architecture! Because it's working in a production environment as I write this answer.

Can I give arguments to a react-app? (dependency injection with React)

I would like to use a React-App, and give it some arguments (props), depending on where I embed it.
So one step back, we are using React actually as a library, and not the entire page is in React. We have a very functioning website, and some parts are now being build in react. Our motivation is: If you want the same component in another page, you can simply copy-paste 3 lines which include the css and js files, put a <div id="myReactAppRoot"></div>, and that's it. Very quick, very clean, and instantly functioning.
My question
When I have another "copy" of the React app, I would like to give it a different starting state.
One example use-case: I have two pages, in one I want the data to be grouped by X and in the other grouped by Y.
Another use-case, which I will have on my next project: disable editng, depending on the users permission level (the permission level is known by the main page).
How can I achive this?
My current solution
My current solution is simply having a utils.js in the React app, which I use to access the windows object and get out the pieces I want:
const utils = {
fun1: window.myProject.forReact,fun1,
groupElements: window.myProject.forReact.groupElements,
permissionLevel: window.myProject.forReact.permissionLevel
};
export default utils;
Then importing utils in other components and using the functions/reading the values. And of course making those objects available on the main page.
But really, this feels wrong.
My ideal solution would look a lot like the Vue.js way:
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
Because I can just copy paste this bit, and change the Hello Vue!, and all the sudden the starting state is different in my "second copy" of it. Or even more cleverly, wrap it in a function that gets some initialState and put that JavaScript part in a file which I reference.
I have considered
I have considered editing the compiled JavaScript code that is being referenced. So far I only saw that the actual hooking into the myReactAppRoot-element is being done in the main.chunck.js, and editing that file seems too much like a hack (feels more wrong than my current solution).
To expand on Mike's comment, let's first translate your Vue app into React:
//html
<div id="root"></div>
//JS
const App = ({message}) => <div>{message}</div>
const rootElement = document.getElementById("root");
ReactDOM.render(
<App message={"Hello, React!"}/>
rootElement
);
A function that mounts an instance of App on a DOM node with id domId would be something like
function mountAppWithMessageOnNode(message, domId){
const rootElement = document.getElementById(domId);
ReactDOM.render(
<App message={message}/>
rootElement
);
}

react-intl, async translation import and SSR

I have a React app which uses react-intl to provide translations and it's working fine for browser DOM rendering. I have a component which deals with async locale/translation based on props it receives, using import() and then once locale + translation data are loaded, <IntlProvieder> is rendered. Like so:
// constructor
import(`react-intl/locale-data/${langCode}`).then((localeData) => {
addLocaleData(localeData.
this.setState({localeDataLoaded: true});
});
import(`../../translations/${locale}.json`).then((translations) => {
this.setState({translations: translations.default});
});
render() {
if (!this.state.translations || !this.state.localeDataLoaded) {
return null;
}
return (
<IntlProvider locale={this.props.locale} messages={this.state.translations} >
{this.props.children}
</IntlProvider>
);
}
However, when it comes to SSR, the setState() calls don't trigger render() thus blocking the rest of the app from rendering.
I'd like to know what others are doing with regards to i18n and server/DOM rendering. Ideally I'm not going to be sending extraneous locale data to users over the web and I'd like a single component to manage all of this for both renderers.
Alternatively I may end up baking translations into the output file and generating JS files per locale
For SSR you'd have to load things before rendering in the request lifecycle, then pass those down to react-intl

What's the difference between hydrate() and render() in React 16?

I've read the documentation, but I didn't really understand the difference between hydrate() and render() in React 16.
I know hydrate() is used to combine SSR and client-side rendering.
Can someone explain what is hydrating and then what is the difference in ReactDOM?
From the ReactDOMServer docs (emphasis mine):
If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.
The text in bold is the main difference. render may change your node if there is a difference between the initial DOM and the current DOM. hydrate will only attach event handlers.
From the Github issue that introduced hydrate as a separate API:
If this is your initial DOM:
<div id="container">
<div class="spinner">Loading...</div>
</div>
and then call:
ReactDOM.render(
<div class="myapp">
<span>App</span>
</div>,
document.getElementById('container')
)
intending to do a client-side only render (not hydration).
Then you end with
<div id="container">
<div class="spinner">
<span>App</span>
</div>
</div>
Because we don't patch up the attributes.
Just FYI the reason they didn't patch the attributes is
... This would be really slow to hydrate in the normal hydration mode and slow down initial render into a non-SSR tree.
I don't have anything specific to add to what's been said above about the use of hydrate, but in trying to learn about it I put together a little example, so here's the work for whoever finds it helpful.
Goal
Serve two pages, one which uses ReactDOM.hydrate and one which uses ReactDOM.render. They will depend upon some react components written in JSX, which are loaded by <script> tags, given artificial delay (by the server) to illustrate the difference between hydrate and render.
Basic Structure
One file which has the HTML "skeleton"
One file with the custom React components written in JSX
One script which generates all pages for the server to use
One script to run the server
Results
After I generate the pages and run the server, I go to 127.0.0.1 and am presented with the header hydrate, a button, and two links. I can click the button, but nothing happens. After a few moments, the document finishes loading and the button starts counting my clicks. Then I click on the "render" link. Now, the page I'm presented with has the header render and two links, but no button. After a few moments, the button appears and is immediately responsive.
Explanation
On the "hydrate" page, all the markup is immediately rendered, because all the necessary html is served with the page. The button is unresponsive because there are no callbacks connected yet. Once components.js finishes loading, the load event fires from the window and the callbacks are connected with hydrate.
On the "render" page, the button markup isn't served with the page, but only injected by ReactDOM.render, so it isn't immediately visible. Note how the appearance of the page is jarringly changed by the script finally loading.
Source
Here is the custom react component I am using. It will be used by the server in node with react to statically render components, and it will also be loaded dynamically from the server for use in pages (this is the purpose of checking for exports and React objects at the beginning of the file).
// components.jsx
var exports = typeof(exports) == 'object' ? exports : {};
var React = typeof(React) == 'object' ? React : require('react');
function MyButton(props) {
[click, setClick] = React.useState(0);
function handleClick() { setClick(click + 1); }
return (
<button onClick={handleClick}>Clicked: {click}</button>
);
}
exports.MyButton = MyButton;
This is the script used to generate all the pages required for the server. First, babel is used to transpile components.jsx into javascript, then these components are used, along with React and ReactDOMServer, to create the actual pages. These pages are created with the fuction getPage which is exported from the file pageTemplate.js, shown next.
// genScript.js
let babel = require('#babel/core');
let fs = require('fs');
let ReactDOMServer = require('react-dom/server');
let React = require('react');
let pageTemplate = require('./pageTemplate.js');
script = babel.transformFileSync(
'components.jsx',
{presets : [['#babel/react']]}
);
fs.writeFileSync('components.js',script.code);
let components = require('./components.js');
hydrateHTML = pageTemplate.getPage(
'MyButton',
ReactDOMServer.renderToString(React.createElement(components.MyButton)),
'hydrate'
);
renderHTML = pageTemplate.getPage(
'MyButton',
'',
'render'
);
fs.writeFileSync('hydrate.html',hydrateHTML);
fs.writeFileSync('render.html',renderHTML);
This file just exports the getPage function mentioned previously.
// pageTemplate.js
exports.getPage = function(
reactElementTag,
reactElementString,
reactDOMMethod
) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js" defer></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" defer></script>
<script src="./components.js" defer></script>
</head>
<body>
<h1>${ reactDOMMethod }</h1>
<div id="react-root">${ reactElementString }</div>
hydrate
render
</body>
<script>
window.addEventListener('load', (e) => {
ReactDOM.${ reactDOMMethod }(
React.createElement(${ reactElementTag }),
document.getElementById('react-root')
);
});
</script>
</html>
`;
}
Finally, the actual server
// server.js
let http = require('http');
let fs = require('fs');
let renderPage = fs.readFileSync('render.html');
let hydratePage = fs.readFileSync('hydrate.html');
let componentsSource = fs.readFileSync('components.js');
http.createServer((req, res) => {
if (req.url == '/components.js') {
// artificial delay
setTimeout(() => {
res.setHeader('Content-Type','text/javascript');
res.end(componentsSource);
}, 2000);
} else if (req.url == '/render.html') {
res.end(renderPage);
} else {
res.end(hydratePage);
}
}).listen(80,'127.0.0.1');
Hydrate is basically used in case of SSR(Server side Rendering). SSR gives you the skeleton or HTML markup which is being shipped from a server so that for the first time when your page loads it is not blank and search engine bots can index it for SEO(A use case of SSR). So hydrate adds the JS to your page or a node to which SSR is applied. So that your page responds to the events performed by the user.
Render is used for rendering the component on client side browser Plus if you try to replace the hydrate with render you will get a warning that render is deprecated and can't be used in case of SSR. it was removed because of it being slow as compared to hydrate.
In addition to above...
ReactDOM.hydrate() is same as render(), but it is used to hydrate(attach event listeners) a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup.
Using ReactDOM.render() to hydrate a server-rendered container is deprecated because of slowness and will be removed in React 17 so use hydrate() instead.
The entire process of putting functionality back into the HTML that was already rendered in server side React is called hydration.
So the process of re-rendering over the once rendered HTML is referred to as hydration.
So if we try to hydrate our application by calling ReactDOM.render() its supposed to be done by calling ReactDOM.hydrate().
render will flush out anything in the specified element(named as 'root' in most cases) and rebuild it ,while hydrate will keep anything that is already inside the specified element and build from that,making the initial page load faster.

How to handle child components with isomorphic React?

I have server code like this:
var data = {
scripts: scripts,
children:[<Comp1 />, <Comp2 />, <Comp3 />]
};
// keeping smaller for easier example than reality
var markup = '';
markup += '<script type="text/javascript">' +
'var PROPS = {' +
'Layout: ' + JSON.stringify(data) +
'};' +
'</script>';
markup += React.renderToString(
<Layout {...data} ></Layout>
);
So the server renders everything fine. Then no matter what I try, I get all sorts of warnings about how I'm handling the children for serialization and re-use in the browser when I run: React.render(App(window.PROPS.Layout), document.getElementById('content'));
Many of my attempts make React complain that I should be using createFragment. But when I do that, I still get errors that I should be wrapping it.
My goal is to render the Layout component, with several children, and have React in the browser know the same elements are below. Many attempts also yield this error:
Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) d=".1frk89jhyio.1"></div><div data-react
(server) d=".1frk89jhyio.1"><div style="float:lef
My client code is this:
var React = require('react'),
Layout = require('./components/layout');
React.render(<Layout {...PROPS.Layout} />, document.getElementById('content'));
You shouldn't stringify your components like that to serve for your client. You should just render you application again. Things to stringify should only be raw data like {id: 1, name: 'limelights'}.
On the server
React.renderToString(<Layout {...data} />);
and on the client
var data = JSON.parse(window.PROPS.data);
React.render(<Layout {...data} />, document.body);
The point of having an isomorphic application is that the same code runs on both the server and the client.
The answer is that I was doing things the theoretical wrong way.
Having the server render like <Layout {...data} ><Comp1 /></Layout> will work. However when the browser kicks in, it won't have children props and you'll lose Comp1. Then I had tried to pass children through in as props like the example in my question. The real way to do this is simply having the server do <Layout {...data} ></Layout>.
The real key to the answer is that the render method of Layout should be placing the children directly. I can use state (or props, still working out semantic differences) within render to determine if children should be different.

Categories