react server side rendering with client side routing - javascript

An initial server rendering for my homepage route ( / ) works fine.
Also, subsequent client side navigation to ( /#/page2 ) works fine.
However, if I load /#/page2 directly from the address bar, the server rendered homepage loads in the browser first and then visibly transitions to /#/page2, which is not what I want. I want only /#/page2 to show up without first flashing the homepage.
What's happening is that node is serving up the homepage for the request to /, and then when the response hits the client, the client is running the route handler for /#/page2. Both are behaving correctly. But it's not what I want.
How do I avoid this behavior?
I think what I need is a way to for both the server and client to be aware of the different routes and both be able to handle them (isomorphically), however, the fragment part of the url is not known to the server.
Anyone else have this problem?
This problem isn't react specific. It is specific to SSR to a deep link.
My node router handles "/" as follows
router.get('/', function(req, res) {
var React = require('react');
var Router = require('react-router');
var Routes = require("../app/clapi-routes.jsx");
var router = Router.create({location: req.url, routes: Routes});
router.run(function(Handler, state) {
var html = React.renderToString(<Handler/>);
return res.render('index.ejs', {html:html});
})
});
index.ejs is just:
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/json-inspector.css"/>
</head>
<body style="margin:0">
<%- html %>
<script src="/build/bundle.js"></script>
</body>
</html>

Stop using hash powered navigation. Everything that comes after # is client-side only and useless for something like this. So /#/page2 needs to become /page2.
I'm not sure about react, but the same issue exists with other routing systems and there it's really easy to turn off the # in urls.
In angular's ui-router is done like this $locationProvider.html5Mode(true);
Your server-side would need to know to react to all the URLs your client-side knows, but that's how robustness is achieved - doesn't matter how navigation happens (client-side event or link click), both client and server can both handle the scenario end-to-end.

I've found out that there are two steps:
First: Change the react router to use Router.HistoryLocation instead of the default (HashLocation). This makes your routes use html5 push state and changes your route paths from /#/page2 to /page2
// in app.jsx (client side routing)
Router.run(AppRoutes, Router.HistoryLocation, function(Handler) {
React.render(<Handler/>, document.body);
});
Second: Ensure that your node page routes all return the same "index.ejs". Otherwise your routes, like /page2 will 404 on a full page refresh (or deeplink)
// in server.js (server side routing)
router.get('*', function(req, res) {
Router.run(Routes, req.path, function(Handler) {
var html = React.renderToString(<Handler/>);
return res.render('index.ejs', {html: html});
});
});
Additionally: If you are serving "public" static assets, declare that before your routes, and remove any public/index.html you might have so that your node router handles / requests with the server rendered content.

Related

Route ajax calls to path other than one mentioned while making the call

My project is build in C# MVC. Assume its hosted on abc.com.
Ajax call made for a resource for ex on Controller:Home and Method:ClientInfo will be something as :
url: '/Home/ClientInfo'
For Testing purpose I have deployed same project on assume xyz.com/web
In this case my Home page is xyz.com/web.
Now ajax calls made for the resource Controller:Home and Method:ClientInfo with the same ajax url '/Home/ClientInfo' is returning 404 Not Found.
Since the url should be something as
url: '/web/Home/ClientInfo'
One way of handling this is I edit all ajax urls throughout the project which doesn't seems right way.
I have common Layout can I write code there in javascript to route all urls under directory '/web' or is there any other way ?
*Note : I don't have control on web.config of xyz.com to handle the calls and write rules in there.
Please Suggest
You could make a JavaScript file that exports the API routes and import it when you need the routes, so you have one point to change when needed.
example ES6
File called routes.js with API routes:
export const API = '/web/';
export const CLIENT_INFO_API = `${API}home/clientinfo`;
Then whenever you need to use the routes in other files:
//Import routes file
import * as ROUTES from 'routes.js';
//Use the routes
ROUTES.API
ROUTES.CLIENT_INFO_API
example ES5
File called routes.js with API routes:
var routes = {};
routes.API = '/web/';
routes.CLIENT_INFO_API = routes.API+'home/clientinfo';
module.exports = routes;
Then whenever you need to use the routes in other files:
//Import routes file
const ROUTES = require('routes.js');
//Use the routes
ROUTES.API
ROUTES.CLIENT_INFO_API

App does not render when using browserHistory instead of hashHistory in React Router

I am using React Router in my current project:
const store = Redux.createStore(bomlerApp);
const App = React.createClass({
render() {
return (
React.createElement('div', null,
this.props.children
)
)
}
})
var rootElement =
React.createElement(ReactRedux.Provider, {store: store},
React.createElement(ReactRouter.Router, {history: ReactRouter.browserHistory},
React.createElement(ReactRouter.Route, { path: '/', component: App },
React.createElement(ReactRouter.IndexRoute, { component: Home })
)
)
)
ReactDOM.render(rootElement, document.getElementById('react-app'));
This does not work. The app does not render at all and I don't get any error messages.
However, if I use ReactRouter.hashHistory instead, the app works.
What am I not understanding here?
Server Configuration: the browser history setup can generate real
looking urLs without reloading the page. But what happens if the user
refreshes or bookmarks on a deep nested urL? these urLs are
dynamically generated at the browser; they do not correspond to real
paths on the server, and since any urL will always hit the server on
the first request, it will likely return a page not Found error.
To implement the browser history setup, you need to import the
createBrowserHistory method from the History library. You can then
invoke it, passing the generated browser history configuration as the
history prop of the Router component
***> to work with browser history setup, you need to make rewrite
configurations on your server, so when the user hits /some-path on the
browser, the server will serve index page from where react router will
render the right view.***

React-Router Webpack exclude server logic from bundling to client side 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)

filesystem based emberjs app

I'm building a filesytem based emberjs-app. But unfortunately the security won't allow me to push stuff to the history (see e.g. this but i guess that applies to all browsers).
setting the locationtype to none is fine, but I would still like to utilize the back and forward buttons and urls of the browser.
Is there a way to configure this (maybe setting the base-url to index.html, without rewriting the build process)?
edit
I call the url from my browser like this: file:///path/index.html.
in my routes.js and fileroute.js I've got this workaround:
// routes.js
export default Router.map(function() {
// this route only redirects to main
this.route('fileroute', {path: 'index.html'});
});
// routes/fileroute.js
// only for running app on filesystem
export default Ember.Route.extend({
redirect: function() {
this.transitionTo('fileroute.projects');
}
});
So I guess each hash-change would already effect the files-url
file:///path/#differentroute
also for
file:///path/#index.html/childRoute

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