Throughout this chapter, you’ll build the browser portion of the isomorphic view render. You’re focusing on the lower half of the image in figure 8.1. You’ve seen this figure many times, but I’m revisiting it here to give context to this chapter.
All the code for this chapter is in the same GitHub repo that’s used in chapter 7, which can be found at http://mng.bz/8gV8. After you’ve pulled down this code, switch to chapter-8.1.1 (git checkout chapter-8.1.1), which has the code for the first section of the chapter. Each branch provided in a section includes the skeleton code needed from previous sections, but not what will be added in that specific section. The next section’s code will contain the complete code from previous sections. Each time you need to switch branches, I’ll let you know.
Remember that you need to run the start command each time you want to build the code (and after you make any changes):
$ npm start
The app runs at http://localhost:3000 in your browser.
The first thing you need to do to render your code in the browser is to set up your browser entry point. This is called main.jsx. It’ll be where you bootstrap (initialize) your React code in the browser. Your main.jsx entry point will end up being responsible for several things, including the following:
To get the browser code loading, you need to make sure to reference it from your HTML. It needs to be included as a script in the page, at the end of the body. That ensures it doesn’t block page loading and rendering. Because the page was already rendered once on the server, your user won’t know the script hasn’t loaded yet!
The code in the branch (chapter-8.1.1) already includes the webpack configuration. When you run npm start, the browser code is created by webpack and is available to use in the app. Open the html.jsx file and use the code from the following listing to add a reference to the bundled browser file. Note that in production, you’d want to make this URL configurable to wherever your static asset files will live.
<body> <div id="react-content" dangerouslySetInnerHTML={{ __html: props.html }} /> <script type="application/javascript" src="browser.js" /> 1 </body>
After you’ve done this, restart the server. To check whether the browser.js file is properly loading, use the Network tab in Chrome DevTools to see if it loaded. Figure 8.2 shows what to look for.
You should also try adding a console.log statement or setting a breakpoint to verify that your script is loading. After you get this working, you’re ready to render your React components in the browser.
This section covers how to render React in the browser. We already covered this in chapter 3, when you learned all about using React. I’m going to revisit the core implementation details here. Also, I’ll demonstrate why this step is necessary.
Let’s start with the /cart route because it’s already built out from chapter 7. In case you don’t remember, figure 8.3 shows what it looked like by the end of chapter 7.
So far, you’ve made this render only on the server. Some things don’t yet work, such as the Checkout button, which has a button handler with additional logic. The first step to getting that hooked up in the browser is to call ReactDOM.render in the browser. You’re going to add a simple render call that displays a Browser Render message after the browser code is executed and rendered. Figure 8.4 shows what this looks like.
To get this output, you need to set up main.jsx with React and then call render. Listing 8.2 shows your entry point file rendering a simple div with a message. Add the code from the listing to main.jsx. If you need to catch up, all the code up to this point is on the branch called chapter-8.1.2 (git checkout chapter-8.1.2). Or you can keep working on your code that you have up to this point.
import React from 'react'; 1 import ReactDOM from 'react-dom'; 1 function init() { 2 ReactDOM.render( <div> 3 Browser Render </div>, document.getElementById('react-content')); 4 } init(); 2
The obvious problem with this code is that your application has disappeared! That’s not what you want to happen. Open your developer tools and select the Sources tab to set a breakpoint in your main.jsx file. (Use Cmd-P and search for main.jsx—then you’ll be able to set the breakpoint.) Figure 8.5 shows what your Chrome DevTools will look like after setting the breakpoint.
Now that you’ve set the breakpoint, refresh your browser. The code execution will pause on your breakpoint. Look at your app in the main browser window. It’ll look correct, with the cart route rendered (as in figure 8.3). If your app is loading but you notice a React error in the console output, don’t worry about it for now (something like “React attempted to reuse markup in a container”). I’ll explain that later in the chapter.
Compare the DOM markup from the two states to see what’s going on before and after the browser render. Figure 8.6 shows the markup before and after the browser render call is made.
In this section, you’ve set up the browser code to run and added a simple React render to the browser. This illustrates the interaction between the server render and the initial browser render. In the next section, we’ll walk through using the server state in the browser.
To make the app isomorphic, you need to re-create the state from the server. Follow these steps:
1. Serialize the state of the app on the server and send it down in the stringified DOM markup.
2. Deserialize the state on the browser so it’s a consumable JSON object.
3. Initialize Redux with the app state (JSON object).
4. Pass the initialized Redux store into your React components.
Figure 8.7 shows this flow in the context of the All Things Westies app.
It’s important to remember that the state created on the server needs to exactly match the state used to bootstrap your React code on the browser. That ensures that there’s no need to update the browser DOM during the initial load of the page.
First, let’s set up the data on the server—both serializing it and then sending it down to the browser to be consumed.
In this section, you’ll update the code in renderView.jsx (created in chapter 7). The first step is to capture and serialize the current app state.
Serialization is the act of taking data (often JSON in JavaScript) and converting it to a format that can be sent between environments. In this case, you send it from the server to the browser via a network request.
By the end of this section, you’ll be able to access your server state in the console because you’ll be putting the server state on the window object. Figure 8.8 shows this output in Chrome Developer Tools.
You can switch to the chapter-8.2.1 branch to get the base code for this section (git checkout chapter-8.2.1), or keep following along with the code you’ve added so far. The branch has all the code listings so far. The following listing walks you through how to get and serialize your app’s current state in your renderView middleware. Add this new code to the renderView middleware.
Promise.all(promises).then(() => { const serverState = store.getState(); 1 const stringifiedServerState = JSON.stringify(serverState); 2 const app = renderToString( <Provider store={store}> <RouterContext routes={routes} {...renderProps} /> </Provider> ); const html = renderToString( <HTML html={app} serverState={stringifiedServerState} /> 3 ); return res.send(`<!DOCTYPE html>${html}`);
Now that you’ve created the string representation of the current app state, you need to set it in the DOM markup. You do that using dangerouslySetInnerHTML and a script tag. The following listing shows the code to add to html.jsx.
<body> <div id="react-content" dangerouslySetInnerHTML={{ __html: props.html }} /> 1 <script dangerouslySetInnerHTML={{ 2 __html: ` window.__SERIALIZED_STATE__ = 3 = JSON.stringify(${props.serverState}) 4 ` }} /> 1 <script type="application/javascript" src="browser.js" /> </body>
Upon restarting the server, you’ll be able to see your app state stringified and printed in the browser console (as in figure 8.8) by running the following:
window.__SERIALIZED_STATE__
Although it’s cool that you can now see your state in the browser, this isn’t useful. In the next two sections, we’ll walk through how to take this state and use it with Redux and your React components.
If this sounds complicated and scary, don’t fear! It’s simple. In fact, it’s easier than serializing the state in the first place. The goal is to take the stringified data that the server sends down and get it into a state that the app can work with. You take a string input and turn it back into a JavaScript object.
Deserialization is the act of taking serialized data and converting it to a format that’s usable by the current environment. In this case, you take a string and convert it to a JavaScript object.
The following listing shows the code you need to add to main.jsx to get the state off the window object. Parse the window.__SERIALIZED_STATE__ value and save it to a variable.
import React from 'react'; import ReactDOM from 'react-dom'; const initialState = JSON.parse(window.__SERIALIZED_STATE__); 1 console.log(initialState); 2
If you view the browser console, you’ll see the state. That’s all there is to deserializing the app state. Next, you’ll take this state and inject it into Redux so your app starts up in the same state as on the server.
Now that you have the state deserialized, you need to initialize Redux with the state that was generated on the server. You don’t need to add any new Redux code; everything you created in chapter 7 works perfectly here. The branch for this section is chapter-8.2.3 (git checkout).
If you’re using React Router 4, check out appendix A for an overview of setting up the routes and related router code. The appendix shows the main.jsx setup code compared to the Router code in listing 8.6 in this chapter.
All you need to do in main.jsx is initialize Redux with the correct state. The following listing shows you how to pass in the state to the init Redux function. You should update main.jsx with this code (replacing the placeholder code from the previous sections).
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; 1 import { browserHistory, Router } from 'react-router'; 2 import initRedux from './shared/init-redux.es6'; 3 import sharedRoutes from './shared/sharedRoutes'; 4 const initialState = JSON.parse(window.__SERIALIZED_STATE__); console.log(initialState); const store = initRedux(initialState); 5 function init() { ReactDOM.render( <Provider store={store}> 6 <Router routes={sharedRoutes} history={browserHistory} /> 7 </Provider>, document.getElementById('react-content')); } init();
Main.jsx imports initRedux from initRedux.jsx. The code in the initRedux.jsx module takes in the state and creates the store. That’s how you hydrate the store. You added this code in chapter 7. It’s shown here as a reminder, but it’s already in the repo for you in src/shared/init-redux.es6:
export default function (initialStore = {}) { const reducer = combineReducers({ products, cart }); const middleware = [thunkMiddleware, loggerMiddleware]; return compose( applyMiddleware(...middleware) )(createStore)(reducer, initialStore); }
Notice that the initialStore param that’s passed into the function is used in the createStore function to initialize the Redux store. Because this happens before your components are created, the first render of your components ends up using the state from the server.
With the state from the server, React is able to calculate that the initial virtual DOM matches the DOM attached to the root container provided to the render function. It knows that making any browser DOM updates is unnecessary.
Now that you’ve successfully rendered the view in the browser, let’s explore potential gotchas around the details of the first render in the browser. Note that up to this point, you’ve only set up the code that handles the initial render. Later in the chapter, you’ll add support for getting data when routing in the browser.
At this point, it should be clear how to get your app loading in the browser. It’s also important to understand what happens during the initial render in the browser, because it’s different from what happens later as the user interacts with the app.
This section walks you through the first render of React in the browser. React begins to bootstrap in your main.jsx file when you call ReactDOM.render. Each component that’s being initiated goes through the following steps:
1. The constructor is called.
2. componentWillMount is triggered.
3. The render method is called.
4. componentDidMount is called.
Remember that this starts with the root components, which all come from third-party libraries (React Redux and React Router). It then moves on to the components that React Router calculates need to be loaded for the current route. Then any of their child components are rendered.
It’s important to understand that componentWillMount is called on the first load for every component on both the server and the browser! Any code you have in it needs to be truly isomorphic. For example, if you were to add the following code to src/components/app.jsx, your server would break and be unable to load any routes:
componentWillMount() { window.test = true; }
That’s because your server can’t find a global variable called window. The window object doesn’t exist in Node.js. If you put this code into any of the methods that run during the first render (constructor, componentWillMount, or render), it’ll break on the server.
Conversely, componentDidMount, which runs after the first render is complete, is called only in the browser. This distinction becomes powerful when you need to update a component after reaching the browser but you don’t want to break the isomorphic render.
You may have noticed a big red warning in the console early in this chapter. Figure 8.9 shows what that looks like.
This isn’t a fun error. (To be fair, errors aren’t usually fun.) It sounds hard to debug the first time you read it: “the checksum was invalid”—what the bleep does that mean?
It means React rendered the component tree in the browser, compared it to the DOM that already exists, and found it to be different. But React is smart and knows that if there are already children inside the DOM element that you told it to render into, this is an isomorphic render. The two DOMs should match on the first render. But something broke, and they didn’t match!
At work, we often run a/b tests to experiment with various design and UX implementations to find out what works best to meet our goals with the product. One day we went to test our Login button. We wanted to find out whether we should call it “Login” or “Signup.” Even though the team is experienced in working with an isomorphic app, the experiment still ended up being run inside componentWillMount of our header component, and the isomorphic render warning started getting thrown.
Adding to our problems, another update was made to the code (around the same time) that changed the app state inside componentWillMount. It turns out that React will display this warning only for the first instance it runs into, which meant that when we fixed the first problem, we uncovered the second problem.
It’s surprisingly easy to get into this situation. One small change in the browser to the app state anytime in the first render (inside your constructor, componentWillMount, or render) can cause the error. The most common reason I’ve seen for getting into this situation is changing state based on cookies.
Cookies are global state that lives outside your app code. They’re a powerful and important tool for web app development. But they can complicate your initial app state. You either should account for the cookies on the server, or make sure to deal with them at the correct time in the browser. Chapter 10 covers this in more depth.
The reason you should care about this warning is that you lose all the positive benefits of your isomorphic render. You still have the good perceived performance—the UI and content is shown right away—but it can take a noticeably long time for the page to become usable, causing frustration for your user.
This is where componentDidMount becomes a powerful tool in your application. The next section explores how to use it to avoid isomorphic render errors.
Let’s say you wanted to save a user preference in the cookies. A common use case is whether to show a site-wide dismissible banner. This is beneficial to the user because it allows for global, easily accessible state in the browser. But you must be vigilant about when to check for these cookies to prevent isomorphic errors.
This example and solution assume you don’t want to read cookies on the server. In some apps, it may be more practical to read the cookies on the server and start with the correct app state in the server. We’ll walk through these trade-offs in more depth in chapter 10.
In this example, you’ll add a banner that informs users of a semi-annual sale. The rules for showing the banner are: show it to a user if they’ve never seen it before, and continue showing it until they dismiss it. You track whether they’ve seen it by writing a cookie in the browser. Figure 8.10 shows what you want this to look like.
Now you need to add a banner. If you want to check out the base code, switch to branch 8.3.3 (git checkout chapter-8.3.3), which has all the code added in this chapter so far and already has this banner component created for you (writing this component isn’t important to understanding this concept, but I’ve provided it for you in the following listing so you have some context).
class Banner extends React.Component { handleDismissBanner() { 1 // will do something } render() { return ( <div className=="banner" }> <div className="dismiss"> 2 <button className="btn-reset" onClick={this.handleDismissBanner} 3 > X </button> </div> <div className="content"> {this.props.children} 4 </div> </div> ); } }
Additionally, the following code has been added after the header in src/components/app.jsx (~line 13):
<Banner show> <h3>Check out the semi-annual sale! Up to 75% off select Items</h3> </Banner>
Now you need to add the code that decides whether to show this banner when the page loads. That involves checking a cookie and then telling the banner to be visible or stay hidden. The following listing shows how to update banner.jsx to make this work.
class Banner extends React.Component { constructor(props) { super(props); this.state = { 1 show: false }; this.handleDismissBanner = this.handleDismissBanner.bind(this); } componentDidMount() { const cookies = document.cookie; 2 const hideBanner = cookies.match('showBanner=false'); 2 if (!hideBanner) { this.setState({ 2 show: true }); } } handleDismissBanner() { document.cookie = 'showBanner=false'; 3 this.setState({ 3 show: false }); } render() { const bannerClasses = classnames( { show: this.state.show }, 'banner' ); 4 return ( <div className={bannerClasses}> 4 <div className="dismiss"> <button className="btn-reset" onClick={this.handleDismissBanner} > X </button> </div> <div className="content"> {this.props.children} </div> </div> ); } }
Using state to generate banner classes will add a class called show when the banner is visible. The CSS for this class changes the display to block so the component becomes visible.
Generally, you don’t want to set state in componentDidMount. But in this case, the isomorphic render makes it the best place to update the state, because we want to ensure that the first render matches the server state. Be careful setting state in this function—you can easily get into a situation where you cause unnecessary re-renders of your component.
Congrats, you’ve made it! Your app loads and renders from the server and successfully renders in the browser. Sadly, it still doesn’t do anything in the browser because you haven’t told it to do anything. Let’s make the app load data in the browser when routing between routes.
The routes you’ve set up will make the routes at the top of the page work. But if you navigate first to the root or to /products and then click Cart, you’ll find that the cart loads without any items. That’s because you haven’t set up the SPA portion of the app to fetch any data. Figure 8.11 shows what this looks like.
To get the app fetching data in the browser, you’ll take advantage of the prefetchActions static function to fetch the data on each route. React Router provides callbacks for various portions of its own lifecycle. There’s an onChange callback that can be configured. The function provided will be called before each route is rendered, giving you the opportunity to fetch any data from your API that’s needed for the route.
If you’re using React Router 4, check out appendix A for an overview of setting up the routes and related router code. It also shows how to prefetch the data on the browser and handle route changes in the React lifecycle instead of the React Router lifecycle.
If you want to check out the base code, switch to branch 8.4.1 (git checkout chapter-8.4.1). The following listing shows the code to add to sharedRoutes.jsx.
let beforeRouteRender = (dispatch, prevState, nextState) => { 1 const { routes } = nextState; 2 }; export const routes = (onChange = () => {}) => { 3 return ( <Route path="/" component={App} onChange={onChange} > 4 <Route path="/cart" component={Cart} /> <Route path="/products" component={Products} /> <Route path="/product/detail/:id" component={Detail} /> <Route path="/profile" component={Profile} /> </Route> ); }; const createSharedRoutes = ({ dispatch }) => { 5 beforeRouteRender = beforeRouteRender.bind(this, dispatch); 6 return routes(beforeRouteRender); 7 }; export default createSharedRoutes; 5
Passing in the onChange handler is necessary because you need to bind dispatch to it in the browser. This function is exported so the server code can call it. A couple of things are important here. First, you need to add the onChange handler only to the top-level route. That’s because onChange fires whenever a subroute changes. When the user changes between cart or products, it’ll be fired. This also means it won’t fire on the initial render. That would be unnecessary because the data is already available from the server state.
The most complicated piece of this logic is getting the prefetchActions array from the components. This is where the routes variable from nextState is important. The routes variable is an array of objects. Each route has a component listed on it. From this, it’s possible to collect all the actions that need to be called for the route. The following listing shows the last piece of code you need in order to make all this work. Add the code to sharedRoutes.jsx. You’ll notice it’s similar to the code in renderView.jsx.
let beforeRouteRender = (dispatch, prevState, nextState) => { const { routes } = nextState; routes.map((route) => { 1 const { component } = route; 2 if (component) { if (component.displayName && component.displayName .toLowerCase().indexOf('connect') > -1 3 ) { if (component.WrappedComponent .prefetchActions) { 4 return component.WrappedComponent.prefetchActions (); } } else if (component.prefetchActions) { 3 return component. prefetchActions(); } } return []; }).reduce((flat, toFlatten) => { 5 return flat.concat(toFlatten); }, []).map((initialAction) => { 6 return dispatch(initialAction()); }); };
You may notice that the structure of sharedRoutes has changed quite a bit. That’s because you need access to the dispatch method from Redux to trigger actions. In both main.jsx and renderView.jsx, you need to update the way you’re accessing the shared routes. The following listing shows the change that needs to be made in main.jsx.
function init() { ReactDOM.render( <Provider store={store}> <Router routes={sharedRoutes(store)} history={browserHistory} /> </Provider>, document.getElementById('react-content')); }
Instead of directly plugging in the routes, the default export is now a function. Here you call the function, passing in the store so that sharedRoutes can pass dispatch into the onChange handler.
The change to the renderView middleware is similar to the change in main.jsx. But you have to first change the import because on the server you don’t need to do the onChange logic—it’ll never fire. The following listing demonstrates what needs to change.
...additional code import { match, RouterContext } from 'react-router'; import { routes } from '../shared/sharedRoutes'; 1 import initRedux from '../shared/init-redux.es6'; import HTML from '../components/html'; export default function renderView(req, res, next) { const matchOpts = { routes: routes(), 2 location: req.url }; ...additional code }
With everything configured to load data on the server, you should see the cart populated. Additionally, if you view the Network tab, you’ll see the cart data being fetched from the server. This will be shown only if you load the app via /products first and then select Cart from the navigation. Figure 8.12 shows what you’re looking for.
Now that you’ve built out the browser portion of the code, you can review the complete code for this chapter on branch chapter-8-complete (git checkout chapter-8-complete).
In this chapter, you learned how to build the browser portion of an isomorphic app. You added a browser entry-point file called main.jsx and handled the initialization logic required to make the app start in the browser in the same state that it was rendered on the server.
3.140.198.173