CHAPTER 8

image

Isomorphic React Applications

To put it simply, a single page application is merely an empty HTML body that uses JavaScript to bring the page to life. While there are lots of benefits to this approach, there is also one visible downside: by the time the browser is able to download and run the application’s JavaScript (and ask the server for the initial data), users will experience a flash of blank page before seeing any content.

Isomorphic JavaScript applications (also called universal JavaScript applications) are applications whose code is (entirely or partially) shared between client and the server. By running the application’s JavaScript on the server, the page can get prepopulated before being sent to the browser, so the user can immediately see the content even before the JavaScript runs on the browser. When the local JavaScript runs, it will take over further navigation, giving the user the snappy interaction expected of a single page application with the quick first load of server-rendered pages.

In this approach, not only do users get a better experience because the application loads and renders faster, but other benefits arise as side effects: you get progressive enhancement for free (the app doesn’t stop working completely when JS fails) along with better accessibility and search engine indexability.

Node.js and Express

In order to run and prepopulate React applications on the server, you will use Node.js and Express. You’ve been using Node.js and Node’s package manager (npm) since the first chapter of this book. As you’ve seen before, Node.js is a JavaScript runtime that allows the execution of JavaScript applications outside the browser. Although it became an important tool for local development/package management of client-side JavaScript projects, it really shines as a server-side solution for JavaScript, and in particular, in building network programs such as web servers, making it similar to PHP and Python.

Express is a Node.js web application server framework, designed for building single-page, multi-page, and hybrid web applications. It is so commonly used that it’s considered the de facto standard server framework for Node.js.

The next section provides a quick introduction to Express and won’t cover anything related to React. If you are already familiar with Express, feel free to skip this section.

Node.js and Express “Hello World”

It is beyond the scope of this book to cover Node.js and Express, but to get familiarized with the basic setup before moving forward with universal React applications, let’s build a plain Node.js and Express “Hello World” application. Starting with a blank, new folder, you will create a package.json project file (using npm init -y command to accept all defaults) and a server.js file.

Next, let’s install the project’s dependencies: the Express framework and Babel (the compiler that lets you use the latest features of JavaScript). To install Express using npm, use the command npm install --save express.

The Babel installation is a little more complex. Out of the box, the babel-core package doesn’t do anything. In order to actually do anything to your code you need to enable plug-ins (or combination of plug-ins, called presets). In this example, you will use the ES6 preset: npm install --save babel-core babel-preset-es2015.

Finally, you also want to install the babel-cli package, which can be used to compile files from the command line. To install the Babel compiler command line globally, use the command npm install --global babel-cli.

Configuring Babel

Note that Babel needs to be configured on a project basis in order to work. The easiest way to configure Babel is to create a .babelrc file on the root folder of your project. In your case, the configuration includes simply setting the ES6 preset on this project. Listing 8-1 shows the final .babelrc configuration file.

Creating the Express Server

Create a new server.js file so you can start coding your server-side application. At this point, the project structure should look like Figure 8-1.

9781484212615_Fig08-01.jpg

Figure 8-1. Bare-bones Node.js and Express project

In the server.js file you require Express and you create an instance of the Express server. By convention, you typically use an “app” constant to point to the express.Server, like so:

import express from ’express’;
const app = express();

In the sequence, you can set up one or more routes for your application. A route consists of a path (string or regexp), a callback function, and an HTTP method. The callback function accepts two parameters: request and response. The request object contains information about the HTTP request that raised the event (including query string, parameters, body, HTTP headers, etc.). In response to the request, you use the response object to send back the desired HTTP response to the client browser.

Your Hello World example calls app.get(), which represents the HTTP GET method , with the path “/”, followed by the callback function that uses the response object to send a string back to the browser:

app.get(’/’, (request, response) => {
  response.send(’<html><body><p>Hello World!</p></body></html>’);
});

Finally, you can make the server start listening to a given port. In the following code, you call listen(), specifying port 3000 and a callback method that will be invoked while the server is running:

app.listen(3000, ()=>{
  console.log(’Express app listening on port 3000’);
});

The complete source code for the server.js file is shown in Listing 8-2.

Running the Server

To start the server in debug mode (to see the logs generated by Express), type the following command on the terminal:

DEBUG=express:* babel-node server.js

With the server running, you can point your browser to localhost:3000. The results should be similar to Figure 8-2.

9781484212615_Fig08-02.jpg

Figure 8-2. Running the Node.js and Express server and testing on the browser

To make things easier and save some typing, you can pass this command as the start script on package.json file. This way the next time you want to start the server locally you only need to type npm start. Listing 8-3 shows the updated package.json.

Using Templates

Sending string responses using response.send is a quick way to get started with Express, but for any realistic job it can became cumbersome to format all responses this way and deliver complete HTML structures. Express supports the use of templates for this reason. A template is HTML markup, enhanced with tags that will either insert variables or run programming logic. Express supports a variety of template formats. For this example you will use a template format called EJS.

First, make sure to install EJS as a dependency of the application with npm install --global ejs. Next, you need to configure your application to use EJS templates. This can be done using the set method:

app.set(’view engine’, ’ejs’);

By default, template files must be saved in a views folder. Create a new views folder with an index.ejs template file, as shown in Figure 8-3.

9781484212615_Fig08-03.jpg

Figure 8-3. Template folder and index.ejs template file

To instruct your application to render a template instead of sending a string, you use the response.render method, passing the template name and an object that will be accessible from inside the template to display dynamic values. Listing 8-4 shows the complete index.js source code with all the updates you made to use templates and render server.js. Listing 8-5 shows the index.ejs source code.

Serving Static Assets

Express comes with built-in middleware to serve static content. The express.static() middleware takes one argument that refers to the root directory from which the static assets are to be served. To serve static files from a public folder, for example, just add the following line to the server code:

app.use(express.static(__dirname + ’/public’));

Isomorphic React Basics

Now that you’re familiar with Express, let’s move on to render an actual React component on the server. React and the React-DOM package offer built-in support for rendering components in the server through the ReactDOMServer.renderToString method. It renders a given component and generates annotated markup to be sent to the browser. On the browser, React can pick up the annotated markup and only attach event handlers, allowing you to have a very performant first-load experience.

Creating the Project Structure

An isomorphic React project structure has different requirements than those of a client-side–only application. For this reason, instead of using the React App Boilerplate app you’ve been using so far as the base for new projects, you will create a project structure from scratch in this case.

Starting in a new folder, the first thing to do is to create a package.json project file. You can create one quickly by running npm init -y. The project structure will contain two folders at the root level: a public folder (for static assets that will be served to the browser) and an app folder (where you will save React components and other project files that will be shared by both client and server). Inside the app folder you create a components folder to keep your project organized.

The project starts with three files: server.js, index.ejs, and browser.js. The server.js file will contain server-side JavaScript code (where you will set up an Express server and render the components). The browser.js file will contain client-side JavaScript code. The index.ejs file will contain the basic HTML page structure that will be sent to the browser. Figure 8-4 shows the project structure files and folders.

9781484212615_Fig08-04.jpg

Figure 8-4. The project structure

The Contacts App Files

In this project, you will use an example similar to one from Chapter 3, a contactList component with a search bar. The project files won’t be identical, though; you will create a simplified component hierarchy to focus on the server-side rendering and receive the array of contacts through a prop named initialData. The component hierarchy will contain

  • ContactsApp: The main component
    • SearchBar: Shows an input field so user can filter the contacts
    • ContactList : Loops through data creating a series of ContactItems

Figure 8-5 shows the desired output.

9781484212615_Fig08-05.jpg

Figure 8-5. The Contacts app

In total, the React project has four files, three components and an external JSON file containing the contacts list. You create all the component files in the components folder; the source code is shown in Listings 8-6 through 8-8). The json file will be saved in the public folder, and its content is shown in Figure 8-6.

The ContactList component receives an array of contacts and the filterText. It filters the contacts and loops through the array, rendering each contact’s information (Listing 8-6).

The SearchBar component renders a controlled form component to the user and calls a callback on every change. The value inputted by the user is used to filter the contact list (Listing 8-7).

The ContactApp renders the ContactList and SearchBar components. It receives the initial contact list through the initialData prop, and attributes this prop value to its own state (Listing 8-8).

Finally, Figure 8-6 shows the contacts.json file in the application’s root folder.

9781484212615_Fig08-06.jpg

Figure 8-6. The contacts.json file in the root folder of the project structure

Rendering React Components on the Server

With the app structure and the sample components ready, you can now write the server script. To start, install all the dependencies for server-side development: the Express server, the EJS template format, and the React packages. To install all at once, use the command npm install --save express ejs react react-dom.

You also need to install Babel, so use the ES6 syntax and JSX. To install the latest Babel version with support for ES6 and JSX as well as its peer dependency (webpack), use npm install --save webpack babel-core babel-loader babel-preset-es2015 babel-preset-react.

Finally, you need to install the babel compiler command line. For convenience, you can install it globally using the command npm install --global babel-cli.

Babel Configuration

Babel needs to be configured on a project basis in order to work. The easiest way to configure Babel is to create a .babelrc file on the root folder of your project. In your case, the configuration includes simply setting the ES6 preset on this project. Listing 8-9 shows the final .babelrc configuration file.

Express App

The template file is pretty straightforward: just the basic HTML tags with a root div where the dynamic content will be inserted. Listing 8-10 shows the source code.

Next, in the server.js JavaScript file you set up an Express server with the following considerations:

  • The Express server will be configured to
    • Use EJS as template format and look for the template files in the root folder.
    • Serve static assets from the public folder.
  • Your ContactApp components need a list of contacts. This list could come from a database or an API server, but for simplicity purposes you will load from the json file you saved in the public folder.

The server code is shown in Listing 8-11.

Everything is wired up and working. You can test right now, but the result is just a “hello” string on the browser. To start the server, use node_modules/.bin/babel-node server.js.

Rendering React Components

Now comes the interesting part: rendering the React component on the server. There are a few things to consider here:

  • As mentioned, you will use a react-dom method called renderToString to generate annotated markup from the component and send to the browser.
  • You won’t use JSX on the Express server. As mentioned in Chapter 2, to instantiate React components outside JSX, you need to wrap the component in a factory before calling it.

Listing 8-12 shows the updated server code.

If you run the server and test on the browser, you will see that the component was rendered correctly and that the generated HTML contains all the necessary annotations the client-side React will need to mount the component on the browser. Your result should look like Figure 8-7.

9781484212615_Fig08-07.jpg

Figure 8-7. Server-side rendered React component

Notice, however, that right now there is no interactivity in the browser. The component is static and is not filtering the contacts. This happens because you didn’t create or send any JavaScript to run on the browser; you only created server-side JavaScript. The browser is simply receiving and showing an HTML page with no dynamic content so far.

Mounting React on the Client

You are creating an isomorphic project setup that allows you to use the same component code to render both on the server and the client, but you’re not done just yet because you only rendered on the server so far. You need to provide a JavaScript file to the browser to make React mount on top of the prerendered components and hook up event listeners.

Client-side Setup

You already have an empty JavaScript file (browser.js) for the purpose of generating the client-side JavaScript, but this file needs to be compiled and packed before being sent to the browser.

In all the examples of this book you used webpack with Babel to do this process, and it won’t be different now. Create a webpack configuration file in the root folder. It will be very simple since you’re not going to use advanced features such as the webpack’s development server, just basic setup to pack the browser.js file and output a bundle.js file in the public folder, as shown in Listing 8-13.

With this configuration in place, you can run webpack –p to generate the client-side bundled JavaScript file.

Passing the Component’s initialData

Now that you have a configuration file in place to compile and pack the browser.js file into a bundle.js file on the public folder, you need to update your template file to load this client-bundled JavaScript file.

That’s not the only update you need on the template file, though. Mounting React on top of a server-rendered component is different from every other client render in that you need to provide the exact same props that were used to render the component on the server; otherwise React will be forced to rerender the entire DOM (React will give you a warning for this). What is needed is a mechanism for passing the same props used on the server render to the client. You can achieve this by creating a script tag in the HTML template and just dumping all the props inside. The client JavaScript can then parse and use the exact same props.

In plain English, you need two script tags in the template file: one contains the initial data needed by the React components (all the data and props), and the other is used to load the client JavaScript. Listing 8-14 shows the updated index.ejs file.

Listing 8-15 shows the server.js with the updated render call, passing the array of contacts that will be needed by the client side script to instantiate the ContactsApp component locally.

browser.js

The browser.js file is analogous to the server.js file: you import the desired components and render it. You even need to pass the exact same props that were passed to the component when it was rendered on the server. If you pass different props, the client-side React won’t be able to “mount” on top of the prerendered component, assign event listeners, and assume interactivity. Fortunately, the server is passing the props it used inside a script tag with an id of initialData; the client-side JavaScript can parse and use them. Listing 8-16 shows the complete source.

To avoid any errors in circumstances where no initialData is needed, notice that you are checking if the initial data script tag contains any content before parsing it.

Before running the server to test, make sure to compile and pack the browser.js to public/bundle.js using the command webpack -p. See Figure 8-8.

9781484212615_Fig08-08.jpg

Figure 8-8. React on client-side mounting a server-prerendered component

Routing

React Router is the de facto routing solution for React applications, and since it’s 1.0 release it supports server rendering out of the box. The routing setup in the server, though, is a little bit different than in a client because, besides matching routes to components, you also want to send 500 responses for errors and 30x responses for redirects.

To facilitate these needs, you drop one level lower than the <Router> API with

  • Match to match the routes to a location without rendering
  • RoutingContext for synchronous rendering of route components

Setting Up Internal Routes

You will continue working on the ContactsApp example, but now you’re going to implement a new route called Home. Start by installing the React Router. Be aware that the examples in this book are using React Router 1.0.0. You also need to install its dependency history. Make sure to install both using npm install --save react-router history.

Next, set up your routes file. Since the routes are going to be shared between the client and the server, you save it in the app folder. Initially, you set up three routes: a new parent App route, a new Home index route, and a route called Contacts to show your existing ContactsApp. Listing 8-17 shows the source code.

In this route, you’re assuming you have two new components: App and Home. Let’s create both in the app/components folder, as shown in Listings 8-18 and 8-19.

Dynamic Data Fetching

Until now, your project only contained a single entry point: the ContactsApp component. Every time you loaded your application in the browser, the server prefetched the contacts list and sent a prepopulated component to the browser. In an application with multiple routes, the server needs to detect which data is needed by the component that maps the current route. It doesn’t make sense, for example, to always load the contacts list if the user is at the Home route. On the other hand, if the user starts navigating at the Home and goes to the Contact route, the component won’t be prepopulated, so it needs to be able to fetch data from the server as needed.

In other words, you need an approach that allows

  • Data prefetching on the server only for the component that maps to the current route
  • Data fetching on the client in case the user navigates to a different route where data is needed but wasn’t prefetched

While there isn’t a single correct way of declaring a component’s data fetching needs in a way that can be fulfilled both on the server and on the client, a popular approach is to create a static method on the component’s class to declare the data needed by that component. The static method is accessible even when the component is not instantiated, which is crucial for prefetching.

To exemplify, let’s change the way your sample isomorphic application handles data fetching. The ContactsApp is the only component that needs data, so you declare a static method to fetch remote data. If the user enters the application on the Contacts route, the server will then run this method and pass the results as props to the component. If the user enters the application through any other route and later navigates to the Contacts route, the browser will run this method to fetch data on demand.

To start, you install the isomorphic-fetch npm package. It’s a clever package that uses Node’s default fetch on the server and implements a polyfill for the browser: npm install --save isomorphic-fetch.

Next, let’s update the ContactsApp component to create the static requestInitialData method.

You also implement the componentDidMount lifecycle method that is only called on the browser to check if whether the initialData was provided by the server or not (in which case it invokes the requestInitialData method to fetch the initial data now). Listing 8-20 shows the updated code.

Rendering Routes

Now you’re going to make big changes in both server.js and browser.js to render the routes on both client and server.

Rendering Routes on the Server

Starting with server.js, you make the changes in steps. The first step is to change the “get” entry point from “/” to “*” so all routes will invoke the callback. You also configure the error, not found, and redirect routes. For existing routes, you render the appropriate component. Listing 8-21 shows these changes.

A few additional things to notice on this code:

  • The ContactsApp import and factory were removed (since this will be managed by the router now).
  • For better organization, you move the response.render method outside the routing and to a new function called renderRoute (which you implement in the next step).

In the next step, let’s reimplement the component rendering. React Router provides an object called RoutingContext with the hierarchy of all the components that must be rendered for the current route. It’s possible to pass the RoutingContext directly to React’s renderToString to generate the markup for all the components, but this won’t work in your case because you also need to prefetch data and pass it as props to the components that implement the requestInitialData static method.

What you do, instead, is loop through all the components inside RoutingContext to check if any of them implements requestInitialData. If you find a component that does, you prefetch the data and pass it as props to the internal component by overriding the function used in the RoutingContext to instantiate the internal components. Listing 8-22 shows the updated code.

The complete source code for the updated server.js file is shown in Listing 8-23.

Rendering routes on the Browser

Next, you need to make similar adjustments to the browser.js client file: render a route and check if there is initialData, passing it as props to the correct component. Again you rely on React Router’s createElement prop to override the default function used to instantiate React elements to pass the initialData as props for the component that implements requestInitialData static methods. Listing 8-24 shows the updated code.

If you test right now starting on the Home route (“/”), you will notice that the contact app will fetch data from the browser when you navigate to its route. However, if you access the application directly into the /contacts route, the server will prefetch the contacts data and send a populated component to the browser.

Summary

In this chapter, you learned about the benefits of isomorphic applications, which includes a better perceived performance, search engine optimization, and graceful degradation (the app works even if the local JavaScript is disabled). You now know how to render React components on the server and how to “mount” on the browser prerendered react components.

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

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