This chapter covers
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.
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.
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.
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.
To write the recipes app and make it run as an isomorphic app, you’ll use several JavaScript libraries:
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).
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.
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
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.
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:
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.
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:
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. |
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.
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:
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.
{ "presets": ["es2015", "react"], 1 "plugins": [ 2 "transform-es2015-destructuring", 3 "transform-es2015-parameters", 3 "transform-object-rest-spread" 3 ] }
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.
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.
require('babel-register'); 1 require('./app.es6'); 2
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.
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:
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.
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.
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 } };
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.
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.
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.
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.
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.
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.
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.
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 );
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.
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;
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.
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.
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;
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.
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;
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!
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.
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> ); } }
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.
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.
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.
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.
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.
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.
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.
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.
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.
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()) ]) } }
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.
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.
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 } }
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.
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 }
Redux is completely wired up, but the view still doesn’t have access to the data. The next section covers connecting 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.
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') );
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.
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
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.
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.
This route will use Express middleware to handle and render the request. The middleware will also fetch the necessary data.
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.)
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'); });
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.
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!!!"); }); }
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.
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.
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) }); }
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.
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.
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.
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') );
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.
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 }
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.
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:
18.117.103.5