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
Related
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.
I know this topic was mentioned here and here but it didn't work for me.
I'm trying to get parameters from URL using req.query. In my server.js file I've got this:
app.get('/reset-pass', function(req,res) {
console.log(req.url);
console.log(req.query);
})
When I'm entering URL e.g. http://localhost:3000/reset-pass?email=anything, server console outputs this:
/reset-pass
[Object: null prototype] {}
and when I fetch from http://localhost:4001/reset-pass, browser console outputs empty object:
data {
"query": {}
}
As you can see I run my node server on 4001 port and client site on 3000 (because I'm running React on that port) and it's working well doing POST requests or redirects, but in GET case it doesn't return query params. Any weird # do not appear in my path (or I just don't know it).
What's wrong with it?
Try
req.query.email
Hope this solves your issue
There must be another problem in your code.
Because i just tested as you did, totally same route.
router.get('/reset-pass', function (req, res) {
console.log(req.url);
console.log(req.query);
res.json({
url: req.url,
query: req.query,
});
});
Returns as:
{
"url": "/reset-pass?email=anything",
"query": {
"email": "anything"
}
}
And console.log as:
And it's ok. There is no problem with this operation. You should check other parts of your code.
I've figured out what was wrong.
Everything was perfectly fine with server. When I typed query in browser using server port, not client port (http://localhost:4001/reset-pass?email=anything), console.log in server terminal outputs everything correctly (as douscriptist said) or on page (e.g. using res.send()) it displays expected data.
The problem was with displaying URL parameters in React (in client view).
Trying to fetch data was just unnecessary in this case. I should have used React Router specifying routes like this:
App.js
<Router>
<Switch>
<Route exact path="/" component={Main} />
<Route path="/reset-pass">
<Route path="/:email" component={ResetPass} />
</Route>
<Route component={NoMatch} />
</Switch>
</Router>
And then in ResetPass.js using match and location I'm able to display query parameters. For quick pars URL string I've added query-string module
ResetPass.js
import React, { Fragment } from "react";
import queryString from "query-string";
const ResetPass = ({ match, location }) => {
console.log("MATCH", match);
console.log("LOCATION", location);
const parsed = queryString.parse(location.search);
return (
<Fragment>
<p>Params: {parsed.email}</p> {/* => Params: anything*/}
</Fragment>
);
};
export default ResetPass;
So when I enter URL http://localhost:3000/reset-pass?email=anything, console.log (in browser) returns this:
MATCH {path: "/:email", url: "/reset-pass", isExact: true, params: {email: "reset-pass"}}
LOCATION {pathname: "/reset-pass", search: "?email=anything", hash: "", state: undefined}
I hope that I've explained everything correctly.
I am new in react programming. Trying to solve issue my self but, stuck on following issue.
I have following react router code.
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
class Main extends Component {
render() {
return (
<Router>
<Switch>
<Route exact path='/' component={Content} />
<Route path='/user/:id' component={User} />
<Route path='*' component={NotFound} />
</Switch>
</Router>
);
}
export default Main
In content there are list of users with their photo. If i click on person photo it will redirect me to particular user.
I wrote my code like:
<Link to={'/user/' + userItem.id}>
<img className="useritem-img" src={userItem.photo} alt={userItem.tagline}/>
</Link>
It will open User Component properly with new URL like: http://localhost:3000/user/457365 on photo click.
But, when copy and paste same url in new tab it will not open. May be i am wrong some where.
Any help would be greatly appreciated.
Edit:
I am getting following error when i open that page:
Cannot GET /user/457365
I am not using create-react-app just simple react application.
Following is my server.js
app.use(express.static('dist'));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.listen(port, function (err) {
if (err) {
console.log(err);
} else {
open('http://localhost:' + port);
}
})
If you are getting that error that means that the server is trying to handle the routing. Therefore you should make sure that the server allows the SPA to handle the routing.
For instance, if you are using express, you probably want to do something like this:
app.use(express.static(path.join(__dirname, 'client/build')));
app.get('/api/whatever', (req,res) => {
// Whatever your api does
});
// Allow the SPA to take care of the routing
app.get('*', (req,res) =>{
res.sendFile(path.join(__dirname+'/client/build/index.html'));
});
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.
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)