I need to hydrate a root element without actually importing the component in my client side javascript. I am rendering the component server-side, where each route will render a different full-page react component.
Here is my server.js:
import express from 'express'
import path from "path"
import React from "react"
import ReactDOMServer from "react-dom/server"
import About from "./about"
import Home from "./home"
const app = express()
const port = 3000
app.get('/', (req, res) => {
render(res, <Home />)
})
app.get('/about', (req, res) => {
render(res, <About />)
})
function render(res, component) {
res.send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>SSR App</title>
</head>
<body>
<div id="root">${ReactDOMServer.renderToString(component)}</div>
<script src="client.js"></script>
</body>
</html>
`)
}
app.use(
express.static(path.resolve(__dirname, "..", "public"))
)
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
You can see I am importing the Home and About components for each of their respective routes. These are full page react components.
Here is what home.js looks like, for example:
import React from "react"
export default function Home() {
const [times, setTimes] = React.useState(0)
return (
<div>
<h1>Home</h1>
<p>clicked {times} times</p>
<button onClick={() => setTimes((times) => times + 1)}>add</button>
</div>
)
}
Now here is the part I need to change. I have a client-side script, and right now, I am importing the Home component there. However, obviously this doesn't work when I visit the /about route, because I don't have hydration code for that particular component. I basically need to dynamically hydrate the root element based on the server-side route rendered html.
Example client.js:
import React from "react"
import ReactDOM from "react-dom/client"
import Home from "./home"
ReactDOM.hydrateRoot(document.getElementById("root"), <Home />)
What I need it to look like:
import React from "react"
import ReactDOM from "react-dom/client"
ReactDOM.hydrateRootDynamically(document.getElementById("root"))
Here are the scripts I am using:
"scripts": {
"client": "esbuild src/client.js --bundle --outfile=public/client.js --loader:.js=jsx",
"server": "esbuild src/server.js --bundle --outfile=public/server.js --loader:.js=jsx --platform=node",
"dev": "npm run client && npm run server && node public/server.js"
},
Like I said this currently works for the / home page, but I want to add more express routes that point to different react full-page components, and I don't want to have to create client-side hydration scripts for every single route.
How do I do this?
Related
I'm learning Next.js and I'm trying to integrate the #react-aria/overlays package in my project. I have a layout component, where I'm simply invoking the usePreventScroll method like this:
usePreventScroll({
isDisabled: true
});
This layout component is used in my _app.js.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import * as gtag from '../lib/gtag'
import 'styles/vendor.scss';
import 'styles/globals.scss';
import Layout from 'components/layout';
import { SSRProvider } from '#react-aria/ssr';
const App = ({ Component, pageProps }) => {
return (
<SSRProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</SSRProvider>
)
}
export default App;
When going to my browser and loading a page, it gives me the following error:
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
at Layout (/home/bas/projects/test-website/build/server/pages/_app.js:718:3)
at div
at $c5f9596976ab8bd94c5879001549a3e$var$OverlayContainerDOM (/home/bas/projects/test-website/node_modules/#react-aria/overlays/dist/main.js:864:7)
at ModalProvider (/home/bas/projects/test-website/node_modules/#react-aria/overlays/dist/main.js:810:5)
at OverlayProvider
at SSRProvider (/home/bas/projects/test-website/node_modules/#react-aria/ssr/dist/main.js:33:13)
at UIContextProvider (/home/bas/projects/test-website/build/server/pages/_app.js:1144:74)
at ManagedUIContext (/home/bas/projects/test-website/build/server/pages/_app.js:1105:3)
at App (/home/bas/projects/test-website/build/server/pages/_app.js:5171:3)
at AppContainer (/home/bas/projects/test-website/node_modules/next/dist/next-server/server/render.js:23:748)
What's the problem here and how would I be able to solve it?
I tried wrapping the the Layout component in the packages <SSRProvider>.
You can dynamically load the component and disable SSR:
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The code example has been taken from the NextJS docs. If that's not your thing, you can call the hook or render the component as long as processs.browser is true.
Next js is computes your 1st page on server. so it does not understand browser scroll or localstorage or other browser api.
you can add a check in your code block if window object is present or execution is running in server and then execute usePreventDefault.
import {useIsSSR} from '#react-aria/ssr';
function Layout() {
let isSSR = useIsSSR();
useEffect(() => {
!isSSR && usePreventScroll({ ... })
}, [isSSR])
}
I want to try Server Side Render in React, but my code doesn't work.
I should be missing something. But I don't know what it is as I'm new to react.
Here's the code as well:
server/app.js
import express from 'express';
import { renderToString } from 'react-dom/server';
import Home from '../src/index.js';
const app = express();
const content = renderToString(<Home />);
app.get('/', function (req, res) {
res.send(
`
<html>
<head>
<title>ssr</title>
</head>
<body>
<div id="root">${content}</div>
</body>
</html>
`
);
})
app.listen(3001, () => {
console.log('listen:3001')
})
src/index.js
import React from 'react';
const Home = () => {
return (
<div>
<div>Hello World</div>
</div>
)
}
export default Home
package.json
{
"scripts": {
"start": "nodemon --exec babel-node server/app.js"
}
}
.babelrc
{
"presets": [
"env"
],
"plugins": []
}
You can not render react page from nodejs like ejs because react is not like any template. it is a library.But if you actually want it, then you need server side rendering. (SSR)
const content = renderToString(<Home />);
you are using jsx here but not transpiling jsx into js. you need to install
npm i #babel/preset-react
also instead of "babel-preset-env" use this
npm i #babel/preset-env
then
{
"presets": ["#babel/preset-react", ""#babel/preset-env]
}
I created a project with React, react-router, #loadable/component.
Now I'm trying to add SSR to this project.
I did server side rendering with react-router.
And then I added #loadable/component to import all pages component:
import loadable from '#loadable/component';
const routersConfig = [
{
path: '/',
component: loadable(() => import('./Home')),
exact: true,
},
{
path: '/2',
component: loadable(() => import('./Home2')),
exact: true,
},
];
Then I added all this parts of code: https://www.smooth-code.com/open-source/loadable-components/docs/server-side-rendering/
And now it works.
But It works with the problem: a content blinks while loading.
How I understand the page's loading process:
Browser gets a content generated by SSR (the first query in network tab)
Browser renders a content (with left and top margins )
Browser downloads two enterpoints and vendors from html (app.js, normalizer.js, vendor.js)
Browser executes app.js and normalizer.js. Left and top margins are removed.
App.js starts downloading page's chunk - home.js. In this moment content disappears
When home.js is downloaded, the content appears again.
I shoot a video to illustrate this process. (I'm sorry for quality, stackoverflow forbides files which size is more then 2MB ). I'm throttling network speed to imagine all page's download process.
My question is why the content disappears? How to fix it?
My code
server.js
const sheetStyledComponents = new ServerStyleSheet();
const sheetsJssRegistry = createSheetsRegistry();
const statsFile = path.resolve(process.cwd(), './build-ssr/dist/loadable-stats.json');
const extractor = new ChunkExtractor({
statsFile,
entrypoints: [
'app',
'normalize',
],
});
try {
const client = ApolloSSRClient();
const tree = (
<ApolloProvider client={client}>
<ApplyTheme sheetsRegistry={sheetsJssRegistry}>
<StaticRouter location={req.url}>
<Home />
</StaticRouter>
</ApplyTheme>
</ApolloProvider>
);
// there is combination of Apollo graphql, jss, styledComponent functions
const body = await getMarkupFromTree({
renderFunction: flow(
sheetStyledComponents.collectStyles.bind(sheetStyledComponents),
extractor.collectChunks.bind(extractor),
renderToString
),
tree,
});
const scriptTags = extractor.getScriptTags();
// It isn't used yet
const linkTags = extractor.getLinkTags();
const styleTags = sheetStyledComponents.getStyleTags();
const html = (await rawHtml)
.replace(
'</head>',
`
${styleTags}
<style type="text/css" id='jss-server-side-styles'>
${sheetsJssRegistry.toString()}
</style>
<script>
window.__APOLLO_STATE__ = ${JSON.stringify(client.extract())};
</script>
${scriptTags}
</head>
`
)
.replace('<div id="app"></div>', `<div id="app">${body}</div>`);
res.send(html);
index.jsx
const SSRApp = (
<ApolloProvider client={ApolloClient}>
<ApplyTheme>
<BrowserRouter>
<App />
</BrowserRouter>
</ApplyTheme>
</ApolloProvider>
);
loadableReady(() => (
ReactDOM.hydrate(
SSRApp,
document.getElementById('app'),
)
));
It was my fault.
The hydration version of app contained BrowserRouter -> Switch -> Router -> HomePage
And the SSR version contained only StaticRouter -> HomePage
Because of this, after rendering SSR version, react removed all DOM and created new one with Router.
i changed in server.js. its worked for me
yours maybe (server/index.js or server.js or server/app.js..etc)
import Express from 'express';
import Loadable from 'react-loadable';
// from //
app.listen(3000, () => {
console.log('app now listening on port', port);
});
// to //
import Loadable from 'react-loadable';
Loadable.preloadAll().then(() => {
app.listen(port, () => {
console.log('app now listening on port', port);
});
});
for more config understanding you can see
The first step to rendering the correct content from the server is to make sure that all of your loadable components are already loaded when you go to render them.
To do this, you can use the Loadable.preloadAll method. It returns a promise that will resolve when all your loadable components are ready.
I'm trying to render react isomorphicaly, and it renders but I get the Warning/Error in client saying:
I'm using jspm and npm as package managers;
warning.js:25 Warning: render(...): Replacing React-rendered children with a new root component. If you intended to update the children of this node, you should instead have the existing children update their state and render the new components instead of calling ReactDOM.render.
The source outputed by server renderToString:
<div id="reactRoot"><div data-reactroot="" data-reactid="1" data-react-checksum="845161664"><marquee data-reactid="2"><h1 data-reactid="3">App</h1></marquee></div></div>
The source replaced by react after render:
<div id="reactRoot"><div data-reactid=".0"><marquee data-reactid=".0.0"><h1 data-reactid=".0.0.0">App</h1></marquee></div></div>
The express server middleware:
import App from './components/App/App.jsx';
// [...] Express code removed for brevity
app.use('*', (req, res, next) => {
try {
res.render('index.html', {
reactHtml: renderToString(
<App />
)
});
} catch (err) {
next(err);
}
});
The index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>App</title>
<script>
console.log('Running on <%- #env %> environment!');
<% if (#env == 'development') : %>
System.import('systemjs-hot-reloader/default-listener.js');
<% end %>
System.import('/app.jsx!')
.catch(console.error.bind(console));
</script>
</head>
<body>
<div id="reactRoot"><%- reactHtml %></div>
</body>
</html>
I'm using ect as templating engine here;
The app.jsx:
import { render } from 'react-dom';
import App from './components/App/App.jsx';
const mountPoint = document.getElementById('reactRoot');
render(
<App />,
mountPoint
);
The App/App.jsx:
import React from 'react';
const App = () => (
<div>
<marquee><h1>App</h1></marquee>
</div>
);
export default App;
Use renderToStaticMarkup
// render the dynamic code of the page to a string.
var appHtml = React.renderToString(<Application/>);
// now use React as a static templating language to create the <html>,
// <head>, and <body> tags
var fullPageHtml = React.renderToStaticMarkup(<HtmlPage content={appHtml}/>);
res.write(fullPageHtml);
Full Issue resolution discussion can be found here.
https://github.com/aickin/react-dom-stream/issues/4
I'm following the example reported in the react-router guide
var App = React.createClass({
render: function () {
return <div>Hi</div>;
}
});
var routes = (
<Route handler={App} path="/" />
);
// if using express it might look like this
app.use(function (req, res) {
// pass in `req.url` and the router will immediately match
Router.run(routes, req.url, function (Handler) {
var content = React.renderToString(<Handler/>);
res.render('main', {content: content});
});
});
Quite simple, isn't it? Instead this is my code:
export function index(req: express.Request, res: express.Response, next: Function) {
Router.run(routes, req.url, (Handler, state) => {
fs.readFileAsync(
path.join(__dirname, '..', 'mockData/airport-codes.csv'),
'utf-8'
).then((content) => {
return csv.parseAsync(content);
}).then((parsedContent: Array<string[]>[]) => {
ResponseHelper.renderTemplate('index', res, {
output: React.renderToString(React.createElement(Handler, {
header: [
"ID", "Type", "Name", "Latitude (deg)", "Longitude (deg)", "Elevation", "Continent", "Country ISO", "Region ISO", "Municipality", "GPS Code", "IATA Code", "Local Code"
],
initialData: parsedContent
}))
});
}).catch(next);
});
}
Basically what I do is get data and pass to the Handler to initialise the component. This is the file route in react:
import { Route } from "react-router";
import * as React from "react";
import Excel from "../components/Excel";
export default (
<Route handler={Excel} path="/" name="excel" />
);
and this is the entrypoint in the frontend
import * as React from "react";
import * as Router from "react-router";
import routes from "./shared/routes";
Router.run(routes, Router.HistoryLocation, (Root, state) => {
React.render(<Root />, document.getElementById('app'));
});
My problem is that when I start the application I get this warning:
Warning: Failed propType: Required prop `header` was not specified in `Excel`. Check the render method of `Router`.
Warning: Failed propType: Required prop `initialData` was not specified in `Excel`. Check the render method of `Router`.
and consequently an error. Do you know which is the right way to pass props in this case?
EDIT
error message
Uncaught SyntaxError: Unexpected token &
1../components/Excel # bundle.js:4s # bundle.js:1e # bundle.js:1(anonymous function) # bundle.js:1
The lifecycle of the isomorphic app like yours is that first, the server runs the JS code and return an HTML string expression of the rendered result. It mounts that to the DOM and then loads your frontend app on top of that. Of course the DOM should be identical so it doesn't need to really rerender anything, but it does mount all of the JS event handlers, React code, etc to the window.
It will still process the Router.run method and match the routes, but it will be the same page that was loaded from the server. However, after that point, the front-end has to replicate the server side functionality (ie fetching the data, passing to the routed component).
So, in your frontend code you need to fetch the required data that you would normally do on the server, and pass it down to the <Root /> component as props.
Check out this demo, specifically the app/server.js (server side render) and app/client.js (client side render). Hope this helps!
React Router Mega Demo