At this stage of the chapter, we should have got most of the basics that we need to transform our sample app into a full Universal JavaScript app. We met Webpack, ReactJs, and analyzed most of the patterns that help us to uniform and differentiate the code between platforms as needed.
In this section, we will keep improving our example by creating reusable components, by adding universal routing and rendering, and finally universal data retrieval.
In the previous example, we created two very similar components: JoyceBooks
and WellsBooks
. These two components are almost identical; the only difference between them is that they use different data. Now imagine a real case scenario where we might have hundreds or even thousands of authors... Yes, it wouldn't make much sense to keep having a dedicated component for every author.
In this section, we are going to create a more generic component and update our routing to be able to have parameterized routes.
Let's start by creating the generic components/authorPage.js
component:
const React = require('react'); const Link = require('react-router').Link; const AUTHORS = require('../authors'); class AuthorPage extends React.Component { render() { const author = AUTHORS[this.props.params.id]; return ( <div> <h2>{author.name}'s major works</h2> <ul className="books">{ author.books.map( (book, key) => <li key={key} className="book">{book}</li> ) }</ul> <Link to="/">Go back to index</Link> </div> ); } } module.exports = AuthorPage;
This component is, of course, very similar to the two components it is replacing. The two major differences here are that we need to have a way to fetch the data from within the component and a way to receive a parameter that indicates which author we want to display.
For the sake of simplicity, we require authors.js
here, a module that exports a JavaScript object containing data about the authors that we use as a simple database. The variable this.props.params.id
represents the identifier of the author we need to display. This parameter is populated by the router and we will see exactly how in a minute. So, we use this parameter to extract the author from the database object and then we have everything we need to render the component.
Just to make you understand how we are fetching the data, here is an example of how our authors.js
module might look:
module.exports = { 'joyce': { 'name': 'James Joyce', 'books': [ 'Dubliners', 'A Portrait of the Artist as a Young Man', 'Exiles and poetry', 'Ulysses', 'Finnegans Wake' ] }, 'h-g-wells': { 'name': 'Herbert George Wells', 'books': [ 'The Time Machine', 'The War of the Worlds', 'The First Men in the Moon', 'The Invisible Man' ] } };
It's a very simple object that indexes authors by a mnemonic string identifier.
Now the final step is to review our routes.js
components:
const React = require('react'); const ReactRouter = require('react-router'); const Router = ReactRouter.Router; const hashHistory = ReactRouter.hashHistory; const AuthorsIndex = require('./components/authorsIndex'); const AuthorPage = require('./components/authorPage'); const NotFound = require('./components/notFound'); const routesConfig = [ {path: '/', component: AuthorsIndex}, {path: '/author/:id', component: AuthorPage}, {path: '*', component: NotFound} ]; class Routes extends React.Component { render() { return<Router history={hashHistory} routes={routesConfig}/>; } } module.exports = Routes;
This time, we are using the new generic AuthorPage
component in place of the two specific components we had in the previous example. We are also using an alternative configuration for the router; this time, we are using a plain JavaScript array to define our routes instead of putting the Route
components within the render function of the Routes
component. The object is then passed to the routes
attribute of the Router
component. This configuration is totally equivalent to the tag-based one we saw in the previous example and is sometimes easier to write. Other times, for instance when we have many nested routes, the tag-based configuration might be more comfortable to work with. The important change here is our new /author/:id
route that is linked to our new generic component and which replaced our old specific routes. This route is parameterized (named parameters are defined with the "column-prefixed-syntax," as you can see here) and will match both our old routes /author/joyce
and /author/h-g-wells
. Of course, it will match any other route of this kind and the matched string for the id
parameter is directly passed to the component, which will be able to access it by reading props.params.id
.
This completes our example; to run it, you just need to regenerate the bundle file using Webpack and refresh the index.html
page. This page and main.js
remains unchanged.
Using generic components and parameterized routes, we have great flexibility and we should be able to build quite complex apps.
Let's make another small step forwards in our journey through Universal JavaScript. We said that one of the most interesting features of React is the ability to render components even on the server side. In this section, we are going to leverage this feature to update our simple app and render it directly from the server.
We are going to use Express (http://expressjs.com) as the web server and ejs (https:// npmjs.com/package/ejs) as the internal template engine. We will also need to run our server script on top of Babel to be able to leverage JSX, so the first thing we need to do is to install all these new dependencies:
npm install express ejs babel-cli
All our components remain the same as they were in the previous example, so we are going to focus on the server. In the server, we will need to access to the routing configuration so, to make this task simpler, we are going to extract the routing configuration object from the routes.js
file to a dedicated module called routesConfig.js
:
const AuthorsIndex = require('./components/authorsIndex'); const AuthorPage = require('./components/authorPage'); const NotFound = require('./components/notFound'); const routesConfig = [ {path: '/', component: AuthorsIndex}, {path: '/author/:id', component: AuthorPage}, {path: '*', component: NotFound} ]; module.exports = routesConfig;
We are also going to transform our static index.html
file into an ejs template called views/index.ejs
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>React Example - Authors archive</title> </head> <body> <div id="main"> <%- markup -%> </div> <!--<script src="dist/bundle.js"></script>--> </body> </html>
Everything is simple here; there are only two details worth underlining:
<%- markup -%>
tag is the part of the template that will be dynamically replaced with the React content that we will render on the server side before serving the page to the browser.server.js
script:const http = require('http'); const Express = require('express'); const React = require('react'); const ReactDom = require('react-dom/server'); const Router = require('react-router'); const routesConfig = require('./src/routesConfig'); const app = new Express(); const server = new http.Server(app); app.set('view engine', 'ejs'); app.get('*', (req, res) => { Router.match( {routes: routesConfig, 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) { const markup = ReactDom.renderToString(<Router.RouterContext {...renderProps} />); res.render('index', {markup}); } else { res.status(404).send('Not found') } } ); }); server.listen(3000, (err) => { if (err) { return console.error(err); } console.info('Server running on http://localhost:3000'); });
The important part of this code is the Express route defined with app.get('*', (req, res) => {...})
. This is an Express catch-all route that will intercept all the GET requests to every URL in the server. Inside this route, we take care of delegating the routing logic to the React Router that we setup before for the client-side application.
To adopt the React Router in the server, we use the function Router.match
. This function accepts two parameters: the first one is a configuration object and the second is a callback function. The configuration object must have two keys:
routes
: This is used to pass the React Router routes configuration. Here, we are passing the exact same configuration that we used for the client-side rendering, and that's the reason why we extracted it into a dedicated component at the beginning of this section.location
: This is used to specify the currently requested URL on which the router will try to match one of the previously defined routes.The callback function is called when a route is matched. It will receive three arguments, error
, redirectLocation
, and renderProps
, that we will use to determine what exactly the result of the match operation was. We can have four different cases that we need to handle:
renderProps
is an object that contains some data we need to use to render the component. This is the core of our server-side routing mechanism and we use the ReactDOM.renderToString
function to be able to render the HTML code that represents the component associated to the currently matched route. Then, we inject the resulting HTML into the index.ejs
template we defined before to obtain the full HTML page that we send to the browser.So, definitively, the most important part of this code is the following line:
const markup = ReactDom.renderToString(<Router.RouterContext {...renderProps} />
Let's see better how the renderToString
function works:
react-dom/server
and it is capable of rendering any React component to a string. It is used to render the HTML code in the server to be immediately sent to the browser, speeding up the page load time and making the page SEO friendly. React is smart enough that if we call ReactDOM.render()
for the same component in the browser, it will not render the component again, it will just attach the event listeners to its existing DOM nodes.RouterContext
(contained in the react-router
module), which is responsible for rendering the component tree for a given router state. We pass to this component a set of attributes that are all the fields inside the renderProps
object. To expand this object, we are using the JSX-spread attributes operator (https://facebook.github.io/react/docs/jsx-spread.html#spread-attributes), which extracts all the key/value pairs in the object to component attributes.Now we are ready to run our server.js
script with:
node server
Then, we can open the browser and point it to http://localhost:3000
to see our server-rendered app up and running.
Remember that we disabled the inclusion of the bundle file, so at the moment, we have no client-side JavaScript code running and every interaction triggers a new server request that refreshes the page entirely. This is not so cool, right?
In the next section, we will see how to enable both client and server rendering, adding to our sample app an effective universal routing and rendering solution.
In this paragraph, we will update our sample app to leverage both server- and client-side rendering and routing. We have already shown the individual parts working, so now it's just a matter of polishing things up a bit.
The first thing that we are going to do is to uncomment the bundle.js
inclusion in our main view file (views/index.ejs
).
Then, we need to change the history strategy in our client-side app (main.js
). Do you remember that we were using the hash history strategy? Well, this strategy doesn't play well with universal rendering because we want to have the exact same URLs both in the client and the server routing. In the server, we can only use the browser history strategy, so let's rewrite the routes.js
module to use it in the client as well:
const React = require('react'); const ReactRouter = require('react-router'); const Router = ReactRouter.Router; const browserHistory = ReactRouter.browserHistory; const routesConfig = require('./routesConfig'); class Routes extends React.Component { render() { return<Router history={browserHistory} routes={routesConfig}/>; } } module.exports = Routes;
As you can see, the only relevant change is that we are now requiring the ReactRouter.browserHistory
function and passing it to our Router
component.
We are almost done; there is only a small change that we need to perform in our server app to be able to serve the bundle.js
file to the client from our server as a static asset.
To do this, we can use the Express.static
middleware that allows us to expose the content of a folder as static files from a specific path. In our case, we want to expose the dist
folder, so we just need to add the following line before our main server routing configuration:
app.use('/dist', Express.static('dist'));
And that's pretty much it! Now to see our app live, we just need to regenerate our bundle file with Webpack and restart our server. Then you will be able to navigate through your app on http://localhost:3000
as you were doing before. Everything will look the same, but if you use an inspector or a debugger, you will notice that, this time, only the first request will be fully rendered by the server, while others will be managed by browser. If you want to keep playing a bit, you can also try to force refresh the page on specific URIs to also test that the routing is working seamlessly both on the server and on the browser.
Our sample app is now starting to get a solid structure in order to grow and become a more complete and scalable app. However, there is still a very fundamental point that we haven't yet addressed properly, and that is, data retrieval. Do you remember that we used a module that just contains JSON data? Currently, we use that module as a sort of database, but of course, this is a very suboptimal approach for a number of reasons:
It's obvious that we need a better solution—a more decoupled and scalable one.
In this section, we will improve our example by building a dedicated REST API server that will allow us to fetch the data asynchronously and on demand: only when it is really needed and only the specific subset of data that we want to render to the current section of the app.
We want the API server to be completely separated by our backend server; ideally, it should be possible to scale this server independently from the rest of the application.
Without further ado, let's see the code of our apiServer.js
:
const http = require('http'); const Express = require('express'); const app = new Express(); const server = new http.Server(app); const AUTHORS = require('./src/authors'); // [1] app.use((req, res, next) => { // [2] console.log(`Received request: ${req.method} ${req.url} from ${req.headers['user-agent']}`); next(); }); app.get('/authors', (req, res, next) => { // [3] const data = Object.keys(AUTHORS).map(id => { return { 'id': id, 'name': AUTHORS[id].name }; }); res.json(data); }); app.get('/authors/:id', (req, res, next) => { // [4] if (!AUTHORS.hasOwnProperty(req.params.id)) { return next(); } const data = AUTHORS[req.params.id]; res.json(data); }); server.listen(3001, (err) => { if (err) { return console.error(err); } console.info('API Server running on http://localhost:3001'); });
As you can see, we are again using Express as a web server framework, but let's still analyze the main parts of this code:
src/authors.js
). This is, of course, only for simplicity and works for the sake of our example, but in a real world scenario, it should be replaced with a real database such as MongoDB, MySql, or LevelDB. In this example, we will access the data directly from the required JSON object, while in a real case app we will make queries to the external data source when we want to read the data./authors
that returns a JSON array containing all the available authors. For every author, we are exposing the fields' id
and name
. Again, here, we are extracting data directly from the JSON file we imported as database; in a real-world scenario, we would prefer to perform a query to a real database here./authors/:id
, where :id
is a generic placeholder that will match the ID of the specific author for which we want to read the data. If the given ID is valid (there is an entry in our JSON file for that ID), the API returns an object containing the name of the author and an array of books.We can now run our API server with:
node apiServer
It will be now accessible on http://localhost:3001
, and if you want to test it, you can try to make a couple of curl requests:
curl http://localhost:3001/authors/ [{"id":"joyce","name":"James Joyce"},{"id":"h-g-wells","name":"Herbert George Wells"}] curl http://localhost:3001/authors/h-g-wells {"name":"Herbert George Wells","books":["The Time Machine","The War of the Worlds","The First Men in the Moon","The Invisible Man"]}
The API we just built should be accessible for both the backend and the frontend. The frontend will need to call the API with an AJAX request. You are probably aware of the security policies that allow the browser to make AJAX requests only to URLs in the domain where the page was loaded. That means that if we run our API server on localhost:3001
and our web server on localhost:3000
, we are actually using two different domains and the browser will fail to call the API endpoints directly. To overcome this limitation, we can create a proxy within our web server that will take care to expose the same endpoints of the API server locally using an internal convenience route (localhost:3000/api
), as shown in the following picture:
To build the proxy component in our web server, we are going to use the excellent http-proxy
module (https://npmjs.com/package/http-proxy), so we need to install it with npm:
npm install http-proxy
We will see in a moment how it will be included in the web server and configured.
We will call the API using two different prefixes given the current environment:
http://localhost:3001
when we call the API from the web server/api
when we call the API from the browserWe also need to consider that in the browser we have only the XHR/AJAX mechanism to make asynchronous HTTP requests, while on the server we have to use a library like request
or the built-in http
library.
To overcome all these differences and build a universal API client module, we can use a library called axios
(https://npmjs.com/package/axios). This library works both on the client and on the server and abstracts the two different mechanisms that each environment uses to make HTTP requests to a single uniform API.
So, we need to install axios
with:
npm install axios
Then, we also need to create a simple wrapper module that takes care to export a configured instance of axios.
We call this module xhrClient.js
:
const Axios = require('axios'); const baseURL = typeof window !== 'undefined' ? '/api' : 'http://localhost:3001'; const xhrClient = Axios.create({baseURL}); module.exports = xhrClient;
In this module, we are basically checking if the window
variable is defined to detect whether we are running the code on the browser or on the web server so that we can set the proper API prefix accordingly. Then, we simply export a new instance of the axios
client, configured with the current value of the base URL.
Now we can simply import this module in our React components, and depending on whether they are executed on the server or on the browser, we will be able to use a universal interface and all the intrinsic differences of the two environments will be hidden within the code of the module.
Other widely appreciated universal HTTP clients are superagent
(https://npmjs.com/package/superagent) and isomorphic-fetch
(https://npmjs.com/package/isomorphic-fetch)
Now that our components will have to use this new set APIs, they will need to be asynchronously initialized. To be able to do so, we can use an extension of the React Router called async-props
(https://npmjs.com/package/async-props).
So, let's install this module with:
npm install async-props
Now we are ready to rewrite our components to be asynchronous. Let's start with components/authorsIndex.js
:
const React = require('react'); const Link = require('react-router').Link; const xhrClient = require('../xhrClient'); class AuthorsIndex extends React.Component { static loadProps(context, cb) { xhrClient.get('authors') .then(response => { const authors = response.data; cb(null, {authors}); }) .catch(error => cb(error)) ; } render() { return ( <div> <h1>List of authors</h1> <ul>{ this.props.authors.map(author => <li key={author.id}> <Link to={`/author/${author.id}`}>{author.name}</Link> </li> ) }</ul> </div> ) } } module.exports = AuthorsIndex;
As you can see, in this new version of the module we require our new xhrClient
in place of the old module containing the raw JSON data. Then, we add a new method in the component class called loadProps
. This method accepts as arguments an object containing some context parameters that will be passed by the router (context
) and a callback function (cb
). Inside this method, we can perform all the asynchronous actions needed to retrieve the data necessary to initialize the component. When everything is loaded (or in case there is an error), we execute the callback function to propagate the data and notify the router that the component is ready. In this case, we are using xhrClient
to fetch the data from the authors
endpoint.
In the same fashion, we also update the components/authorPage.js
component:
const React = require('react'); const Link = require('react-router').Link; const xhrClient = require('../xhrClient'); class AuthorPage extends React.Component { static loadProps(context, cb) { xhrClient.get(`authors/${context.params.id}`) .then(response => { const author = response.data; cb(null, {author}); }) .catch(error => cb(error)) ; } render() { return ( <div> <h2>{this.props.author.name}'s major works</h2> <ul className="books">{ this.props.author.books.map( (book, key) => <li key={key} className="book">{book}</li> ) }</ul> <Link to="/">Go back to index</Link> </div> ); } } module.exports = AuthorPage;
The code here follows the same logic described in the previous component. The main difference is that, this time, we are calling the authors/:id
API endpoint and we are taking the ID parameter from the context.params.id
variable that will be passed by the router.
To be able to load these asynchronous components correctly, we need to also update our router definition for both the client and the server. For now, let's focus on the client and see how the new version of routes.js
will look:
const React = require('react'); const AsyncProps = require('async-props').default; const ReactRouter = require('react-router'); const Router = ReactRouter.Router; const browserHistory = ReactRouter.browserHistory; const routesConfig = require('./routesConfig'); class Routes extends React.Component { render() { return <Router history={browserHistory} routes={routesConfig} render={(props) => <AsyncProps {...props}/>} />; } } module.exports = Routes;
The two differences from the previous version are that we require the async-props
module and that we are using it to redefine the render function of the Router
component to use it. This approach actually hooks the logic of async-props
module within the rendering logic of the router, enabling the support for asynchronous behavior.
Finally, the last task we need to complete in this example is to update our web server in order to use the proxy server to redirect the API calls to the real API server and to render the router using the async-props
module.
We renamed our server.js
to webServer.js
to clearly distinguish it from the API server file. This will be the content of the new file:
const http = require('http'); const Express = require('express'); const httpProxy = require('http-proxy'); const React = require('react'); const AsyncProps = require('async-props').default; const loadPropsOnServer = AsyncProps.loadPropsOnServer; const ReactDom = require('react-dom/server'); const Router = require('react-router'); const routesConfig = require('./src/routesConfig'); const app = new Express(); const server = new http.Server(app); const proxy = httpProxy.createProxyServer({ target: 'http://localhost:3001' }); app.set('view engine', 'ejs'); app.use('/dist', Express.static('dist')); app.use('/api', (req, res) => { proxy.web(req, res, {target: targetUrl}); }); app.get('*', (req, res) => { Router.match({routes: routesConfig, 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) { loadPropsOnServer(renderProps, {}, (err, asyncProps, scriptTag) => { const markup = ReactDom.renderToString(<AsyncProps {...renderProps} {...asyncProps} />); res.render('index', {markup, scriptTag}); }); } else { res.status(404).send('Not found') } }); }); server.listen(3000, (err) => { if (err) { return console.error(err); } console.info('WebServer running on http://localhost:3000'); });
Let's analyze one-by-one the changes from the previous version:
http-proxy
and async-props
.proxy
instance and we add it to our web server through middleware mapped to the requests that matches/api
.renderToString
function because we must ensure that all the asynchronous data has been loaded. The async-props
module offers the function loadPropsOnServer
to serve this purpose. This function executes all the necessary logic to load the data from the currently matched component asynchronously. When the loading finishes, a callback function is called, and only within this function is it safe to call the renderToString
method. Also notice that, this time, we are rendering the AsyncProps
component instead of RouterContext
, passing it a set of synchronous and asynchronous attributes with the JSX-spread syntax. Another very important detail is that, in the callback, we are also receiving an argument called scriptTag
. This variable will contain some JavaScript code that needs to be placed in the HTML code. This code will contain a representation of the asynchronous data loaded during the server-side rendering process, so that the browser will be able to directly access this data and will not need to make a duplicated API request. To put this script in the resulting HTML code, we pass it to the view along with the markup obtained from the component rendering process.Our views/index.ejs
template was also slightly modified to display the scriptTag
variable we just mentioned:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>React Example - Authors archive</title>
</head>
<body>
<div id="main"><%- markup %></div>
<script src="/dist/bundle.js"></script>
<%- scriptTag %>
</body>
</html>
As you can see, we are adding the scriptTag
before closing the body of the page.
Now we are almost ready to execute this example; we just need to regenerate our bundle with Webpack and to start the web server with:
babel-cli server.js
Finally, you can open your browser and point it to http://localhost:3000
. Again, everything will look the same, but what happens under the hood is now completely different. Open your inspector or debugger on the browser and try to figure out when an API request is made by the browser. You can also check the console where you started the API server and read the logs to understand who is requesting the data and when.
3.138.120.136