Creating a Universal JavaScript app

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.

Creating reusable components

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.

Server-side rendering

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:

  • The <%- 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.
  • We are commenting the inclusion of the bundle script for now because in this section we want to focus only on the server-side rendering. We will integrate a complete universal rendering solution in the next sections.
  • We can now create our 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.

Note

Pattern

The server router component (Express built-in router) is here replaced by a universal router (React Router) that is able to match the routes both on the client and on the server.

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:

  • The first case is when we have an error during the routing resolution. To handle this case, we simply return a 500 internal server error response to the browser.
  • The second case is when we match a route that is a redirect route. In this case, we need to create a server redirect message (302 redirect) to tell the browser to go to the new destination.
  • The third case is when we match a route and we have to render the associated component. In this case, the argument 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.
  • The last case is when the route is not matched, and here we simply return a 404 not found error 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:

  • This function comes from the module 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.
  • The component we are rendering is 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.

Universal rendering and routing

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.

Universal data retrieval

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:

  • We are sharing the JSON file everywhere in our app and accessing the data directly across frontend, backend, and in every React component.
  • Given that we access the data also on the frontend, we end up putting the full database also in the frontend bundle. This is risky because we might accidentally expose any sensitive information. Also, our bundle file will grow with the growth of our database and we will be forced to recompile it after every change on the data.

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.

The API server

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:

  • Our data still lies in a module as a JSON file (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.
  • We are using a middleware that prints in the console some useful information every time we receive a request. We will see later that these logs can help us to understand who is calling the API (the frontend or the backend) and to verify that the whole app is behaving as expected.
  • We expose a GET endpoint identified by the URI /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.
  • We are also exposing another GET endpoint on the URI /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"]} 

Proxying requests for the frontend

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:

Proxying requests for the frontend

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.

Universal API client

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 browser

We 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.

Note

Other widely appreciated universal HTTP clients are superagent (https://npmjs.com/package/superagent) and isomorphic-fetch (https://npmjs.com/package/isomorphic-fetch)

Asynchronous React components

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.

The web server

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:

  • First of all, we need to import some new modules: http-proxy and async-props.
  • We initialize the proxy instance and we add it to our web server through middleware mapped to the requests that matches/api.
  • We change the server-side rendering logic a bit. This time, we cannot directly call the 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.120.136