React-Router Webpack exclude server logic from bundling to client side javascript - javascript

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)

Related

React server side rendering is not updating after client side route change

For the first time, the server side is refreshed, but the next time, only the client side is changed as usual, and the server does not change.
For example, with each refresh or typing of the address in the browser, the server also changes and works, but if I switch between pages on the client side with react router, the server does not change.
what is the problem?
#server/server.js
import path from 'path';
import fs from 'fs';
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import App from '../src/App';
const PORT = 5000;
const app = express();
const router = express.Router();
app.use('/build', express.static('build'));
app.use((req, res, next) => {
if (/\.js|\.css|\.png|\.jpg|\.jpeg/.test(req.path)) {
res.redirect('/build' + req.path);
} else {
next();
}
})
app.get('*', (req, res) => {
const context = {};
const app = ReactDOMServer.renderToString(
<StaticRouter location={req.path} context={context}>
<App />
</StaticRouter>
);
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf-8', (err, data) => {
if (err) {
console.log("Something went wrong:", err);
return res.status(500).send("Oops, better luck next time!");
}
return res.send(data.replace('<div id="root"></div>', `<div id="root">${app}</div>`));
});
});
router.use(express.static(path.resolve(__dirname, '..', 'build'), { maxAge: '10d' }));
app.use(router);
app.listen(PORT, () => {
console.log(`SSR running on ${PORT}`);
});
#server/index.js
require('ignore-styles');
require('#babel/register')({
ignore: [/(node_module)/],
presets: ['#babel/preset-env', '#babel/preset-react'],
plugins: ['#babel/transform-runtime'],
});
require('./server');
#index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import * as serviceWorker from './serviceWorker';
import App from './App';
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
There is nothing wrong with your Server Side Rendering setup. This is what's going on under the hood.
When you first type and enter the URL for a route of the application or you refresh the browser tab it hits the server and loads the index.html file rendered on the server-side with the help of renderToString.
ReactDOMServer.renderToString(...)`
Then index.html is viewed and is hydrated (attaching event handlers...etc) to this skeleton HTML file.
Note that you load the index.html from the build folder and replace only the div with root as the id. After building the app it adds js resources need to change the DOM (these resources are actually your frontend logic you wrote for your app) and this is required for client-side rendering. If you check the index.html it has the following script tags to load them.
...
<script src="/static/js/xxxxxxxxxxxxxxx.chunk.js">
...
When you go to another route by clicking a link inside your app. It does not hit the server again and it starts to execute js attached from the client bundle as I said above. Client-side js does the routing of the app properly. That's why it's not hitting your server. This is the expected nature of an isomorphic web application(Behaves the same on the server-side or client-side). And after that, if you refresh the browser that again loads the index.html from the server.

React server side rendering only specific routes

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

Extend the React Router of exist application on fly

Is there any way to extend react-router of one application which is already hosted on fly? I want to inject additional routes on the click of a link which allows me to inject the script or allows to include my javascript.
Eventually I am looking for two different react applications which has one build and deployment cycle, but interrelated to each other.
Ex. there is the abc.com in which on click of a link(i.e. abc.com/nepage) the entire page is getting reloaded with same content [i.e. say header footer] which is maintained by different team all to gather and they have there one build and deployment cycle.
I want the application to be with SPA even if we have different build and deployment process.
This was achieved using Backbone with help of Backbone.Router.extend, where on click of link the default router for the new page was overridden with all new set of routers and which use to full the supporting files from the path mentioned for the specific router's
With PlainRoutes, child routes can be loaded on-demand (when the user enters the route) and resolved asynchronously. Having that in mind, you can use Webpack chunks to split the code corresponding to theses routes in diferente files. Going even further, you can have multiple entrypoints on Webpack, making users load only the part of the application that affects the current page.
Sample app:
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, browserHistory } from 'react-router'
const App = ({ children }) => {
<div>
<nav>Your navigation header</nav>
{ children }
<footer>Your app footer</footer>
</div>
}
const HomePage = () => <p>Welcome!</p>
const routes = {
path: '/',
component: App,
indexRoute: { component: HomePage },
getChildRoutes (partialNextState, cb) {
require.ensure([], (require) => {
cb(null, [
require('./routes/about'),
require('./routes/blog'),
require('./routes/contact'),
])
})
}
}
ReactDOM.render(
<Router history={ browserHistory } routes={ routes } />,
document.getElementById('container')
)
routes/about.js:
import React from 'react'
const About = () => <p>About page</p>
export default {
path: 'about',
component: About
}
Other routes could be similar to the about route as shown above.

Refreshing page gives Cannot GET /page_url in react-router 2

I am using client side rendering with react 0.14 and react-router 2. I have deployed my app to a local node server.
I am at url (server_url/component1). Whenenver I refresh the page I am getting
Cannot GET /component1
error from server side.
I know this is happening because I am sending request to server again for /component1 which route does not exists on server. But I want to disable that whenever I am refreshing the page it should handled by client side routing only. I do not want to send any request to server.
Entry point to my app index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Router,browserHistory} from 'react-router';
import routes from './routes';
ReactDOM.render(
<Router routes={routes} history={browserHistory}/>
, document.querySelector('.init')
);
My routes.js file
module.exports = (
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/component1" component={comp1}/>
</Route>
)
Edit:
In this scenario should I use browser history or hash history? Are they same?
If I understood you correctly, then you have to redirect all your requests, that don't match defined routes, to the frontend. If it is just a static html file, the route should look like:
app.get('*', function(req, res) {
res.sendFile('public/index.html');
});
UPDATE
To let other routes work, you should put them just in front of catching route, since express applies them vice versa (the first defined route will be applied as the last one):
app.get('/any_route_here', (req, res) => {
res.json({msg: 'ROUTE WORKS'})
});
app.get('*', (req, res) => {
res.sendFile('public/index.html'); //pas routing to react
});
In such a way, if you hit a request like: yourserver.host/any_route_here you will get a json message ROUTE WORKS, but on any other request, the routing will be passed to your frontend.

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