React server side rendering renders incorrect content - javascript

I'm working on a react web app using React, Redux, and React Router, with server side rendering (using express)
The problem I'm facing is a bit hard to explain. I will try to explain it in the following steps.
You first enter the app from a URL like http://www.example.com/articles/1234. The express server will send down the correct content which includes the correct page source and the DOM(from chrome Element panel) as the initial load.
Then let's navigate to a different page like http://www.example.com/articles/5678 (navigating is not causing page refresh since it's a single page app, it only reaches out to the server for SSR content when you do a refresh with your browser or entering a page from a URL in browser address bar) everything works fine so far.
Refresh the page in the browser (You're now on page http://www.example.com/articles/5678) with cmd + r or F5. Again, the server will send down the content. But this time the content that you're receiving is a bit different. The page content in the browser is correct as well as DOM from chrome Elements panel. However, the page source is not correct, it's still the old page source that you got from step 1.
I have tried to log out the content when refreshing the page. The content that the server sent down in step 3 is not the correct content (It's the previous page) but somehow the browser can still see the right content.
And if I refresh the page once more after step 3 then I will get both the correct content and correct page source...
I'm also using Facebook open graph debugger. The debugger is telling me that it follows a redirect to step 3. And the redirect URL it followed is the previous page URL. I know this not quite right, but not sure where I'm doing it wrong...
Here are my express server settings
Thanks for the help!
server.js
require('babel-register');
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const ReactRouter = require('react-router');
const ReactRedux = require('react-redux');
const Store = require('./src/store/configureStore').default;
const routes = require('./src/routes').default;
const compression = require('compression');
const morgan = require('morgan');
const store = Store();
const match = ReactRouter.match;
const RouterContext = ReactRouter.RouterContext;
const Provider = ReactRedux.Provider;
const port = process.env.PORT || 5050;
const ReactHelmet = require('react-helmet');
const Helmet = ReactHelmet.Helmet;
const app = express();
app.use('/dist', express.static('./dist'));
app.set('view engine', 'ejs');
app.use(compression());
app.use(morgan('combined'));
app.use((req, res) => {
// prettier-ignore
match(
{ routes, location: req.url },
(error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
console.log(renderProps);
const body = ReactDOMServer.renderToString(
React.createElement(
Provider,
{ store },
React.createElement(RouterContext, renderProps)
)
);
const meta = Helmet.renderStatic().meta.toString();
res.status(200).render('index', {body, meta});
} else {
res.status(404).send('Not found');
}
}
);
});
console.log('listening on port ' + port); // eslint-disable-line
app.listen(port);

I have eventually resolved the issue.
The main problem was that I placed my async data fetching calls in the componentWillMount lifecycle method instead of componentDidMount.
And I didn't properly handle async data fetching in my App.
I'm now using the approach mentioned in this article to deal with my async data fetching issue.
Basically, I'm doing the following:
On the client side: Create a static method fetchData in components.
On the server side: Use React Router to grab the right component and use the fetchData method to fetch the data needed for that component.
After these promises have been resolved, get the current state from Redux store and render the HTML and send it down to the client.
Inject the state to the client side Redux store and render the page.

Related

Why Doesn't the Fetch API Call My API Endpoint I Created in Express?

I am wondering why the Fetch API in javascript isn't call the endpoint I created in express. When I start my server and go to the '/characters' endpoint, it returns correctly formatted JSON.
Here is my express index.js
const app = express();
const PORT = 3000;
const charactersRoute = require('./routes/characters');
//Characters Route
app.use('/characters', charactersRoute)
app.listen(PORT, function(err) {
if(err) console.log(err);
console.log(`Server is listening on port ${PORT}`)
})
Here is my Characters Route
const express = require('express'); // Web Framework
const https = require('https');
const router = express.Router();
const PORT = 3000;
// app.listen(PORT, function(err) {
// if(err) console.log(err);
// console.log(`Server is listening on port ${PORT}`)
// })
const api = 'https://www.breakingbadapi.com/api/characters/?limit=20';
router.get("/", function(req, res) {
https.get(api, (response) => {
console.log(response.statusCode);
response.on('data', (d) => {
const data = JSON.parse(d);
res.send(data);
})
// res.send("Running")
})
})
module.exports = router;
Here is my Characters.jsx file where I'm using Fetch
import React, {useEffect, useState} from 'react';
import Card from '#mui/material/Card';
import axios from 'axios';
export default function Character() {
const [data, setData] = useState();
useEffect(() => {
fetch('/characters')
.then(res => res.json())
.then(data => setData(data))
// console.log(data);
}, []);
}
When I run my front end and check the response I receive, it returns my index.html.
If I fetch the API URL itself, then it correctly returns the JSON in my frontend. But When I try to fetch the API endpoint I created in express, I don't get any data. Any suggestions?
You did not set the endpoint for the fetch function. It doesn't know what API '/characters' is. It is similar to saying the house number but not telling the street, you don't know where to go. So you need to pass the absolute path to fetch to request data from the server, because the server is a different 'entity', it's not the same with your front-end. Therefore, you need to provide the full API URL. Or, if using axios, since I see you imported it above, you must set axios.defaults.baseURL = <API_URL>, (or provide the full URL in the request itself just like in fetch) and then make the request with the help of axios, not fetch.
Therefore your React code will look a little something like this:
import React, {useEffect, useState} from 'react';
import Card from '#mui/material/Card';
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:3000';
export default function Character() {
const [data, setData] = useState();
useEffect(() => {
const getData = async () => {
try {
const {data} = await axios.get('/characters');
setData(data);
} catch (err) {
console.error(err);
}
};
getData();
}, []);
}
But I suggest you create a different file with all the axios requests and there you set the baseUrl - that's a better practice.
I think is that you are not using cors in your app.
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
Try this, I hope it helps you!
Error pattern
This kind of error occurs when we send a request from the client-side to the server-side using a relative URL (/characters in this case). But the frontend app and the backend app run on 2 different ports.
Reason
When we use relative URLs, the URL will be concatenated with the current host (the frontend host, not the backend). Usually, we receive a 404 error because the resource doesn't exist.
Example
Backend Express app running on port 5000. React app running on port 3000 for development. In React app, if we send a request to /users, then the full URL is http://localhost:3000/users. The request goes to the React app, not the backend server. And we don't receive the desired output.
Action
You should use an absolute URL, something like: http://localhost:5000/users to send the request to your backend app. Consider saving the host part (http://localhost:5000) in a global variable in order to use it in multiple places in your frontend code. When you have different environments for the backend (DEV/STAGING/PRODUCTION), you can change the backend host in only 1 place.

React Redux bundle.js being thrown into request

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.

How do you test NodeJS to see if a middleware was used?

Forgive me if this is something you shouldn't do, but I've looked around to see what is possible.
I want to verify that my express app has a middleware called/used for the app.
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors()); // <----- I WANT TO TEST THIS
app.get('/', (_req, res) => res.send({ hello: 'world' });
app.listen(5000, () => console.log(`Listening on port 5000`));
export default app;
Potential jest test
import app from './index.ts';
// This is a poor attempt to try and get this to work
test('test if CORS is implemented', () => {
const mockCors = jest.mock('cors');
const myApp = app;
expect(mockCors).toHaveBeenCalledTimes(1);
});
If anyone has the solution and if I should not be doing this, what is the reason behind it?
My guess is that you don't actually care that cors is called. Your client won't say to you "I want the cors middleware to be called". All the client should care about is the service that is provided, not how it's implemented. Your tests should do the same. If you decide one day to use another module for CORS, your tests shouldn't need to change, why would they?
My (personal) preferred approach is exactly that. Testing that given some input, my programs gives me the output I desire. Everything inside the program can be changed, as long as the behavior does not change. That will give you peace of mind when refactoring.
Also, for testing an Express app, you don't need a server, you just need the app. Actually starting the server can add complexity to your tests, since they might be hanging if you forget to close the server after them.
So in this example, the very first thing I would do is move everything you have in index.ts into a file called app.ts, and remove this line:
app.listen(5000, () => console.log(`Listening on port 5000`));
And in index.ts, only have this:
import app from './app.ts';
// Start the server
app.listen(5000, () => console.log(`Listening on port 5000`));
Then, I would use Supertest along with Jest, to make requests to that app:
npm i -D supertest
And then, in your test file, test what matters to the client:
import request from 'supertest';
import app from './app.js'; // Just import the app, not the server
describe("myApp", () => {
it('should implement CORS', async() => {
const { headers } = await request(app).get('/');
expect(headers['access-control-allow-origin']).toEqual('*');
});
});

Differences between query routes in nextjs using next-routes

I'm working on a project with next.js and Reactjs that uses a lot of different languages. So I need to change the language url. Example:
www.example.com/es/entradas
www.example.com/en/tickets
www.example.com/de/eintrittskarten
To make routes I saw that there is a module that helps me: next-routes
https://github.com/fridays/next-routes
There are a lot of url and I'm working with a CMS, so my clients will be able to add more, so routes can't be harcoded. I thought to pass the url with queries, like this:
const routes = require('next-routes');
module.exports = routes()
.add('index', '/:lang?')
.add('tickets', '/:lang?/:ticket')
.add('hotel', '/:lang?/:hotel');
My surprise (as you might see), it doesn't work because routes doesn't see the difference between these two last routes. If I write:
www.example.com/en/tickets
It will go correctly to my page "tickets" but if I write
www.example.com/en/hotel
It will go again to my page "tickets" and not to "hotel"
Do you know any way about how could I make this?
In my project I have these files related about routes:
server.js
const next = require('next');
const { createServer } = require('http');
const routes = require('./routes');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dir: './src/shared', dev });
const handle = routes.getRequestHandler(app);
app.prepare()
.then(() => {
createServer(handle)
.listen(3001, (err) => {
if (err) throw err;
console.log("> Ready on http://localhost:3001");
});
});
routes.js
const routes = require('next-routes');
module.exports = routes()
.add('index', '/:lang?')
.add('tickets', '/:lang?/:ticket')
.add('hotel', '/:lang?/:hotel');
The request for /en/hotel is going to the "ticket" route because it is actually matching it.
This is because of the : in front of the word "ticket" in the route. A : will turn a section of a route into a parameter with that name.
So instead what you probably want is:
module.exports = routes()
.add('index', '/:lang?')
.add('tickets', '/:lang?/ticket')
.add('hotel', '/:lang?/hotel');

Client Routing (using react-router) and Server-Side Routing

I have been thinking and I am confused with the routing between Client and Server. Suppose I use ReactJS for server-side rendering before sending the request back to web browser, and use react-router as a client-side routing to switch between pages without refreshing as SPA.
What comes to mind is:
How are the routes interpreted? For example, a request from Home page (/home) to Posts page (/posts)
Where does the routing go, on server-side or client?
How does it know how it is processed?
Note, this answer covers React Router version 0.13.x - the upcoming version 1.0 looks like it will have significantly different implementation details
Server
This is a minimal server.js with react-router:
var express = require('express')
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
var app = express()
// ...express config...
app.use(function(req, res, next) {
var router = Router.create({location: req.url, routes: routes})
router.run(function(Handler, state) {
var html = React.renderToString(<Handler/>)
return res.render('react_page', {html: html})
})
})
Where the routes module exports a list of Routes:
var React = require('react')
var {DefaultRoute, NotFoundRoute, Route} = require('react-router')
module.exports = [
<Route path="/" handler={require('./components/App')}>
{/* ... */}
</Route>
]
Every time a request is made to the server, you create a single-use Router instance configured with the incoming URL as its static location, which is resolved against the tree of routes to set up the appropriate matched routes, calling back with the top-level route handler to be rendered and a record of which child routes matched at each level. This is what's consulted when you use the <RouteHandler> component within a route handling component to render a child route which was matched.
If the user has JavaScript turned off, or it's being slow to load, any links they click on will hit the server again, which is resolved again as above.
Client
This is a minimal client.js with react-router (re-using the same routes module):
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
Router.run(routes, Router.HistoryLocation, function(Handler, state) {
React.render(<Handler/>, document.body)
})
When you call Router.run(), it creates a Router instance for you behind the scenes, which is re-used every time you navigate around the app, as the URL can be dynamic on the client, as opposed to on the server where a single request has a fixed URL.
In this case, we're using the HistoryLocation, which uses the History API to make sure the right thing happens when you hit the back/forward button. There's also a HashLocation which changes the URL hash to make history entries and listens to the window.onhashchange event to trigger navigation.
When you use react-router's <Link> component, you give it a to prop which is the name of a route, plus any params and query data the route needs. The <a> rendered by this component has an onClick handler which ultimately calls router.transitionTo() on the router instance with the props you gave the link, which looks like this:
/**
* Transitions to the URL specified in the arguments by pushing
* a new URL onto the history stack.
*/
transitionTo: function (to, params, query) {
var path = this.makePath(to, params, query);
if (pendingTransition) {
// Replace so pending location does not stay in history.
location.replace(path);
} else {
location.push(path);
}
},
For a regular link this ultimately calls location.push() on whichever Location type you're using, which handles the details of setting up history so navigating with the back and forward buttons will work, then calls back to router.handleLocationChange() to let the router know it can proceed with transitioning to the new URL path.
The router then calls its own router.dispatch() method with the new URL, which handles the details of determining which of the configured routes match the URL, then calls any transition hooks present for the matched routes. You can implement these transition hooks on any of your route handlers to take some action when a route is about to be navigated away from or navigated to, with the ability to abort the transition if things aren't to your liking.
If the transition wasn't aborted, the final step is to call the callback you gave to Router.run() with the top-level handler component and a state object with all the details of the URL and the matched routes. The top-level handler component is actually the Router instance itself, which handles rendering the top-most route handler which was matched.
The above process is re-run every time you navigate to a new URL on the client.
Example projects
React Router Mega Demo
Isomorphic Lab
With 1.0, React-Router depends on the history module as a peerDependency. This module deals with routing in the browser. By default React-Router uses the HTML5 History API (pushState, replaceState), but you can configure it to use hash-based routing (see below)
The route handling is now done behind the scenes, and ReactRouter sends new props down to the Route handlers when the route changes. The Router has a new onUpdate prop callback whenever a route changes, useful for pageview tracking, or updating the <title>, for example.
Client (HTML5 routing)
import {Router} from 'react-router'
import routes from './routes'
var el = document.getElementById('root')
function track(){
// ...
}
// routes can be children
render(<Router onUpdate={track}>{routes}</Router>, el)
Client (hash-based routing)
import {Router} from 'react-router'
import {createHashHistory} from 'history'
import routes from './routes'
var el = document.getElementById('root')
var history = createHashHistory()
// or routes can be a prop
render(<Router routes={routes} history={history}></Router>, el)
Server
On the server, we can use ReactRouter.match, this is taken from the server rendering guide
import { renderToString } from 'react-dom/server'
import { match, RoutingContext } from 'react-router'
import routes from './routes'
app.get('*', function(req, res) {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
} else {
res.status(404).send('Not found')
}
})
})

Categories