Chapter 7. Building the server

This chapter covers

  • Setting up Node.js with Express
  • Writing Express middleware
  • Using React Router on the server to handle view routing
  • Rendering React on the server with the renderToString function
  • Fetching data on the server using Redux
  • Implementing a static method on your components to handle data fetching

This chapter is all about the code that needs to happen on the server. I’ll cover server-specific topics including using Express and using your component and routing code on the server. For example, you’ll learn how to declare the actions of your application in a way that allows the server to fetch them automatically on every page render.

Yes, you read that correctly: you’re going to run your React, React Router, and Redux code on the server. React and React Router each provides server-specific APIs to make this work. Redux needs only minor changes—mostly you’ll call actions from the server. Figure 7.1 illustrates how this works.

Figure 7.1. The main differences between server and browser code for React and React Router

To get the code shown in figure 7.1 working, you need to do the following things:

  • Set up app routing with Express
  • Handle specific routes (for example, the cart and products routes) with React Router using the match function
  • Render your React components on the server using renderToString
  • Fetch the data for your components on the server
  • Respond to requests with the rendered app

These pieces make up the server-rendered part of an isomorphic application. That includes everything from the initial user request for your app to sending a rendered response to the browser.

renderToString vs. render

Let’s go over the differences between the render and renderToString methods so you can better understand why we treat the render on the server as different from the browser render. Table 7.1 describes the output and use case for each method.

Table 7.1. Comparing render and renderToString
 

Output

Runs once?

Environment

render JavaScript representation of your components No. Runs every time there’s an update. Browser
renderToString A string of DOM elements Yes. Doesn’t hold any state. Server

Figure 7.2 shows the part of the isomorphic application flow that this chapter covers. The app you’ll build in this chapter is the All Things Westies app that you started working on in chapter 4. You’ll build the server-rendered portion of the cart but not any of the browser-specific code or interactions. In this chapter, all the data will be mocked out from the server. Figure 7.3 shows what this app looks like after it’s built out. (You’ll build the rest of the app in later chapters.)

Figure 7.2. Isomorphic app flow—server render only

Figure 7.3. The portion of the app you’ll build and render on the server in this chapter

You can find the code for this app at http://mng.bz/8gV8. After you’ve pulled this code down, you’ll want to switch to the chapter-7-express branch to follow along (git checkout chapter-7-express). Before you add the renderToString call, you first need to set up your app server. Let’s walk through Express basics and get the app server set up.

7.1. Introduction to Express

When I started as a client app developer (building the UI portions of apps), there wasn’t much need to be able to do full-stack development. These days, the ability to implement and understand web servers, infrastructure, and distributed systems is a sought-after skill. The good news is that being able to build a server that renders the initial page load of your isomorphic app will go a long way toward improving your knowledge in this area.

Express is a framework for Node.js that makes it easy to build REST APIs and to implement view rendering. In the All Things Westies app, Express handles the incoming requests to the Node.js server—for example, when the user wants to go to the Products page, the first place the request gets handled is by the Express app routing. Part of building an isomorphic app with JavaScript is handling initial requests to your web server; the server handles routing, fetching data, and rendering the page. The fully rendered page is then sent in the response to the browser.

7.1.1. Setting up the server entry point

First, you need to get a basic server up and running. You want to use the command line to start your server and get it running on port 3000, as shown in figure 7.4. I’ve already supplied the root entry point files for you (server.js and app.es6).

Figure 7.4. Starting the Node.js server

In the current branch, Express is already in package.json. To use it with your Node.js application, you need to install it with npm. This will install all the packages needed for this section:

$ npm install
$ npm start

When you navigate to localhost:3000, you’ll see an error, as in figure 7.5.

Figure 7.5. Without route handling, the server throws an error.

The following listing shows the server entry file that’s already provided for you in the base code on the chapter-7-express branch.

Listing 7.1. Server entry—src/app.es6
import express from 'express';                        1

const app = express();                                2

// other code – routes for data endpoints
app.listen(3000, () => {                              3
  console.log('App listening on port: 3000');
});

  • 1 Include Express framework in project.
  • 2 Initialize Express and assign it to app.
  • 3 Call listen on app and set port to 3000—you can do anything in the callback, the console.log statement lets the user know which port the server is running on.

Now that you’ve seen the initial setup of the server code, you’ll add routing with Express.

7.1.2. Setting up routing with Express

The Express router handles all incoming requests to the application and decides what to do with each request. For example, if you want to have a route such as /test that returns text and a 200 response, you need to add code that handles this route to the app.es6 file. Figure 7.6 shows the expected output. Because you haven’t added route handling, this won’t work right now. Eventually, this route handling will allow you to render app routes with React.

Figure 7.6. Routing to the test route with Express

Note

In Express, every incoming request is represented by a request object. This object holds information about the URL, cookies, and other HTTP information such as any headers that were sent.

You can set up specific routes for any type of HTTP verb (GET, POST, PUT, OPTIONS, DELETE). For the main portion of the app, you’ll need to implement only GET requests to respond to user requests for individual web app pages. Listing 7.2 shows how to add a route handler for a GET request to the /test route. You’ll add the code in the listing to app.es6.

Note

Express creates a response object for every incoming request. This object holds information that will be sent back to the browser such as headers, cookies, status code, and response body. It also has helper functions for setting the response body and status code.

Listing 7.2. Add a route—src/app.es6
app.get('/api/blog', (req, res) => {});

app.get('/test', (req, res) => {          1
  res.send('Test route success!');        2
});

app.listen(3000, () => {});

  • 1 get function takes in route (/test) and a callback.
  • 2 Callback must respond to the response (or the request will hang indefinitely).

Here you’re sending a simple string back to the browser indicating that the route exists. The response is sent with the send() method, which is found on the response object.

Regular expressions in routes

In addition to hardcoding full string paths such as /test, Express supports regular expressions as routes. This is helpful for building an app with React Router because you want to hand off the route handling to React Router instead of having individual Express routes. If you wanted the Express app to know about routes such as /cart and /products, you’d have to have duplicate logic in place in both Express and React Router. Figure 7.7 illustrates the differences.

Figure 7.7. Efficiency versus lots of code duplication makes server routing with React Router the best choice.

If you re-create the routes in Express (the bad option in figure 7.7)

  • You have code duplication and no single source of truth for routes.
  • You need to somehow provide the same React Router interface to these routes so that your app render on the server ends up matching your app render in the browser. This is a lot of work!

By reusing React Router and taking advantage of its built-in server functions, you save time and you don’t have to worry about your initial app state on the server being different from the initial app state on the browser.

You want to do this in an isomorphic application because it lets you reuse more of your code by using the routes from React Router. That lets you use the same routes in both environments. Figure 7.8 shows how typing in an arbitrary route will print that route with a success message. This route won’t work until you add the code from listing 7.3.

Figure 7.8. The GET route handler for all routes allows you to pass any route to the server.

Listing 7.3 shows how to set up a global route handler in your app.js file. The global route handler will always come last. It’ll call a middleware function (renderView.jsx—see listing 7.4 in the next section) that uses React Router’s match logic to figure out which view to render.

Listing 7.3. Add a route to handle any view—src/app.es6
// all other routes go before the global handler
app.get('/test', (req, res) => {...});

app.get('/*', (req, res) => {
  res.send(`${req.url} route success!`);
});

The get function takes in a regular expression. * will match all routes—any routes that are matched before the /* route won’t be handled by this route handler. The callback responds with a string that prints the current URL. If you restart the server at this point, you’ll see the output in figure 7.8.

Next you’ll add middleware to your route handlers.

7.2. Adding middleware for view rendering

So far, you’ve set up routes that are terminated in a single function that takes in the request and responds to it with the response object. Next, you want to implement a middleware function that checks for a route match with one of the app views (for example, /cart). Express can chain multiple functions together to handle complex business logic. These individual functions are called middleware. (If you’re thinking this sounds a lot like Redux middleware, it is!)

Because we’ve decided to let React Router handle all the view routing, you’ll use the same sharedRoutes file you created in chapter 4. You can review the code in shared/sharedRoutes.jsx. There are four routes inside a root route (and an IndexRoute to make sure something renders on the root route):

  <Route path="/" component={App} >
    <IndexRoute component={Products} />
    <Route path="/cart" component={Cart} />
    <Route path="/products" component={Products} />
    <Route path="/profile" component={Profile} />
    <Route path="/login" component={Login} />
  </Route>
Note

If you’re using React Router 4, check out appendix A for an overview of setting up the routes.

After you’ve configured the React Router routes, it’s time to write your first Express middleware, which will call the match function and determine whether the requested route exists in your app.

7.2.1. Using match to handle routing

The renderView middleware handles route matching. It uses a function called match that’s provided by React Router. After you’ve added it, you’ll be able to navigate to each route you created in sharedRoutes. Figure 7.9 shows sample output of navigating to localhost:3000/cart after you add the code from listings 7.4 and 7.5.

Figure 7.9. The middleware allows routing on the server to respond correctly based on the React Router shared routes.

Note

If you’re using React Router 4, check out appendix B for an overview of how to handle the routes on the server.

To get the middleware hooked up to the /* (all) route handler you set up in the previous section, you’ll replace the request handler function with the renderView middleware function. The following listing shows the basic route-matching logic of the middleware.

Listing 7.4. Route-matching middleware—src/middleware/renderView.jsx
import { match } from 'react-router';                            1
import routes from '../shared/sharedRoutes';                     2

export default function renderView(req, res) {                   3
  const matchOpts = {
    routes,                                                      4
    location: req.url                                            4
  };
  const handleMatchResult = (
                               error,
                               redirectLocation,
                               renderProps
                            ) => {                               5
    if (!error && !redirectLocation && renderProps) {            6
      res.send('Success, that is a route!');
    }
  };

  match(matchOpts, handleMatchResult);                           7
}

  • 1 Include match function from React Router.
  • 2 Include the routes from shared routes.
  • 3 Middleware function takes in several parameters.
  • 4 Configure the match function options. The object requires your shared routes as well as the location being requested (the URL from the request).
  • 5 This callback will be called from the match function after it determines what to do with the current route.
  • 6 Check to make sure there isn’t an error or redirect.
  • 7 Call match function with options and callback.

Multiple callbacks are used in the listing. The request and response objects are passed to each middleware function. The next parameter is a callback function used to pass to the next middleware in the chain. The other callback for React Router has three parameters: an error object, a redirect location, and renderProps, which represents the components to be rendered if the route is valid.

After you have the middleware set up, you also need to use it in app.es6. The following listing shows how to import and apply the middleware by passing it in as a callback to the route handler. Update app.es6 with the code from the listing.

Listing 7.5. Using renderView middleware for the catchall route—src/app.es6
   import renderViewMiddleware from './middleware/renderView';    1

app.get('/*', renderViewMiddleware);                              2

  • 1 Import your renderView middleware.
  • 2 Replace inline anonymous route handler (you pass middleware function into the route handler).

Figure 7.10 shows how a request enters your app via Express, gets routed by the /* router in Express, and then passes through various middleware functions that handle React Router routes such as /cart or errors. Each middleware function has the option to either terminate the request (successfully or with an HTTP error response code) or call the next callback. Calling next passes the request to the next middleware function in the chain.

Figure 7.10. The flow of a request through the Express router and associated middleware that use React Router to check for the presence of a valid route

This section covered how to use Express middleware alongside React Router on the server to determine the existence of app routes. Next, you’ll render the components on the server.

7.2.2. Rendering components on the server

Phew! You’ve reached the critical juncture—the climax of the story, so to speak. (I saw you roll your eyes.) This section covers the core of getting your components rendered on the server. The goal is to end up with a string representation of the DOM that can be sent as the response to the browser (figure 7.11).

Figure 7.11. Rendered output for the HTML in string form

Rendering your components on the server has two parts. Let’s imagine what happens when the user requests the /cart route. Figure 7.12 shows this flow.

Figure 7.12. The two-step process of rendering a valid HTML route

Here are the steps:

1.  Each view request must render the React tree based on the route matched with React Router. For /cart, this includes the App component, Cart component, and Item component. You saw the App component in chapter 4, and we’ll go over the other components in this chapter.

2.  The final request must contain a complete HTML page with head and body tags. Your core App components don’t include this markup. Instead, you need to create an HTML.jsx component that handles the wrapper markup. Think of this as your index.html file.

These two steps require you to render twice on the server. (This method has alternatives, but they all require additional templating languages and setup. If you want to explore one of these other options, either EJS or Pug work nicely with Node.js.) The first React app I worked on used Pug. Although there’s nothing wrong with this approach, it presents challenges. For one, you need to be up-to-date on yet another library. Also, it doesn’t play as nicely with some of the cool tools available for your workflow such as the Webpack Dev Server.

Building your index component

First, let’s render a basic HTML page so you have a container to put your components into. If you’ve been following along, you can continue in the current branch. If you’ve gotten lost or want to skip to the next checkpoint, you can change branches to the chapter-7-rendering branch (git checkout chapter-7-rendering). The following listing shows the React component that represents your root HTML container. Add this code to html.jsx.

Listing 7.6. HTML container—src/components/html.jsx
import React from 'react';
import PropTypes from 'prop-types';

const HTML = (props) => {                                     1
  return (
    <html lang="en">                                          2
      <head>                                                  2
        <title>All Things Westies</title>
        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/semantic-ui/2.2.2/semantic.min.css"
        />                                                    3
        <link rel="stylesheet" href="/assets/style.css" />
      </head>
      <body>                                                  2
        <div                                                  4
          id="react-content"
          dangerouslySetInnerHTML={{ __html: props.
     renderedToStringComponents }}
        />
      </body>
    </html>
  );
};

HTML.propTypes = {
  renderedToStringComponents: PropTypes.string                5
};

export default HTML;

  • 1 Will be rendered only on the server, will never have state and can be a pure (stateless) component represented by a function.
  • 2 Build basic HTML structure with <html>, <head>, and <body> tags.
  • 3 Include SemanticUI CSS library.
  • 4 Where rendered React component markup for the current route will go
  • 5 Add prop type string to indicate rendered components will be provided as string.

The rendered React component markup will be passed in as a string. Because you’re injecting HTML, you must use dangerouslySetInnerHTML to insert the DOM elements. The most important piece of this React component is that it takes in the rendered HTML that makes up the rest of the component tree. In the next section, you’ll render the main component tree into the html.jsx component.

Remember that components are always rendered only once on the server, so only the first React lifecycle is triggered. Components that are used only on the server (such as html.jsx) can be stateless if they don’t rely on componentWillUpdate.

dangerouslySetInnerHTML

The dangerouslySetInnerHTML property is provided by React to allow you to inject HTML into React components. Generally speaking, you shouldn’t use this property. But sometimes you need to. Every rule has exceptions!

What’s really happening when you set this attribute? Under the hood, React is setting the innerHTML property. But setting innerHTML is a security risk. It can expose you to cross-site scripting (XSS) attacks.

React considers using dangerouslySetInnerHTML a best practice because it reminds you that you don’t want to be setting innerHTML most of the time. For more information, see http://mng.bz/69Ne.

7.2.3. Using renderToString to create the view

The next step is rendering output into the HTML container from listing 7.6. In the following listing, you can see how to call renderToString twice to get your main content rendered into an HTML page. Update the renderView middleware with this code.

Listing 7.7. Render HTML output in the middleware—src/middleware/renderView.jsx
import React from 'react';                                              1
import { renderToString } from 'react-dom/server';                      2
import { match} from 'react-router';
import routes from '../shared/sharedRoutes';
import HTML from '../components/html';                                  3

export default function renderView(req, res, next) {
  const matchOpts = {
    routes,
    location: req.url
  };
  const handleMatchResult = (error, redirectLocation, renderProps) => {
    if (!error && !redirectLocation && renderProps) {
      const app = renderToString(<div>App!</div>);                      4
      const html = renderToString(
       <HTML renderedToStringComponents ={app} />                       5
      );
      res.send(`<!DOCTYPE html>${html}`);                               6
    } else {
      next();
    }
  };
  match(matchOpts, handleMatchResult);
}

  • 1 Include React because of JSX in middleware (this is why it’s a .jsx file).
  • 2 Import renderToString function from React DOM library.
  • 3 Include the HTML component.
  • 4 Call renderToString on placeholder <div>.
  • 5 Call renderToString on HTML component, inject rendered app content into component.
  • 6 Send composed string back on response, append DOCTYPE tag to make markup valid.

Calling renderToString on the placeholder <div> creates the page content that will be inserted in the HTML component.

In the next section, you’ll replace this with the App component.

Rendering components

The final step is to completely render a route inside the middleware. This requires the following:

  • Dynamically rendering app.jsx and a child component based on the route (for example, the cart and all its children)
  • Taking the string output from the render and passing it into html.jsx as a property

You already built the App component in chapter 4. It’s in src/components/app.jsx. You now need to add the Cart component.

The Cart component renders the items in a user’s cart and displays the total cost. It also has a Checkout button. For now, this component has placeholder text (the linter will complain, but you’ll fix this soon). In the next section, you’ll add Redux and data fetching, and the cart will dynamically render the items you pass into it. You already have a cart.jsx from chapter 4. Replace the existing code with the following listing.

Listing 7.8. Cart component—src/components/cart.jsx
import React, { Component } from 'react';

class Cart extends Component {
  render() {
    return (
      <div className="cart main ui segment">
        <div className="ui segment divided items">          1
          Items will go here.
        </div>
        <div className="ui right rail">
          <div className="ui segment">
            <span>Total: </span><span>$10</span>            2
            <div></div>
            <button
              className="ui positive basic button">         3
              Checkout
            </button>
          </div>
        </div>
      </div>
    );
  }
}
export default Cart;

  • 1 Container for rendering cart items
  • 2 Render total number of items in cart.
  • 3 Checkout button

Now that you have the components for the /cart route, you still need to render them on the server. Listing 7.9 shows you how to take the middleware code and update it to work with dynamic routes.

Note

If you’re using React Router 4, check out appendix B for an overview of how to handle the routes on the server.

Listing 7.9. Render—src/middleware/renderView.jsx
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';       1
import routes from '../shared/sharedRoutes';               2
import HTML from '../components/html';

export default function renderView(req, res, next) {
  const matchOpts = {
    routes,
    location: req.url
  };
  const handleMatchResult = (
                              error,
                              redirectLocation,
                              renderProps
                            ) => {                         3
    if (!error && !redirectLocation && renderProps) {
      const app = renderToString(
        <RouterContext                                     1
          routes={routes}                                  2
          {...renderProps}                                 3
        />
      );
      const html = renderToString(<HTML renderedToStringComponents={app} />);
      console.log(`<!DOCTYPE html>${html}`);
      res.send(`<!DOCTYPE html>${html}`);
    } else {
      next();
    }
  };

  match(matchOpts, handleMatchResult);
}

  • 1 RouterContext is a React Router component used to properly render your component tree on the server.
  • 2 Pass in shared routes to RouterContext so the location is properly initialized and matches the browser render.
  • 3 Calculated by the match function from React Router, passed into RouterContext, which knows how to pull out the correct component to render.

The key takeaway is to use the renderProps value (passed into your callback from React Router). This lets the Router know which component to render. It’s also how you make the routing consistent on both the server and the browser.

So far, you’ve learned how to take advantage of React’s renderToString to render your components on the server. But you also need to be able to fetch the data that populates your components on the server. In the next section, you’ll hook up Redux and add a static method called prefetchActions to your React components to indicate what actions need to be called for an individual component to be rendered properly at runtime.

7.3. Adding Redux

As you add Redux to the app, you’ll be rendering the fetched data into the view, as in figure 7.13.

Figure 7.13. The data fetched by Redux on the server will be rendered into the list view in the cart.

One of the trickiest parts of building web apps (or any data-driven, user-facing app) is handling asynchronous code. It’s kind of like when I’m cooking breakfast and my premade breakfast sausages say they’ll take 6–8 minutes to make. Does that mean I should start my eggs 4 minutes after starting to cook the sausages, or 8 minutes? My eggs will cook much faster than the sausages, but I want everything to be ready at the same time.

Similarly, when using Redux on the server, you need your data to be ready before you render your view, or the view created on the server and the view created after the browser code runs won’t always match. You need to guarantee that the data needed by the view is available before you begin to render the view. Figure 7.14 shows the flow of Redux on the server.

Figure 7.14. The Redux flow on the server

You’ll take several steps to make sure you can fetch all the necessary data for the Cart component (the same process can be applied to other parts of the app):

  • Create cart actions and reducers to fetch the cart data
  • Use the renderView middleware to call actions
  • Use a static method on your Cart component to allow the middleware to know what actions it needs to call
  • Display the fetched data in the Cart component

7.3.1. Setting up the cart actions and reducers

The All Things Westies app has the concept of user sessions and being logged out or logged in. Therefore, the app can track items in the user’s cart and then persist this data so the user can come back later and finish the transaction. (You could also use cookies or local storage to do this.)

In this section, you’re going to assume that the user has previously put items in the cart and has come back directly to the cart to finish shopping. You’ll be working with the cart route (http://localhost:3000/cart).

First you need to initialize Redux on the server. The following listing shows the Redux configuration that you need to add to init-redux.es6. If you’ve been following along, you can add this code, or you can switch to chapter-7-adding-redux (git checkout chapter-7-adding-redux).

Listing 7.10. Initialize the Redux store—src/shared/init-redux.es6
import {
  createStore,
  combineReducers,
  applyMiddleware,
  compose } from 'redux';                               1
import thunkMiddleware from 'redux-thunk';              2
import loggerMiddleware from 'redux-logger';            3
import cart from './cart-reducer.es6';                  4

export default function (initialStore = {}) {
  const reducer = combineReducers({                     5
     cart
  });
  const middleware = [
                       thunkMiddleware,
                       loggerMiddleware
                     ];                                 6
  return compose(
      applyMiddleware(...middleware)
    )(createStore)(reducer, initialStore);              7
}

  • 1 Import all functions needed from Redux.
  • 2 Import Thunk middleware so you can use asynchronous actions.
  • 3 Import logger middleware to help with debugging—on the server, it’ll log in terminal.
  • 4 Import cart reducer.
  • 5 Create the root reducer, which will eventually have other subreducers for user, product, and blog data.
  • 6 Set up the middleware.
  • 7 Compose the middleware and reducers to create new store.

You also need to create the action you’ll be calling to fetch the cart data. The cart needs to know what items are currently in the user’s cart. You add this code in the cart-action-creators.es6 file, shown in the following listing.

Listing 7.11. Cart actions—src/shared/cart-action-creators.es6
import fetch from 'isomorphic-fetch';

export const GET_CART_ITEMS = 'GET_CART_ITEMS';               1

export function getCartItems() {                              2
  return (dispatch) => {
    return fetch('http://localhost:3000/api/user/cart',
      {                                                       3
        method: 'GET'
      }
    ).then((response) => {
      return response.json().then((data) => {                 4
        return dispatch({
          type: GET_CART_ITEMS,                               5
          data: data.items                                    5
        });
      });
    })
  };
}

  • 1 String constant for the action
  • 2 On the server, the getCartItems action will be called from the renderView middleware.
  • 3 Use fetch API to get cart data from the Node.js server.
  • 4 On a success response, read JSON from the response.
  • 5 Take parsed JSON; return action object.

In that last line, the type is the string constant, and the data is the items array from the JSON. Listing 7.12 shows what this data looks like.

For the All Things Westies app, all the data will be handled on your Node.js server. Everything is mocked out in JSON files. The cart data is shown in the following listing. It’s already provided for you in the branch.

Listing 7.12. Mock cart data—data/cart.json
{
  "items": [                                                  1
    {
      "name": "Mug",                                          2
      "price": 5,                                             3
      "thumbnail":
 http://localhost:3000/assets/cart-item-placeholder.jpg     4
    },
    {
      "name": "Socks",                                        2
      "price": 10,                                            3
      "thumbnail":
 http://localhost:3000/assets/cart-item-placeholder.jpg     4
    },
    {
      "name": "Dog Collar",                                   2
      "price": 15                                             3
    },
    {
      "name": "Treats",                                       2
      "price": 15,                                            3
      "thumbnail":
 http://localhost:3000/assets/cart-item-placeholder.jpg     4
    }
  ]
}

  • 1 JSON returns object with an array of cart items.
  • 2 Each item has a name that’s a string.
  • 3 Each item has a price that’s a number.
  • 4 Each item has a thumbnail that’s a string with an image URL.

After the cart data is fetched from the server by the getCartItems action, your cart reducer will put the data in the Redux store. Listing 7.13 shows the code required to set up the cart reducer. Remember that the job of the reducer is to take in the current store and an action. It then uses the action to update the store and return a new state of the app. Add the code from the listing to the cart reducer.

Listing 7.13. Cart reducers—src/shared/cart-reducer.es6
import {
  GET_CART_ITEMS
} from './cart-action-creators.es6';               1

export default function cart(state = {}, action) {
  switch (action.type) {                           2
    case GET_CART_ITEMS:
      return {
        ...state,
        items: action.data                         3
      };
    default:
      return state;
  }
}

  • 1 Use an action string constant to ensure consistency.
  • 2 Read the type from the action passed into reducer to see if you should handle the action in this reducer.
  • 3 Write data from theaction into current state.

Good job! You’ve created all the pieces of Redux (setup, action, and reducer) needed to fetch the route data in the renderView middleware. Next, you’ll implement the data-fetching logic so you can see the data loaded into the view.

7.3.2. Using Redux in renderView middleware

Now you need to include the store in your renderView middleware. The following listing shows how to add this in.

Listing 7.14. Adding the Redux store to your middleware—src/middleware/renderView.jsx
import initRedux from '../shared/init-redux.es6';                 1

export default function renderView(req, res, next) {
  const matchOpts = {...};
  const handleMatchResult = (error, redirectLocation, renderProps) => {
    if (!error && !redirectLocation && renderProps) {
      const store = initRedux();                                  2
      // ... more code
    }
  }
  // ... more code
}

  • 1 Include initialization code in middleware.
  • 2 Call initialization function and store it on a const variable called store.

After you have the store in the middleware, you can dispatch actions on the server. But you want to be smart and dispatch only the actions for the current route. To do that, you need to extend your React components to be able to declare your actions on a per-route basis.

Setting up initial actions

There are a couple of valid ways to declare the data needs for a route. You can store this information with the route declaration or you can put the data declaration on the components. I’m going to show you how to put the data declaration on the components. The Cart component knows which action creator functions need to be called to fetch the appropriate data for the cart view. Later, the renderView middleware will use these function references and call them to get the JSON data responses needed to populate the store.

With React Router, you can easily access any component you’ve declared in a route component. By declaring the data needs on the components that are in your shared routes, you can compose a list of actions from multiple components in the middleware. The next listing shows how the Cart component declares its action needs. In this case, it needs the data fetched by the getCartItems action. To indicate that, it stores a reference to the action creator function. The renderView middleware will call this action.

Listing 7.15. Declaring initial actions—components/cart.jsx
import { getCartItems } from '../shared/cart-action-creators.es6';

class Cart extends Component {

  static prefetchActions() {                                1
    return [                                                2
      getCartItems                                          3
    ];
  }
  render(){
    return {
      <div className="cart main ui segment">...</div>
    }
  }
}

  • 1 Declare static function.
  • 2 Return array so you can list multiple action creators (actions hold business logic of how to fetch data and update app state).
  • 3 List action creators needed for component (don’t call them here, just pass them as function references).

Remember that static functions aren’t part of the class instance. They don’t have access to any specifics of the component instance such as properties or state. Any context for a static function needs to be passed in from the caller. In this case, you don’t need any context.

Static functions

Static functions live on the React class or any JavaScript class. These functions can be called without instantiating an instance of the class.

Why would you want to use a static function? Usually, they’re used to provide a utility. In an isomorphic application, you can use static functions to define the data calls that a React component needs to be rendered.

7.3.3. Adding data prefetching via middleware

Now that your Cart component is declaring its own data needs by defining the actions that need to be called for it be rendered properly, you can use the middleware to fetch the data before you render the components. The first thing you need to add is code that collects all the actions from the components on renderProps. The following listing shows you what to add to get this working.

Listing 7.16. Calling all initial actions on components—src/middleware/renderView.jsx
export default function renderView(req, res, next) {
  const matchOpts = {...};
  const handleMatchResult = (error, redirectLocation, renderProps) => {

    if (!error && !redirectLocation && renderProps) {
      const store = initRedux();
      let actions = renderProps.components.map(
       component) => {                                                   1
        if (component) {
          if (component.displayName &&
            component.displayName.toLowerCase().indexOf('connect') > -1
          ) {                                                              2
            if (component.WrappedComponent.prefetchActions) {
              return component.
               WrappedComponent.prefetchActions();                       3
            }
          } else if (component.prefetchActions) {
            return component.prefetchActions();                            3
          }
        }
        return [];
      });
    actions = actions.reduce((flat, toFlatten) => {                        4
      return flat.concat(toFlatten);                                       4
    }, []);                                                                4
  };
  match(matchOpts, handleMatchResult);
}

  • 1 Run map on each component returned by renderProps.
  • 2 Check if component is wrapped by looking for ‘connect’ (check for and call prefetchActions on WrappedComponent property).
  • 3 Call prefetchActions on component if function exists, should always return array.
  • 4 map function will create an array of arrays—reduce it so you can concat them into one array.

This code enables the server to know what actions to call for the route. Remember, you can call prefetchActions only on components that are known to React Router.

The actions array is now a list of action creators that can be called. Next you’ll set them up to dispatch and then use Promise.all to wait until all your initial actions complete before rendering the React components. Remember, you’re calling only the actions required for the current route. The following listing shows how to add the asynchronous code handling so that you wait to render the components until you have all the data needed for the route.

Listing 7.17. Calling all initial actions on components—src/middleware/renderView.jsx
import { Provider } from 'react-redux';                               1

export default function renderView(req, res, next) {
  const matchOpts = {...};
  const handleMatchResult = (error, redirectLocation, renderProps) => {
    if (!error && renderProps) {
      const store = initRedux();

      let actions = renderProps.components.map((component) => {...});
      actions = actions.reduce((flat, toFlatten) => {...}, []);

      const promises = actions.map((initialAction) => {               2
        return store.dispatch(initialAction());                       2
      });
      Promise.all(promises).then(() => {                              3
          const app = renderToString(
            <Provider store={store}>                                  4
              <RouterContext routes={routes} {...renderProps} />
            </Provider>
          );
          const html = renderToString(<HTML html={app} />);
          return res.status(200).send(`<!DOCTYPE html>${html}`);
      });
    }
  }
  match(matchOpts, handleMatchResult);
}

  • 1 Import Provider component from React Redux (to create components with Redux on the server).
  • 2 Call store.dispatch on each action creator.
  • 3 Call Promise.all on your actions—after they’ve resolved, you can render components.
  • 4 Wrap React Router component inside Provider.

After you wrap the React Router component inside Provider, you pass the store into Provider. The store will now be updated with all the data fetched and updated by your actions.

Displaying data in the cart

Because the app is now fetching the data for the route, you can make your Cart component update with the dynamic data. The following listing shows the additional logic in your Cart component that displays each cart item.

Listing 7.18. Complete Cart component—components/cart.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { getCartItems } from '../shared/cart-action-creators.es6';
import Item from './item';

class Cart extends Component {

  static prefetchActions() {}

  constructor(props) {
    super(props);
    this.proceedToCheckout = this.proceedToCheckout.bind(this);
  }

  getTotal() {
    let total = 0;
    const items = this.props.items;
    if (items) {
      total = items.reduce((prev, current) => {                   1
        return prev + current.price;
      }, total);
    }
    return total;
  }

  proceedToCheckout() {
    console.log('clicked checkout button', this.props);          2
  }

  renderItems() {
    const components = [];
    const items = this.props.items;
    if (this.props.items) {
      this.props.items.forEach((item, index) => {                 3
        components.push(<Item key={index} {...item} />);          3
      });
    }
    return items;
  }

  render() {
    return (
      <div className="cart main ui segment">
        <div className="ui segment divided items">
          {this.renderItems()}                                    3
        </div>
        <div className="ui right rail">
          <div className="ui segment">
            <span>Total: </span><span>${this.getTotal()}</span>   1
            <button
              onClick={this.proceedToCheckout}
              className="ui positive basic button"
            >
                Checkout
            </button>
          </div>
        </div>
      </div>
    );
  }
}

Cart.propTypes = {
  items: PropTypes.arrayOf(
    PropTypes.shape({
      name: PropTypes.string.isRequired,
      price: PropTypes.number.isRequired,
      thumbnail: PropTypes.string.isRequired
    })
  )
};

function mapStateToProps(state) {
  const { items } = state.cart;                                   4
  return {
    items
  };
}

function mapDispatchToProps(dispatch) {
  return {
    cartActions: bindActionCreators([getCartItems], dispatch)
  };
}

export default connect(
                        mapStateToProps,
                        mapDispatchToProps
                      )(Cart);                                    4

  • 1 Calculate total based on cart items on prop, use reduce to return sum of all prices.
  • 2 Placeholder click handler for button (you won’t see console.log output because you haven’t hooked up the browser code).
  • 3 Render cart items with the Item component, create new Item for each item in items array.
  • 4 Hook up cart to Redux so it can get cart items on props.

Restart the app. Then if you navigate to /cart, you’ll see each item fully rendered. But the Checkout button doesn’t work! You won’t see any console output when you click it because you haven’t rehydrated the React tree in the browser. Chapter 8 will teach you how to make the server/browser handoff and get the browser-specific code working.

Summary

In this chapter, you learned how to implement server-side rendering. You wrote Express middleware that handles the routing and rendering for your app. And you learned how to use Redux on the server and prefetch your data to create the Cart component on the server.

  • Express can be used to render your views.
  • You can use React Router on the server so you don’t have to duplicate your routing code.
  • You create and use custom Express middleware to determine the route you’re on and render the components for the current route.
  • React provides a method called renderToString that allows you to return a string including the markup from your route.
  • Instead of letting the normal app flow fetch your data, you prefetch the data needed for your components.
..................Content has been hidden....................

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