Chapter 2. A sample isomorphic app

This chapter covers

  • Setting up your build to work on the server and the browser
  • Rendering the views
  • Fetching data with Redux
  • Handling the request on the server
  • Serializing the data on the server
  • Deserializing the data on the browser

In this chapter, I’m going to walk you through all the key parts of an isomorphic app built with React, Redux, Babel, and webpack. Think of this chapter as an opportunity to dip your feet in before taking the full plunge. You won’t need to understand all the details, but by the end you’ll have a sense of how all the pieces fit into the app, and that will provide you context for working through the rest of the book.

If you’re already proficient in building React apps, this chapter along with chapters 7 and 8 will get you started. If you’re not already comfortable with React, I’ll take you through React and the other building blocks for the app in chapters 3 through 6.

2.1. What you’ll build in this chapter: recipes example app

First, let’s look at the app you’ll build in this chapter. Figure 2.1 shows the recipes app you’ll construct. In this chapter, you’ll build the homepage for the app, which will show a list of top recipes and a featured recipe. Getting all the pieces of your first isomorphic app together is an involved process, so for your first pass at building an isomorphic app, I’ll keep the end goal simple.

Figure 2.1. The home screen for the recipes app you’ll build in this chapter

This app will have a single route and won’t handle any user interaction. Isomorphic architecture is overly complex for such a simple app, but the simplicity will allow me to present the core concepts. In later chapters (starting in chapter 4), I’ll teach you how to build a more complex app with routing and user interaction.

In chapter 1, we went over the three main steps in an isomorphic app: server render, initial browser render, and single-page application behavior. In this chapter, you’ll learn how to create an application that can take advantage of this render flow. You’ll build the server, serialize the data, hydrate the data on the browser, and render the browser view. Figure 2.2 shows how the pieces that you’ll build in this chapter fit together.

Figure 2.2. The application flow you’ll build in this chapter—initial render from the server and browser hydration

Definition

Serializing occurs when you take JSON and turn it into a string. This string is easy to send between applications and can be sent to the browser as part of the server response. Hydrating (or deserializing) the data means taking the string and converting it back into a JSON object that can be used by the app in the browser.

2.1.1. Building blocks: libraries and tools

To write the recipes app and make it run as an isomorphic app, you’ll use several JavaScript libraries:

  • Babel and webpackCompilation and build tools. Babel will compile the code into a version understood by the JavaScript compiler regardless of browser implementation. Webpack will allow you to bundle code for the browser, including libraries that are installed via npm (Node packet manager).
  • ExpressEnables simple server-side routing for rendering the view.
  • React and ReduxThe view and the business logic. You’ll write your React components using a template language called JSX, which is the standard for React. It looks a lot like HTML but allows you to insert logic and variables into your view code.
  • Semantic UISimplify the CSS by providing a standard set of classes. The focus of this book isn’t CSS, so this will make following along in the various examples easier.
Semantic UI for layout and CSS

Semantic UI is a CSS library that provides basic styling and predefined layouts, components, and grids. I’ve used Semantic UI’s CSS for the layout in the view for the recipes example app. Documentation on Semantic UI can be found at http://semantic-ui.com.

Before you get started building and running the code, let’s look at what parts of the code run on the server, on the browser, and on both environments. Figure 2.3 maps the various parts of the app (React components, Redux actions and reducers, entry points for the server and browser) to the environments they run in. Some code (for example, React and Redux) will run in both environments. Other code is specific to either the server or browser (for example, Express for the server).

Figure 2.3. An overview of how the various libraries and build tools are used across the two environments the code runs in (server and browser). The files listed here can be found in the code example. See the next section for instructions on downloading the code.

The diagram also demonstrates what build tools are used for which environments. Webpack will be used to build only the browser code. The server code will be built with npm scripts. Babel is used in both environments.

2.1.2. Download the example code

You can download the code for this example from GitHub at https://github.com/isomorphic-dev-js/chapter2-a-sample-isomorphic-app.git. I recommend you do so, as all the required packages and code are already set up for you, and you can easily follow along.

To check out the code from GitHub, run the following command inside the directory you want to clone the project into:

$ git clone https://github.com/isomorphic-dev-js/chapter2-a-sample-
     isomorphic-app.git
Tip

If you need help getting started with Git, Learn Git in a Month of Lunches by Rick Umali (Manning, 2015) is a good resource.

Look at the key folders and files in the app. Figure 2.4 shows the core folder structure of the app (other files and folders are also in the repo, but the figure calls out what’s relevant for this chapter). You can map this to the environments shown in figure 2.3. The entry points for the server (app.es6) and the browser (main.jsx) are of particular importance because all the code that’s environment specific will go in these files.

Figure 2.4. Folder organization and top-level build and configuration files. A subset view of the folder structure for the recipes app that shows what files pertain to build and tools, the server, and the browser.

2.2. Tools

After you’ve cloned the repo, it’s time to run the app. The code for this chapter includes a simple Node.js server that will render the recipes app homepage. The Node.js server will also serve up the data for the recipes. For this example, the recipes will be loaded from a JSON file. In the real world, you’d want to use a database or API to be able to persist the data.

To get everything up and running for the recipes app, you’ll learn about the following:

  • Setting up the development environment and installing packages with npm
  • Compiling and running the server code with Babel
  • Building the browser code with webpack
  • Handling multiple code entry points

2.2.1. Setting up the environment and installing packages

You’ll be using Node.js to run the web server. It’s suitable for many use cases but especially good for isomorphic apps, as it allows you to write the entire stack of the application in JavaScript.

Node.js download and docs

This chapter assumes that you have basic familiarity with Node.js and have already installed it on your machine. To get the latest version of Node.js and stay up-to-date on the docs, visit https://nodejs.org. Node.js comes with npm.

I’m running Node.js version 6.9.2. If you run a major version lower than 6, you may need additional Babel packages that aren’t covered in this book. If you run a major version higher than 6, you may not need all the Babel packages included.

Before you get started running the server, you need to install all the npm packages for this example. You’ll find a list of all npm packages as well as documentation for the packages listed in tables 2.1 and 2.2 at www.npmjs.com. The packages needed for the recipes app are already provided in the package.json of the project. To install them, run the following command in your terminal:

$ npm install

Two groups of packages get installed:

  • devDependencies includes build tools such as Babel and webpack. Packages in the devDependencies section of package.json don’t get installed when the NODE_ENV variable is set to production. See table 2.1 for additional information.
  • dependencies include any libraries that are required to run the application. See table 2.2 for additional information.

Table 2.1. List of devDependencies (for building and compiling the app)

Package

Description

babel-core The main Babel compiler package. More information at https://babeljs.io.
babel-cli The Babel command-line tool. Used to compile the server code.
babel-loader Webpack loader for using Babel with webpack.
babel-preset-es2015, babel-preset-react, babel-plugin-transform-es2015-destructuring, babel-plugin-transform-es2015-parameters, babel-plugin-transform-object-rest-spread Babel has many preset options, so we include the ones relevant to this project. These packages include rules for React, ES6, and compiling JSX.
css-loader Webpack loader for using CSS inside webpacked files.
style-loader Webpack loader for using CSS inside webpacked files.
webpack A build tool for compiling JavaScript code. Enables the use of ES6 and JSX in the browser as well as the use of packages written for Node.js (as long as they’re isomorphic). More information at https://webpack.js.org.
Table 2.2. Core dependencies for the recipes app

Package

Description

express A Node.js web framework that provides routing and route-handling tools via middleware. More information at https://github.com/expressjs.
isomorphic-fetch Enables the use of the fetch API in the browser and the server.
react The main React package. More information at https://facebook.github.io/react/.
react-dom The browser- and server-specific DOM-rendering implementations.
redux Core Redux code.
react-redux Provides support to connect React and Redux.
redux-thunk Redux middleware.
redux-promise-middleware Redux middleware that supports promises.

Open the code in your editor and find the package.json file. You’ll see all the libraries listed in the preceding tables. Now that you understand the dependencies of the example app, you can set up and run the server.

2.2.2. Running the server

To get the server running (so you can test the API, as shown in figure 2.5), you first need to build the server code using Babel. You’re probably wondering why you need to compile code for a language that’s interpreted at runtime. This step is required for two reasons:

  • Writing the latest and greatest code with ES6 language featuresJavaScript is also known as ECMAScript (ES). ES6 is a recent version that adds many language features, including classes, maps, and promises. Most of the ES6 spec already runs on Node.js 6.9.2 or later. But if you want to use upcoming features from ES7 (the next version of JavaScript) or use the import statement instead of require statements, a compile is still required. The examples in the book take advantage of import statements.
  • Because the server will render components, you’ll need to run JSX on the serverJSX is the template language that React uses to declare the view. Node.js doesn’t understand how to run JSX, so you’ll need to compile the JSX into JavaScript before using it on the server. I discuss JSX later in the chapter.
Figure 2.5. Expected output from the recipes API endpoint after the server is running

Note

I use two extensions for files in the project instead of .js. For files written with ES6, I use the extension .es6 to indicate the need to compile the file with Babel. For files that include React components, I use .jsx to indicate the presence of a JSX template. This lets us pass only the files we want to the Babel compiler and also makes it easy to distinguish between working and compiled files. The .jsx extension is also picked up by some editors and IDEs as a signal to use different syntax highlighting.

To build and run the server, you use the Babel tools and configuration that are set up in the npm packages. Two additional pieces of code are required to make this all work. First, to use Babel, you need a Babel configuration. The best way to do that is to create a .babelrc configuration file. Inside .babelrc, I’ve called out two presets for the compiler to use: es2015 and react. The following listing shows this code, which is already included in the repo.

Listing 2.1. Babel configuration—babelrc
{
  "presets": ["es2015", "react"],               1
  "plugins": [                                  2
    "transform-es2015-destructuring",           3
    "transform-es2015-parameters",              3
    "transform-object-rest-spread"              3
  ]
}

  • 1 Packages that contain groups of plugins, making configuration easier and quicker
  • 2 Plugins are the base unit in Babel, and each plugin is responsible for one type of update.
  • 3 Three plugins allow use of spread operator (...) so you can easily work with and update objects.

The presets listed here map to the preset packages you installed earlier in the chapter. This will ensure that ES6 code and JSX template code are compiled properly.

Note

The babel-cli and related tools are powerful and flexible. Visit https://babeljs.io to find out what else Babel can do. For example, Babel supports sourcemaps for compiled files. Also, if you prefer different build tools, you can use Babel with most of the popular JavaScript build tools.

As for the other required piece of code, I’ve set up this project to use Babel inline in development mode on the server. You don’t have to precompile any of the code to have it run on the server. The server.js file is just two lines of code. The following listing shows the code, which is already included in the repo.

Listing 2.2. Running the server with Babel—src/server.js
require('babel-register');          1
require('./app.es6');               2

  • 1 Include Babel—it will parse all code that comes after it (not recommended for production use).
  • 2 Include root application code for server.

With everything configured and set up, all you have to do to start the Node.js server is run the following:

$ npm start

The Node.js server is now running on localhost at port 3000. Load http://localhost:3000/recipes and you’ll see a JSON object with several recipes. Sample output will look like the JSON object in figure 2.5. Remember, the server plays two roles in the recipes app: it renders the initial view and provides the data API.

Next, we’ll explore how webpack uses Babel to create the browser code.

2.2.3. Building the code for the browser with webpack

Every time I learn a new build tool, I spend hours being frustrated, wondering why I’m ramping up on yet another library that may or may not give me long-term workflow improvements. Although webpack has a steep learning curve, the time I invested to learn it has been well worth it. Each time I’ve run into something new that I need to do with build scripts, I’ve found that webpack can get the job done. Additionally, it has a strong community and has become one of the top choices for modern web apps.

Webpack is a build tool that you can run from the command line and configure via a JavaScript configuration file. It supports a wide range of features:

  • Using loaders to compile ES6 and JSX code and load static assets via loaders
  • Code splitting for smart bundling of code into smaller packages
  • Ability to build code for the server or the browser
  • Out-of-the-box sourcemaps
  • The webpack dev server
  • Built-in watch option

You’re going to use webpack to build the browser bundle. Unlike on Node.js, browser support for the latest version of JavaScript is inconsistent. To write code with ES6, I need to compile it into a format the browser can read (ES5). Also, as on the server, JSX must be compiled into a format that the JavaScript compiler can understand. To do that, you’ll take advantage of the webpack config and then run that config via the npm script you saw in the previous section. To run the webpack script, you also run this:

$ npm start

The package.json includes a prestart script that runs the command for webpack.

Note

Although it’s possible to build your Node.js server with webpack, that will present challenges for building and testing later and require you to run two Node.js servers. It’s preferable to use webpack only for building the browser code.

As on the server, you’ll use Babel to compile the code. The webpack configuration file is located at the top level of the project and is a JavaScript module. The code is already included in the repo. The following listing explains how it works.

Listing 2.3. Webpack configuration—webpack.config.js
module.exports = {
  entry: "./src/main.jsx",                                     1
  output: {
    path: __dirname + '/src/',                                 2
    filename: "browser.js"                                     3
  },
  module: {
    rules: [
      {
        test: /.(jsx|es6)$/,                                  4
        exclude: /node_modules/,                               5
        loader: "babel-loader"                                 6
      },
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx', '.css', '.es6']                7
  }
};

  • 1 Defines input starting point or entry path—file runs only on browser
  • 2 Output folder—the dist directory where we build all other files
  • 3 Output filename
  • 4 Regular expression that tells loader what files to apply loader to
  • 5 Excludes node_modules because these files are already compiled and production ready
  • 6 Defines loader to apply to matched files, for Babel to compile ES6 and JSX
  • 7 Supported file extensions—empty file extension allows import statements with no extension (your src files have one extension but your compiled files have a different one)

You can also load any CSS you need via files included in webpack. To do that, you need to define a loader that will handle any require statements that include a .css extension. Because our app is isomorphic and you aren’t using webpack for the server, it’s important to include CSS only in files that will be loaded in the browser. In this case, the CSS include will be in main.jsx:

{test: /.css$/,loaders: ['style-loader', 'css-loader']}

That’s all you need for now. For a full intro to webpack, make sure to read chapter 5.

2.3. The view

This section and the following pieces explore the specific technologies that will be used to wire the app together. Figure 2.6 shows how each piece fits into the app lifecycle.

Figure 2.6. How React and Redux fit into the application flow

The key takeaway here is that the app lifecycle is single directional. Anytime the app state is updated, the view receives an update and displays it to the user (step 4). When the view receives user input, it notifies the app state (Redux) to make an update (step 2). The view doesn’t worry about the implementation of the business logic, and app state doesn’t worry about how it’ll be displayed.

2.3.1. React and components

When building apps, the user interface is the most important piece. I enjoy building apps with great UIs. In these apps, users easily find what they’re looking for and can interact with the app without frustration. React makes this process easier. I find that its concepts map well to the way I think about piecing together good UIs.

To build the view for the recipes app, I’ll show you how to take advantage of React to implement a declarative view that can be used to render both on the server and the browser. React offers a render cycle that allows you to easily separate which code will run on both the server and browser and which will run on only the browser. Additionally, React comes with built-in methods for constructing the DOM on both the server and the browser.

First let’s talk about the idea of components. Look at the example app in figure 2.7. You could write this whole app as just one block of HTML, but it’s best practice to break this UX into small components. In the figure, you can see how you’d break up the recipes app into components. To keep it simple, I’ve created only three components. In a real app, one with many views, I’d create even smaller components to increase my ability to compose components together. This also reduces code duplication and speeds up development.

Figure 2.7. The recipes app is divided into three main components. By composing them together, the app is created.

The way you build components with React is by writing JavaScript modules and declaring your view in JSX. The next section provides an introduction to JSX.

2.3.2. Using JSX

React uses a template language called JSX. For the most part, JSX looks and acts like normal HTML, which makes it easy to learn and use. JSX consists of HTML tags (which can also be additional React components) and sections of code that are JavaScript. The syntax is presented here:

You can see that at the point where you reference JavaScript, you must wrap your code in {}. This indicates to the compiler that the code inside the brackets is executable. JSX is compiled by Babel into pure JavaScript. You could write your components with the base React functions, but that’s slower and less readable.

Components can display data passed in via their properties, called props. Props are similar to HTML attributes and can be written in the opening tag of any JSX element. You’ll find more information in chapter 3, which covers JSX and React properties and state.

The recipe app has four React components: the component that renders the HTML wrapper (used only on the server), the app wrapper component (the root of the React tree), and two view components called Featured and Recipes.

2.3.3. App wrapper component

First, we’ll look at main.jsx and app.jsx to get the root of the app setup. If you want to follow along in this section, you can switch branches to the react-components branch (git checkout react-components). The starting branch for a section provides a skeleton sample that you’ll add the code listings into. If you’d like to see the complete code for this section, you can switch to the react-components-complete branch (git checkout react-components-complete).

To render the components in the browser, you need to set up React in main.jsx code. The following listing shows you what to add to make the components render in the browser. Add the code to src/main.jsx.

Listing 2.4. Browser entry point—src/main.jsx
import React from 'react';                                     1
import ReactDOM from 'react-dom';                              1
import App from './components/app.jsx';                        2
require('./style.css');                                        3

ReactDOM.render(                                               4
  <App />,                                                     4
  document.getElementById('react-content')                     4
);

  • 1 Imports React dependencies
  • 2 Includes root App component
  • 3 Includes styles
  • 4 Renders the App component into the DOM—second parameter indicates the DOM element React should be rendered into

App is a container component. It knows about the business rules and data required by its children. More importantly, it’s aware of the application state. In this case, that means it will be connected to Redux later in the chapter. The following listing shows the App component. Replace the placeholder code in the repo (in src/components/app.jsx) with the code from the listing.

Listing 2.5. App (top-level component)—src/components/app.jsx
import React from 'react';
import Recipes from './recipes';
import Featured from './featured';

class App extends React.Component {                            1

  render() {                                                   2
      return (
          <div>
            <div className="ui fixed inverted menu">
              <div className="ui container">
                <a href="/" className="header item">
                  Recipes Example App
                </a>
              </div>
            </div>
            <div className="ui grid">
              <Recipes {...this.props}/>                        3
              <Featured {...this.props.featuredRecipe}/>        3
            </div>
            <div className="ui inverted vertical footer segment">
            Footer
            </div>
          </div>
      );
  }
}

export default App;

  • 1 To declare a React component that uses state, create a class that extends the base component class.
  • 2 Every component that has a render function, must return either null or valid JSX.
  • 3 A component defines the layout of its children.

The App component renders the header and footer but also has two additional React components that it includes as children. Recipes displays the list of recipes returned from the /recipes endpoint. Featured displays just the featured recipe you get back from the server via /featured. These child components require information from the parent, which is passed down in the form of properties.

The data being passed down is from the API and is fetched by Redux and stored in the app state. Run npm start after adding the app.jsx code and you’ll see the header, footer, and some placeholder strings for Recipes and Featured at http://localhost:3000/index.html.

2.3.4. Building child components

The two child components display the properties that are passed into them. They don’t have any awareness of other parts of the application such as Redux. This makes them reusable and loosely coupled to the business logic in the app. The following listing shows the Featured recipe component. Add this code to src/components/featured.jsx, replacing the placeholder code.

Listing 2.6. Featured component—src/components/featured.jsx
import React from 'react';

const Featured = (props) => {
  const buildIngredients = (ingredients) => {                     1
    const list = [];

    ingredients.forEach((ingredient, index) => {
      list.push(
        <li className="item"
            key={`${ingredient}-${index}`}>
          {ingredient}
        </li>
      );
    });

    return list;
  }

  const buildSteps = (steps) => {                                 2
    const list = [];

    steps.forEach((step, index) => {
      list.push(
        <li className="item"
            key={`${step}-${index}`}>
          {step}
        </li>
      );
    });

    return list;
  }

  return (
    <div className="featured ui container segment six wide column">
      <div className="ui large image">
        <img src={`http://localhost:3000/assets/${props.thumbnail}`} />
      </div>                                                     3
      <h3>{props.title}</h3>                                     3
      <div className="meta">
        Cook Time: {props.cookTime}
      </div>                                                     3
      <div className="meta">
        Difficulty: {props.difficulty}
      </div>                                                     3
      <div className="meta">
        Servings: {props.servings}
      </div>                                                     3
      <div className="meta">
         Tags: {props.labels.join(', ')}
      </div>                                                     3
      <h4>Ingredients</h4>
      <div className="ui bulleted list">
        {buildIngredients(props.ingredients)}                    4
      </div>
      <h4>Steps</h4>
      <div className="ui ordered list">
        {buildSteps(props.steps)}                                5
      </div>
    </div>
  );
}

Featured.defaultProps = {                                        6
  labels: [],
  ingredients: [],
  steps: []
}

export default Featured;

  • 1 Function converts an array of ingredients into an array of list items; this is called from the render function.
  • 2 Function takes in the steps array and converts it to a list item, called from render function.
  • 3 Featured component is a container that renders information about the featured recipe; it renders a recipe passed in via props.
  • 4 Function converts an array of ingredients into an array of list items; this is called from the render function.
  • 5 Function takes in the steps array and converts it to a list item, called from render function.
  • 6 Set properties that are arrays to defaults so if there’s no data, the component can still render.

After adding this code, you’ll see the featured recipe displayed but without data (you haven’t hooked it up to any data yet). There’s one more step to show the complete homepage: adding the Recipes component code.

The next Recipes component handles more complex data than Featured. It’s similar in that it only displays recipes data and has no awareness of the rest of the application. The following listing shows the Recipes list component. You’ll add this code to src/components/recipes.jsx, replacing the placeholder code.

Listing 2.7. Recipes component—src/components/recipes.jsx
import React from 'react';

const Recipes = (props) => {

  const renderRecipeItems = () => {                                   1
    let items = [];
    if (!props.recipes) {
      return items;
    }
    props.recipes.forEach((item, index) => {                          2
      if (!item.featured) {
        items.push(
          <div key={item.title+index} className="item">               3
            <div className="ui small image"><img src="" /></div>
            <div className="content">
              <div className="header">{item.title}</div>
              <div className="meta">
                <span className="time">{item.cookTime}</span>
                <span className="servings">{item.servings}</span>
                <span className="difficulty">{item.difficulty}</span>
              </div>
              <div className="description">{item.labels.join(' ')}</div>
            </div>
          </div>
        )
      }
    });
    return items;
  }

  return (
    <div className="recipes ui items six wide column">                4
      {renderRecipeItems()}
    </div>
  );
}

export default Recipes;

  • 1 Function called from JSX in return statement
  • 2 Because you can’t write loops directly in JSX, build an array of items that can be rendered by JSX.
  • 3 Each recipes item is rendered here; data for recipes is passed down via props.
  • 4 The render function is a wrapper for the recipes list and has no state, like featured component.

In both components, the properties are passed in from the parent component. These components are updated only if their parent component receives an update. That ties into the single-direction flow discussed at the beginning of this section. As top-level components receive updates from the app state, they can then pass these changes down to their children. Because there’s no data in the app, you won’t see a visual change at this point—there are no recipes to render!

2.3.5. HTML container

The final React component is the one that the server uses to render the full HTML markup. It’s mostly standard HTML tags but has a couple of spots to insert the rendered markup and the data. The following listing shows the full HTML component. Add this code to src/components/html.jsx so you have a container to render into on the server.

Listing 2.8. HTML template component (server only)—src/components/html.jsx
import React from 'react';

export default class HTML extends React.Component {
  render() {
    return (
      <html>                                                         1
        <head>                                                       1
          <title>Chapter 2 - Recipes</title>
          <link rel="stylesheet"
                href="https://cdn.jsdelivr.net/semantic-ui/2.2.2/
     semantic.min.css" />                                            2
        </head>
        <body>                                                       1
          <div id="react-content"
            dangerouslySetInnerHTML={{
                                      __html: this.props.html
                                    }}/>                             3
          <script
            dangerouslySetInnerHTML={{
                                      __html: this.props.data
                                    }}/>                             4
          <script src="/browser.js"/>
        </body>
      </html>
    );
  }
}

  • 1 Component creates a valid HTML page—html, head, body tags included.
  • 2 CSS referenced from head tag.
  • 3 Component receives rendered HTML as prop.
  • 4 Prop being passed in here is data, a stringified JSON object representing the current state of the app on the server—otherwise the handoff from server to browser can’t happen.

dangerouslySetInnerHTML is used because it’s a prerendered string. Normally, you can’t put HTML in a React component. This special property allows you to bypass this restriction. It’s named that way as a reminder to be cautious and intentional with HTML in components.

Now that all the React components for the app have been created, you’ll set up the business logic for the recipes app.

2.4. App state: Redux

In this section, I’ll show you how to use Redux to build the business logic for the recipes app. The recipes app doesn’t have much user interaction because it’s so simple. But Redux will still be responsible for fetching the data for the app. Chapter 6 presents a complete look at Redux, including handling user interactions.

2.4.1. Understanding Redux

The flow of Redux loosely follows the flow originally defined by Facebook’s Flux architecture. All updates to the app state are single directional. When a change is requested, it’s processed by the business logic (actions), updated in the app state (reducers), and finally returned to the view as part of a brand-new copy of the app state. Figure 2.8 shows how this works.

Figure 2.8. Redux overview: flow of user interaction to store update to view update

The store

Redux is based on the idea of a single root state object for the entire application, commonly referred to as the store. This state can be a combination of multiple deeply nested objects. The recipes app is simple, so it will have just a single root object called recipes.

The store is immutable, meaning changes to the state object always return a new state rather than modifying the existing state. I like to think of this as the model of the application, where the data is stored.

Definition

Immutable objects are read-only. To update an immutable object, you need to clone it. In JavaScript, when you change an object, it affects all references to that object. Mutable changes can have unintended side effects. By enforcing immutability in your store, you prevent this from happening in your app.

Actions

To make updates to state, you dispatch actions. Actions are where most of our business logic takes place. I like to think of actions as the controllers of the app.

An action can be anything in your app. Actions can be used to fetch data (such as getRecipes or getFeatured). They can also be used to update the app state—for example, keeping track of items added to a shopping cart. Think of these actions as discrete messages that describe a single state update. Actions are synchronous by default, but we can include middleware in Redux that allows asynchronous actions.

Actions (which are JavaScript objects) are usually wrapped in action creators, which are JavaScript functions that return or dispatch an action. They’re helper methods that give your code more reusability by centralizing the creation of action objects.

Reducers

Actions are handled by reducers. A reducer takes the input from the action, including any data fetched asynchronously from the server or an API, and inserts it into the proper place in the store. Reducers are responsible for enforcing the immutable requirement of the state object. By using reducers, the actions and the store are decoupled, which gives greater flexibility to the app.

I’ll walk you through setting up Redux and adding actions, reducers, and the code that makes Redux and React work together.

2.4.2. Actions: fetching the recipes data

First, we’ll look at the recipes data that you need to fetch to populate the view. For this one-page app, you’ll need only asynchronous actions. To learn more about actions and action creators, see chapter 6 for a full explanation. If you’d like to check out the code for this section, change to the redux branch (git checkout redux). To see all the code for this section in final working form, check out redux-complete (git checkout redux-complete).

In the action-creators file in the recipes app, you’ll add two action creators. One will fetch the list of all the recipes, and the other will fetch the featured recipe. Listing 2.9 shows the implementation for the actions. Add the code in this listing to the src/action-creators.es6 file.

Note

I’ve included a library called isomorphic-fetch to help make XHR calls. It provides an implementation of the fetch API for both Node.js and in the browser. You can find more information and the documentation at https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API and https://github.com/matthew-andrews/isomorphic-fetch.

Listing 2.9. Action creators for recipes and featured data—src/action-creators.es6
export const GET_RECIPES = 'GET_RECIPES';                              1
export const GET_FEATURED_RECIPE = 'GET_FEATURED_RECIPE';              1

export function fetchRecipes() {                                       2
  return dispatch => {
    return fetch('http://localhost:3000/recipes', {                    3
      method: 'GET'
    }).then((response) => {
      return response.json().then((data) => {                          4
        return dispatch({                                              5
          type: GET_RECIPES,                                           6
          data: data.recipes                                           7
        });
      });
    })
  }
}

export function fetchFeaturedRecipe() {                                8
  return dispatch => {
    return fetch('http://localhost:3000/featured', {                   3
      method: 'GET'
    }).then((response) => {
      return response.json().then((data) => {                          4
        return dispatch({                                              9
          type: GET_FEATURED_RECIPE,                                   6
          data: data.recipe                                            10
        });
      });
    })
  }
}

export function getHomePageData() {                                    11
  return (dispatch, getState) => {
    return Promise.all([
      dispatch(fetchFeaturedRecipe()),
      dispatch(fetchRecipes())
    ])
  }
}

  • 1 Best practice is to create constants for all your actions so action creators (functions listed here) and reducers can use them. Then you won’t have discrepancies between strings.
  • 2 fetchRecipes, the first action creator, handles logic for making a request to the server for recipes data.
  • 3 Implements fetch API for making GET request to appropriate endpoint.
  • 4 On a successful response, get JSON from the response—using a promise to get JSON response is standard with the fetch API.
  • 5 Dispatch action.
  • 6 Type is the only required property of every action, set it using string constants declared at the top of the module.
  • 7 Attach JSON data to action payload on a property called data.
  • 8 The second action creator handles the logic for requesting the featured recipe from the server.
  • 9 Dispatch action.
  • 10 Attach JSON data to action payload on a property called data.
  • 11 This action creator composes the other two action creators—making it easier for view and server to request related data.

By themselves, these actions won’t do anything. All they’re responsible for is determining what will be updated in the app state. They then send the action to the reducers. The reducers take in the objects from the action creators in fetchRecipes and fetchFeaturedRecipe. They return a new copy of the store (maintaining state as an immutable object), with the updated data. Figure 2.9 shows this flow.

Figure 2.9. Dispatching an action triggers a lookup in the reducer followed by an update to the store.

The following listing shows the recipes reducers in the app. It also demonstrates how to keep the app state immutable. Add this code to src/recipe-reducer.es6.

Listing 2.10. Reducers—src/recipe-reducer.es6
import {
         GET_RECIPES,
         GET_FEATURED_RECIPE
       } from './action-creators';                         1

export default function recipes(state = {}, action) {      2
  switch (action.type) {                                   3
    case GET_RECIPES:
      return {
        ...state,                                          4
        recipes: action.data                               5
      };
    case GET_FEATURED_RECIPE:
      return {
        ...state,                                          4
        featuredRecipe: action.data                        5
      };

    default:
      return state                                         6
  }
}

  • 1 Include the constants from action creators.
  • 2 A reducer is a JavaScript function that takes in the current state and an action.
  • 3 Using a switch statement is recommended because many reducers end up with > 4 cases to handle—this switch uses a type property to determine how to handle each action.
  • 4 Using the spread operator to clone the state object maintains an immutable store.
  • 5 Use data from the action to override the current state so the new app state is the old app state with the modified data.
  • 6 If a reducer is triggered but no case matches, return the current store state—no changes required, no need to create new object.

Now that you have action creators and reducers, you need to initialize and configure Redux. Because both the browser and the server will be initializing Redux, you’ll abstract the code into a module called init-redux. You add the code from the following listing to src/init-redux.es6.

Listing 2.11. Using initialState to start Redux—src/init-redux.es6
import {
  createStore,                                                1
  combineReducers,                                            1
  applyMiddleware,                                            1
  compose } from 'redux';                                     1
import recipes from './recipe-reducer';                       2
import thunkMiddleware from 'redux-thunk';                    3

export default function () {
  const reducer = combineReducers({                           4
    recipes                                                   2
  });

  let middleware = [thunkMiddleware];                         3

  return compose(                                             5
    applyMiddleware(...middleware)                            5
  )(createStore)(reducer);                                    5
}

  • 1 Include the Redux functions used to create the Redux store.
  • 2 Include the recipes reducer you created earlier.
  • 3 Include Thunk middleware. It lets you write action creators that can dispatch additional actions and use promises.
  • 4 Use the combineReducers function to create one root reducer (in bigger apps, you’ll have many reducers).
  • 5 Use the functions imported from Redux to initialize the store and pass in middleware options—compose takes functions and combines them from right to left.

Redux is completely wired up, but the view still doesn’t have access to the data. The next section covers connecting React and Redux.

2.4.3. React and Redux

You still have a couple of steps before you can get React and Redux working together properly and have the browser code ready to go. You’ll use an npm package called react-redux to hook up your React components to Redux. This package provides a React component called Provider that you use to wrap all your other React components. These wrapped components can then optionally subscribe to updates from the Redux store using another component that the library has, called connect. The following listing shows how to include the Provider in the browser entry point file. Update src/main.jsx with the code in bold.

Listing 2.12. Redux and React setup—src/main.jsx
import { Provider } from 'react-redux';                       1
import initRedux from './init-redux.es6';                     2
require('./style.css');

const store = initRedux();                                    3

ReactDOM.render(
  <Provider store={store}>                                    4
    <App />
   </Provider>,
  document.getElementById('react-content')
);

  • 1 Include the Provider component that sets up a Redux store in the app so you can use the connect wrapper in your components.
  • 2 Include the module you just added that initializes Redux in application.
  • 3 Call initRedux to set up the Redux store.
  • 4 Wrap root app component with the react-redux Provider component and pass in the newly created store to the Provider component.

The Provider component acts as the stateful top-level component. It knows when the store updates and passes that change down to its children. Individual components can also subscribe to the store as needed. The following listing shows the code to add to the root component (src/components/app.jsx) so that it becomes a Redux connected component.

Listing 2.13. Connecting the app component to Redux—src/components/app.jsx
import React from 'react';
import { connect } from 'react-redux';                             1
import { bindActionCreators } from 'redux';                        1
import Recipes from './recipes';
import Featured from './featured';
import * as actionCreators from '../action-creators';              1

class App extends React.Component {

  componentDidMount() {
    this.props.actions.getHomePageData();                          2
  }

  render() {}
}

function mapStateToProps(state) {                                  3
  let { recipes, featuredRecipe } = state.recipes;                 4
  return {                                                         5
    recipes,
    featuredRecipe
  }
}

function mapDispatchToProps(dispatch) {                            6
  return { actions: bindActionCreators(actionCreators, dispatch) }
}

export default connect(
                        mapStateToProps,
                        mapDispatchToProps
                      )(App)                                 7

  • 1 Import the Redux dependencies and action creators you added previously.
  • 2 With the component configured to use Redux, you can dispatch actions from the view.
  • 3 Function lets you convert app state to properties on your component.
  • 4 This component requires data fetched from server, so you need to get recipes and featuredRecipe objects out of the current app state.
  • 5 Return values you want to access directly on this.props in your component.
  • 6 Function lets you make actions simpler to call from the component—instead of calling dispatch(action) each time, the view can call the action without knowing about dispatch.
  • 7 Instead of exporting App component, export connect component, which takes in two helper functions and App component as parameters.

Connect allows you to tap into the app state from components that need to know about how to display the data and where to get it from. Now the App component has access to all the properties needed to make the view work. At this point, if you restart the app, the view will be populated with data! Next, we’ll walk through the server code.

2.5. Server rendering

Now that you have your views and business logic set up, it’s time to look at server-rendering the homepage. You’re going to add a single route for the homepage. This isn’t very “real-world”—chapter 7 introduces a more robust way of handling the server, including using React Router on the server.

If you’re following along and want to check out the code so far, you can switch to the server-browser-rendering branch (git checkout server-browser-rendering). Note that in this section, you’ll no longer be loading index.html. Instead, load the app at http://localhost:3000.

2.5.1. Setting up a basic route on the server with middleware

This route will use Express middleware to handle and render the request. The middleware will also fetch the necessary data.

Definition

Express middleware is made up of chainable functions that each do a single job. Middleware can terminate the request by sending a response or can transform requests and do other business logic, including error handling.

The line of code in the Listing 2.14 needs to be added to src/app.es6. This code adds a handler for the root route. Make sure you add it so that server rendering will work. (I added the other code for you so the data endpoints would work in all the other examples.)

Listing 2.14. Set up the root route—src/app.es6
import renderViewMiddleware
 from './middleware/renderView';                     1

app.get('/featured', (req, res) => {});

// handle the isomorphic page render
app.get('/', renderViewMiddleware);                    1

// start the app
app.listen(3000, () => {
  console.log('App listening on port: 3000');
});

  • 1 Add an Express route to get the homepage using renderViewMiddleware.

2.5.2. Fetching the data

Next, let’s look at renderViewMiddleware and see how it fetches the data and renders the view. Remember, you have only one route in the recipes app, so you’re able to assume what Redux action needs to be dispatched. The following listing shows how the middleware to render the view works. Replace the code in src/middleware/renderView.jsx with this code.

Listing 2.15. Isomorphic view middleware data fetching—src/middleware/renderView.jsx
import initRedux from '../init-redux';
import * as actions from '../action-creators';

export default function renderView(req, res, next) {            1

  const store = initRedux();                                    2

  store.dispatch(actions.getHomePageData())                     3
    .then(() => {
      console.log(store.getState());
      res.send("It worked!!!");
    });
}

  • 1 Middleware function definition—express middleware receives a request object, a response object, and the next callback for passing control to next middleware in the chain.
  • 2 Set up Redux reducers and compose store—on the server, it starts with an empty store.
  • 3 Dispatch required action and wait for it to resolve before moving on to render.

At this point, if you run npm start and load the app at http://localhost:3000, you’ll get a message: “It worked!!!”. In the terminal output, you should see the current state, including an array of recipes and the featured recipe. You’ve set up the data fetching, but you still need to render the view. The next section covers adding the React server rendering code to renderView.jsx.

2.5.3. Rendering the view and serializing/injecting the data

For this single route, the rendering logic is simple. The one weird bit is that you end up doing two React renders on the server. When I first started building isomorphic apps, we used a different server-side templating language to build the index HTML. But this had a lot of downsides, including additional knowledge that each developer on the team had to have before understanding the full render flow. Then we switched to rendering the components for the route into a React component that represented the full-page markup. One less skill to master!

This is to remove the need to have another view template language in use on the server. The following listing shows how to implement the render logic. Add the bold code to the renderView middleware.

Listing 2.16. Isomorphic view middleware view render—src/middleware/renderView.jsx
import React from 'react';
import ReactDOM from 'react-dom/server';
import { Provider } from 'react-redux';
import initRedux from '../init-redux';
import * as actions from '../action-creators';
import HTML from '../components/html';
import App from '../components/app';

export default function renderView(req, res, next) {

const store = initRedux();

store.dispatch(actions.getHomePageData())
  .then(() => {
      let html;
      const dataToSerialize = store.getState();                  1

      html = ReactDOM.renderToString(                            2
        <Provider store={store}>
          <App />
       </Provider>
      );

      const renderedHTML = ReactDOM.renderToString(              3
        <HTML data={`window.__INITIAL_STATE =
          ${JSON.stringify(dataToSerialize)}`}
              html={html} />
      )
      res.send(renderedHTML)
  });
}

  • 1 Serialize the data so you can pass the state down to the browser.
  • 2 Render components by rendering app.jsx and injecting the data you fetched in the previous step.
  • 3 Render the full HTML page by rendering html.jsx with the previously rendered components and serialized data.

The other key piece of logic that must happen during your view render is getting the app state attached to the DOM response. You can see this in the code in the listing—it’s necessary so the browser can do its initial render with the exact same app state as was used on the server.

2.6. Browser rendering

The code for browser rendering is one of the most straightforward parts of the whole isomorphic app flow, but it’s also one of the most important pieces to get right. If you don’t render the app in the same state as on the server, you’ll break the isomorphic render and ruin all the performance gains you’ve earned.

2.6.1. Deserializing the data and hydrating the DOM

The server did all that hard work to get the data to the browser. To grab that data, all the browser needs to do is point at the window object the server set up via a script tag. You do that in main.jsx. Add the code in the next listing to main.jsx.

Listing 2.17. Add code for main.jsx—src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app.jsx';
import { Provider } from 'react-redux';                           1
import initRedux from './init-redux.es6';                         2
require('./style.css');

console.log("Browser packed file loaded");

const initialState = window.__INITIAL_STATE;                      3
const store = initRedux(initialState);                            4

console.log("Data to hydrate with", initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('react-content')
);

  • 1 Include the Provider component, which will become the root component, as on the server.
  • 2 Include Redux initialization module.
  • 3 Grab server serialized state off the window object.
  • 4 Instead of starting Redux with an empty initial state on the server, pass server data into the Redux setup.

Then, inside the initRedux function, the data from the server gets used. Listing 2.18 shows the configuration of Redux and how initialStore can be passed into it. You need to add the following code to the init-redux file.

Listing 2.18. Using initialState to start redux—src/init-redux.es6
import {
  createStore,
  combineReducers,
  applyMiddleware,
  compose } from 'redux';
import recipes from './recipe-reducer';
import thunkMiddleware from 'redux-thunk';

export default function (initialStore={}) {            1
  const reducer = combineReducers({
    recipes
  });

      let middleware = [thunkMiddleware];

  return compose(
    applyMiddleware(thunkMiddleware)
  )(createStore)(reducer, initialStore);               2
}

  • 1 initialStore has the value that was passed in from main.jsx (defaults to empty object when none is passed in).
  • 2 initialStore value is passed into the Redux createStore function—the store is now hydrated with data from server.

Now the app is ready to listen for user interaction and to continually update without talking to the server (the SPA flow). For the recipes app, if you wanted to expand the functionality and add detail pages for recipes, the server wouldn’t be involved in loading the detail page when the user clicks it from the homepage. In the GitHub repo, you can see the complete app on the server-browser-rendering-complete or master branches.

Summary

In this chapter, you learned to build a complete isomorphic app. Congrats—you covered a lot of ground by building this example! The next few chapters take a deeper dive into the various parts of isomorphic apps. You learned the following in this chapter:

  • Babel and webpack enable JavaScript code to be compiled. Webpack allows npm packages to be used with browser code.
  • React components make up the view portion of the application. JSX is used to declare the components.
  • Redux acts as the controller and the model of the isomorphic app.
  • The Node.js server uses Express middleware to respond to requests. Custom middleware for rendering React is needed for isomorphic apps. This middleware also sends down the initial serialized state of the application.
  • The browser uses a separate entry point to load in the initial state and start the app.
..................Content has been hidden....................

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