The application I'm working on is based on React Fiber and React Router V3.
Trying to use hydrate() instead of render() with async components I've faced with the following issue: HTML returned from SSR is different from client-side one.
As a result React remounts the whole DOM and throws the following warning: Did not expect server HTML to contain....
React Training does not provide solution as well: Code-splitting + server rendering
Is there any solution to achieve this?
Updates:
Simple Example
(pseudo code)
App.js:
export default () => <div>Lorem Ipsum</div>;
client.js:
const createRoutes = store => ({
path: '/',
getComponent(nextState, cb) {
require('./App'); // some async require
},
onEnter: (nextState, replace, cb) => {
store.dispatch(fetchData())
.then(() => cb())
.catch(cb);
}
});
match({history, routes: createRoutes(store)},
(error, redirectLocation, renderProps) => {
hydrate(
<Router history={history} routes={createRoutes(store)} />,
document.getElementById('app')
);
});
server.js
match({routes: createRoutes(store), location: req.url},
(err, redirectLocation, renderProps) => {
const content = renderToString(<RouterContext {...renderProps}/>);
// send content to client
});
I've been investigated the issue a bit more deeper and found the solution.
To achieve DOM hydration the following point should be token in account:
I the example above in client.js I invoked createRoutes(store) twice. This is redundant because renderProps already has routes property prepared for <Route /> component. Due to this mistake onEnter was called twice, so data fetching was performed twice too.
To avoid HTML mismatch on server and client side data fetching in onEnter should not be called on the first client-side render.
match function waits for getComponent callback is performed before render. So the main question is wrong, because this functionality is available out of the box.
Related
I'm trying to understand loaders in react-router#6.4. How and why is a BrowserRouter doing a DB call? Is this just a contrived example and this is meant to be a client db call for illustration purposes or a there some undocumented server activity taking place here?
https://reactrouter.com/en/main/route/loader
createBrowserRouter([
{
element: <Teams />,
path: "teams",
loader: async () => {
return fakeDb.from("teams").select("*");
},
children: [
{
element: <Team />,
path: ":teamId",
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
are react router 6.4 loaders called on the server, client or both?
The loaders are called on the client. react-router-dom mainly handles client-side routing. If you are using server-side-rendering then the suggestion from RRD is to use Remix.
I'm trying to understand loaders in react router 6.4+. How and why is
a browserRouter doing a DB call?
The "how" is trivial, the loader functions are callback functions that are run prior rendering the routed element. The "why" is so the app can fetch data, validate a user, submit analytics/metrics, etc, when a route is being loaded/accessed. What you want or need to do prior to loading the routed component is really up to your specific use cases.
createBrowserRouter([
{
element: <Teams />,
path: "teams",
loader: async () => {
return fakeDb.from("teams").select("*");
},
children: [
{
element: <Team />,
path: ":teamId",
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
Is this just a contrived example and this is meant to be a client db
call for illustration purposes or a there some undocumented server
activity taking place here?
Yes, this is a completely contrived code example for illustration purposes, no undocumented activity. "fakeDb" is some client-side database client that is selecting all the tuples from a "teams" collection and returning it to the Teams component, accessible via the useLoaderData hook. fetch is a standard Javascript API to asynchronously fetch data.
In your example both async functions are making calls to some backend. fakeDb seems to target some sort of SQL server via library/sdk. Fetch is a typical method to call a backend api endpoint (on some server).
The router itself may make calls to the server to get the page content, but it may also be cached in the browser if recently access.
All the code in the snippet is executed in the browser, but as mentioned it does ask a server to do something. But what the server does, that code is somewhere else.
I have an issue with how my React Redux SSR application is handling site navigation I have a route for list pages which will display different data depending on the params in the URL.
Routes.js file
export default [
{
...App,
routes: [
{
...HomePage,
path: '/',
exact: true
},
{
...ListPage,
path: '/list/:id',
exact: true
},
In my Index.JS file where my express backend is running I iterate through my routes directory to see the path(s) that matches the request path...
const app = express();
app.use(express.static('public'));
app.get('*', (req, res) => {
const store = createStore(req);
const promises = matchRoutes(Routes, req.path)
.map(({ route }) => {
console.log("Looking at Route: ", route);
if (route.loadData) {
const params = req.path.split('/');
console.log('my params are: ', params)
return route.loadData(store, params[2])
}else{
return null
}
})
.map(promise => {
if (promise) {
return new Promise((resolve, reject) => {
promise.then(resolve).catch(resolve);
});
}
});
Promise.all(promises).then(() => {
const context = {params: req.params};
const content = renderer(req, store, context);
if (context.url) {
return res.redirect(301, context.url);
}
if (context.notFound) {
res.status(404);
}
res.send(content);
});
});
My understanding is that there should only be 2 things to iterate over, the App component Route, and the ListPage component Route it then calls their respective loadData() functions and the websites continues to run. However after it goes through the first 2 routes and populates my page with the relevant information the Index.js file gets called again and iterates through the routes but this time instead of having the URL that the user is trying to access it replaces it with "bundle.js" and I don't understand what's going on here. This is the output I get I would love to only have the top half of the output.
NOTE this image is taken from my console (I've combined both the client and server side output in 1 window) below I'll include a screenshot of my config Files
Of course my code wasn't expecting this as a path and the application breaks because it's trying to get information on a list with the ID of "bundle.js" instead of a standard number.
Question can someone explain to me what my codes doing wrong here or if this is how it's supposed to behave how I work around this I'd greatly appreciate it.
I'm currently trying to create my first SSR application so I'm new to this technology so I might be missing something obvious.
Upon further investigation I noticed that the file bundle.js that I could see in the console was referring to a file at location /list/bundle.js but my bundle was actually in my public directory so I had to modify the script Src so that it would refer to the http://localhost:3000/bundle.js after I did this app functioned how It was supposed.
After a post request to an external API, I would like to redirect back to the homepage. I do have some knowledge with React and this is my first time using Next.js. Here is the code:
export default function New({genres}) {
const createMovie = (values) => {
console.log(values);
axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/movies`, {
title: values.title,
description: values.description,
genres: values.genres,
release_date: values.release_date,
cover_url: values.cover_url
}).then(res => {
const router = useRouter();
router.push('/');
})
}
As you can see I used router.push() but I get this error:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
What is the most efficient way to redirect to other pages in Next.js after a function and/or requests?
You need to move where you call useRouter(). You can keep router.push() where it is.
export default function New({genres}) {
const router = useRouter();
const createMovie = (values) => {...}
}
If you look at the Rules of Hooks, you can only call the hook, useRouter() in this case, at the top level.
I also had my initialization of useRouter in my function. I fixed the same bug by placing that line into my function component instead of my function and calling router.push(...) in the function itself.
I currently have a website built using EJS and using express on the backend on port 3300. The structure of the routes look like this:
localhost:3300
-/movies
-/rating
-/review
-/tvshows
-/rating
-/review
I am currently returning EJS files in the routes like this:
router.get("/:title/rating", function(req, res) {
Movie.find({ movieName: req.params.title })
.exec(function(err, foundMovie) {
if (err) {
console.log(err);
} else {
console.log(foundMovie)
res.render("movie/rating", { movie: foundMovie});
}
});
});
But now, I want to add a new route in the structure that uses React such that the following will be built using React:
localhost:3300
-/documentary
-/rating
-/review
From my understanding, if I want to use React, I would have to re-write all my previous routes (that returns EJS) as React components since you can't merge the two servers (React and Express as they both run on different ports: 3000 and 3300 respectively). But since I have so much written already I tried to render the new routes on the serverside by following this tutorial resulting in:
router.get("/documentary", (req,res) => {
Documentary.find({}).exec(function(err,foundDoc){
if (err){
console.log(err);
} else {
fs.readFile(path.resolve("./views/react/client/build/index.html"), "utf-8", (err, data) => {
if (err) {
return res.status(500).send("Error");
}
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
);
});
}
})
});
And App.js looking like:
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/rating">
<h1>Rating page for Documentaries</h1>
</Route>
<Route exact path="/review">
<h1>Review page for Documentaries</h1>
</Route>
</Switch>
</BrowserRouter>
);
}
I get an error:
Error: Invariant failed: Browser history needs a DOM at invariant
How do I fix the error? Or is there a better way to combine both EJS routes and create new React routes? Thanks in advance
Issue
You need to use a different Router in the server as well.
Since BrowserRouter in the client React app cannot be processed in the server.
Static Router is what you're looking for.
Solution
Server-side rendering in React is very tricky. I recommend you to use Nextjs which makes it easy.
For side-projects, refer to this blog
I am making an isomorphic react application, but now I am stuck of figuring out how to exclude server-side logic from bundling into client side javascript using react-router and webpack.
So my webpack has an entry points to "client.js" which is the clientside bundle javascript.
import React from "react"; import Router from "react-router";
import routes from "../shared/routes";
Router.run(routes, Router.HistoryLocation, (Handler, state) => {
React.render(<Handler/>, document.getElementById('react-app')); });
"client.js" contains react-router routes definition.
And for the server side, I have epxress and route set up as * (all requests route to here)
"server.js"
import routes from "../shared/routes";
app.get('/*', function (req, res) {
Router.run(routes, req.url, (Handler, state) => {
let html = React.renderToString(<Handler/>);
res.render('index', { html: html });
});
});
Since both client and server share the same routes, if I want to set up a route in the react-router e.g. /attractions/:id that will contain server side logic (database query, etc), it will get bundled by the webpack to the client.js
So I am wondering if there is a way to keep just one routes.js that shared by both "client.js" and "server.js" and have "client.js" not bundle some of the server routes.
I came up few possible solutions. But would like to see the best way to do it.
Keep two routes, one for server and one for client, and server routes is the superset of client routes.
Add another layer of abstraction to react-router, so instead of
<Route handler="/attraction/:id"/>
I can use import ABC from "ABCRouteController" and ABCRouteController will determine whether it's node or client and generate route or not generate route.
class AppController extends React.Component {
render () {
let route;
if #isServer
route = <Route handler={#someHandler}" path="/">
else
route =""
return route;
}
}
Add specific routing to server.js. So instead of
app.get('/*', function (req, res) {
Router.run(routes, req.url, (Handler, state) => {
let html = React.renderToString();
res.render('index', { html: html });
});
});
We add more specific routing for handling pure server side logic (similar to two seperate react-router for server and client)