This chapter covers
In chapter 3, you learned the basics of building views with React. Now you’ll build on those skills by exploring more-advanced concepts with React. This chapter will teach you what you need to know in order to build a production app with React.
You’ll be working with the All Things Westies app described in chapter 1. This is the first of many chapters in which you’ll be building this app. The code can be found at https://github.com/isomorphic-dev-js/complete-isomorphic-example.git. To start, you should be on branch chapter-4.1.1 (git checkout chapter-4.1.1). The master branch for this repo contains the complete code from all the chapters in the book.
To run the app for this chapter, use these commands:
$ npm install $ npm start
When the server is running, the app in this chapter will be loaded from http://localhost:3000/ (although there’s nothing to see on the chapter-4.1.1 branch). It’s not isomorphic because I want you to stay focused on the React concepts. As you build out this app in chapters 7 and 8, you’ll turn the app into an isomorphic app.
The app is shown in figure 4.1. I’ve called out the component parts that need to be added to make this work.
There are three main routes (and a home route, /): /products, /cart, and /profile. In the next section, you’ll set up the routing.
To build a web application, you usually need a router. Routers provide a mapping between the URL-based route and the view that the route should load. Because React is the view library, it doesn’t handle application routing on its own. That’s where React Router comes into play.
React Router has become the community choice for routing in React apps. It even has support for server-side routing, making it a great choice for isomorphic apps (covered in chapter 7). React Router makes creating your routes straightforward because it uses JSX to let you declare routes. React Router is a React component that handles your routing logic.
This app and the rest of the book use React Router 3 (v3.0.5). Since I started writing this book, a newer version (v4) has come out. The latest version is a complete rewrite of the way React Router works. It’s more in line with how React works, but it requires a new way of thinking about how the router interacts with an isomorphic app.
I’ve provided a version of the app with explanation in three appendices (A–C). You’ll find examples related to this chapter in appendix A. I explain how to get started with React Router 4 and the major changes with the removal of the React Router lifecycle.
The good news is the React Router team has committed to supporting v3 for the indefinite future (because of the breaking nature of v4). But I do recommend you explore v4 if you’re starting a new project.
React Router uses components to introduce routing into your app and to define the child routes. Before you initiate the app with the router, you must first define a set of routes that’ll be used. You’ll set them up in a sharedRoutes.jsx file.
In this first section, you’ll add the App component with the router. This will let you easily support the server-rendering use case you’ll build later in the book. The following listing shows you the code to add to sharedRoutes.jsx. Remember, if you want to follow along, you should be on branch chapter-4.1.1.
import React from 'react'; 1 import { Route } from 'react-router'; 2 import App from '../components/app'; 3 const routes = ( 4 <Route path="/" component={App}> 5 </Route> ); export default routes;
I’ve provided the skeleton of the App component for you so you don’t need to add this code. The following listing shows the App component.
import React from 'react'; const App = () => { return ( <div> <div className="ui fixed inverted menu"> <h1 className="header item">All Things Westies</h1> 1 <a to="/products" className="item">Products</a> 2 <a to="/cart" className="item">Cart</a> 2 <a to="/profile" className="item">Profile</a> 2 </div> <div className="ui main text container"> Content Placeholder 3 </div> </div> ); }; export default App;
Next, you’ll set up your app to use React Router. The following listing shows you how to set up the main.jsx file.
import React from 'react'; 1 import ReactDOM from 'react-dom'; 1 import { browserHistory, Router } from 'react-router'; 2 import sharedRoutes from './shared/sharedRoutes'; 3 ReactDOM.render( <Router 4 routes={sharedRoutes} 5 history={browserHistory} 6 />, document.getElementById('react-content') );
Instead of rendering a root component into the DOM, React Router ends up being your root component. Another way to think of it is as the top component in your component tree (see figure 4.2).
Under the hood, the router is using the browser history object. It hooks into this object to use push state and other browser-routing APIs.
Additionally, React Router allows you to pass in this history object. That way, it doesn’t make any assumptions about which environment it runs in. On the browser, you pass in a different history object than on the server. That’s part of what makes React Router good for isomorphic apps. Passing in the history object is also a more testable pattern.
To make the rest of the app work, add the child routes the user will use to navigate between the views in the app. This requires two additional steps: creating child routes and setting up app.jsx to render any child. The following listing shows how to add the new routes to the sharedRoutes file. If you want to follow along, the base code for this section is in branch chapter-4.1.2 (git checkout chapter-4.1.2).
//... other import statements import Cart from '../components/cart'; 1 import Products from '../components/products'; 1 import Profile from '../components/profile'; 1 const routes = ( <Route path="/" component={App}> <Route path="/cart" component={Cart} /> 2 <Route path="/products" component={Products} /> 2 <Route path="/profile" component={Profile} /> 2 </Route> ); export default routes;
Each of the child routes will be combined with App. React Router will know the appropriate child component that should be made available to App to be rendered.
The next step in getting the child routes working is to set up the App component to display any arbitrary child. The App component doesn’t need to know which child it’s rendering—only that it needs to render a child. You decouple the implementation of the child and parent. This creates a reusable pattern in which the same child can be used in multiple views, or vice versa. Figure 4.3 shows the React Router and child route relationship.
You can pass in children by nesting React components:
<MyComponent> <ChildComponent /> </MyComponent>
Then inside the render function of MyComponent you reference the child on the props object:
render() { return <div>{props.children}</div> }
React Router handles passing down the children component by assigning props via JavaScript and using the lower-level React APIs such as createElement. You don’t need to worry about this, but if you’re interested in exploring further, check out https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#routercontext.
This pattern allows the child component to be determined dynamically at runtime. The following listing shows how to update the App component to do this. Add the code from the listing to the app.jsx component code that already exists.
const App = (props) => { return ( <div> <div className="ui fixed inverted menu"> ... </div> <div className="ui main text container"> {props.children} 1 </div> </div> ); }; App.propTypes = { 2 children: PropTypes.element 3 };
Setting propTypes on components provides documentation and is considered best practice. It’s an object that describes the expected properties, including whether they’re required.
Because the Router wraps the App component, it passes down several router objects as props. Many of these objects are required in child components, but I’ll focus on three:
In the next section, you’ll use the Link component, which takes advantage of the lower-level router and history APIs so you don’t have to.
React Router goes one step further and provides a React component for you to use when you want to trigger navigation. That way, you don’t need to worry about what’s going on under the hood.
The Link component renders an <a> tag. To use the Link component, you include it in your component and then render it with the properties it needs. If you want to follow along with this section and get the code so far, switch to the branch called chapter-4.1.3 (git checkout chapter-4.1.3). The following listing shows you how to update the header to use the Link component instead of standard links in app.jsx.
import React from 'react'; import { Link } from 'react-router'; 1 const App = (props) => { return ( <div> <div className="ui fixed inverted menu"> <h1 className="header item">All Things Westies</h1> <Link to="/products" className="item">Products</Link> 2 <Link to="/cart" className="item">Cart</Link> 2 <Link to="/profile" className="item">Profile</Link> 2 </div> <div className="ui main text container"> {props.children} </div> </div> ); };
Note that instead of an href property, the Link component requires a to property. After adding the Link components, your app will properly route between views.
The React Router library has one more important part that you’ll want to know about for building production apps: how to hook into the router lifecycle.
React Router provides hooks into its lifecycle to allow you to add logic between routes. A common use case for lifecycle hooks is adding page-view tracking analytics to your application so you know how many views each route gets.
If you’re using React Router 4, check out appendix A to see how to move this code into the React lifecycle and how to handle the concepts discussed in this section.
Imagine if you tried to add this logic into your components. You’d end up with the tracking logic in every top-level component (Cart, Products, Profile). Or you’d end up trying to detect changes based on properties in the App component. Both methods are undesirable and leave a lot of room for error.
Instead, you want to use the onChange and onEnter lifecycle events for React Router. (A third lifecycle hook, onLeave, isn’t covered here.) Figure 4.4 shows the order in which these handlers fire.
For each route, onEnter is fired when the app goes to the route from a different route. Because / is the root route, it can be entered only once. The onChange handler is fired each time a child route changes. For the root route, this happens on each route action after the first. The following listing shows how to implement these handlers in the sharedRoutes.jsx file. If you’re following along and want to see the code from the previous sections, you can find it on branch chapter-4.1.4 (git checkout chapter-4.1.4).
const trackPageView = () => { 1 console.log('Tracked a pageview'); }; const onEnter = () => { 2 console.log('OnEnter'); trackPageView(); }; const onChange = () => { 3 console.log('OnChange'); trackPageView(); }; const routes = ( <Route path="/" component={App} onEnter={onEnter} onChange={onChange}> 4 <Route path="/cart" component={Cart} /> <Route path="/products" component={Products} /> <Route path="/profile" component={Profile} /> </Route> );
Next, you’ll explore React’s component lifecycle, which is a completely different set of lifecycle functions specific to React. The lifecycle functions give you greater control over when things happen in your app.
A site that has user accounts requires a login. Certain parts of the site will always be locked down so you can view them only if you’re logged in. For example, with the All Things Westies app, users who want to view their settings page to update their password or view past orders will need to log in.
This use case is the opposite of the analytics use case in the preceding section. Instead of doing something on every route, you want to check for logged-in status only on certain routes. You could do that on the routes, if you’d like, with onChange or onEnter handlers. But you can also put this logic inside the appropriate React component. For this example, we’ll use the component lifecycle.
React provides several hooks into the lifecycle of components. The render function, which you’ve already used, is part of this lifecycle. The lifecycle of a component can be broken into three parts (illustrated in figure 4.5):
React lifecycle list and illustration concept are from Azat Mardan’s React Quickly (Manning, 2017, https://www.manning.com/books/react-quickly).
To detect whether the user is logged in, you’ll take advantage of one of the React lifecycle functions. This function is fired before the component has mounted (been attached to the DOM). Listing 4.8 shows how to add the check to the user profile component inside componentWillMount. There’s a placeholder for Profile, and you’ll want to update it with this code. If you’re following along and want to check out the code from the previous sections, switch to branch chapter 4.2.1 (git checkout chapter-4.2.1).
class Profile extends React.Component { componentWillMount() { if (!this.props.user) { 1 this.props.router.push('/login'); 2 } } render() {} }
In profile.jsx, you added a reference to the router prop. But if you run the code now and load the /profile route, the app will throw an error because you haven’t passed in the router object. To do that, you need to update app.jsx to pass props to its children. The following listing takes advantage of two React top-level API calls: React.Children and React.cloneElement.
const App = (props) => { return ( <div> <div className="ui fixed inverted menu"></div> <div className="ui main text container"> { React.Children.map( 1 props.children, 2 (child) => { 3 return React.cloneElement( 4 child, 4 { router: props.router } 4 ); } ) } </div> </div> ); };
In an isomorphic app, the first render cycle is the most important. That’s where you’ll use lifecycle events to control what environment the code runs in. For example, some third-party libraries aren’t loadable or usable on the server because they rely on the window object. Or you might want to add custom scroll behavior on the window event. You’ll need to control this by hooking into the various lifecycle methods available on the first render.
The first render lifecycle is made up of three functions (render and two mounting events):
For the isomorphic use case, it’s important to note some differences between componentWillMount and componentDidMount. Although both methods run exactly once on the browser, componentWillMount runs on the server, whereas componentDidMount never runs on the server. In the previous example, you wouldn’t want to run the user logged-in check in componentWillMount because the check would also run on the server. Instead, you’d put the check in componentDidMount, guaranteeing that it happens only in the browser.
componentDidMount never runs on the server because React never attaches any components to the DOM on the server. Instead, React’s renderToString (used on the server in place of render) results in a string representation of the DOM. In the next section, you’ll use componentDidMount to add a timer for a modal—something you want to do only in the browser.
Imagine that you want to add a countdown timer to the Products page. This timer launches a tooltip modal after a set amount of time. Figure 4.6 shows what this looks like. Timers are asynchronous and break the normal flow of user event-driven React updates. But React provides several lifecycle methods that can be used to handle timers within the lifecycle of a React component.
To add a timer to your component, you need to kick it off after the component has mounted. Additionally, you’ll need to handle the cleanup of the timer when the component unmounts or when certain other actions happen. To check out the base code for this section, switch to branch chapter 4.2.2 (git checkout chapter-4.2.2). The following listing shows how to add the timer code to products.jsx. The base component already exists, so update the code in bold.
import React from 'react'; class Products extends React.Component { constructor(props) { super(props); this.state = { showToolTip: false, searchQuery: '' }; this.updateSearchQuery = this.updateSearchQuery.bind(this); } componentDidMount() { setTimeout(() => { this.setState({ 1 showToolTip: true }); }, 10000); 1 } updateSearchQuery() { 2 this.setState({ searchQuery: this.search.value 3 }); } render() { const toolTip = ( 4 <div className="tooltip ui inverted"> Not sure where to start? Try top Picks. </div> ); return ( <div className="products"> <div className="ui search"> <div className="ui item input"> <input className="prompt" type="text" value={this.state.searchQuery} 5 ref={(input) => { this.search = input; }} 3 onChange={this.updateSearchQuery} 2 /> <i className="search icon" /> </div> <div className="results" /> </div> <h1 className="ui dividing header">Shop by Category</h1> <div className="ui doubling four column grid"> <div className="column segment secondary"></div> <div className="column segment secondary"></div> <div className="column segment secondary"> <i className="heart icon" /> <div className="category-title">Top Picks</div> { this.state.showToolTip ? toolTip : ''} 6 </div> <div className="column segment secondary"></div> </div> </div> ); } } export default Products;
The tooltip shows up at this point (it’s set to show after 10 seconds). But let’s imagine you want to show the tooltip only if the user has never interacted with the page. In that case, you need a way to clear the tooltip when the user has interacted. Technically, you could do that in the onChange handler for search, but for illustrative purposes, you’ll add this in componentWillUpdate. The following listing shows how to do that.
class Products extends React.Component { componentDidMount() { this.clearTimer = setTimeout(() => { 1 this.setState({ showToolTip: true }); }, 10000); } componentWillUpdate(nextProps, nextState) { 2 if (nextState.searchQuery.length > 0) { clearTimeout(this.clearTimer); 3 } console.log('cWU'); 4 } updateSearchQuery() {} }
If you restart the app and interact with the Products page before the 10-second timer is finished, you’ll notice that the tooltip never appears.
The update lifecycle methods are made up of several update methods and the render method, which you can see in the listing. With the exception of the render method, these methods never run on the server (so the accessing window and document are safe):
Update lifecycle based on Azat Mardan’s React Quickly (Manning, 2017).
Remember, the mounting lifecycle will always run before any of these methods.
One final improvement you need to make to the timer is to make sure it gets cleaned up if the user navigates away from the Products page before the timer finishes running. If you don’t do that, you’ll see a React error in the console after 10 seconds. The error explains that the code being run is trying to reference a component that’s no longer mounted in the DOM. This happened because you navigated away from the component the timer was in without turning off the timer. Figure 4.7 is a screenshot of the error.
The following listing shows how to add the time-out cleanup to your componentWillUnmount lifecycle function.
class Products extends React.Component { componentWillUpdate(nextProps, nextState) {} componentWillUnmount() { clearTimeout(this.clearTimer); 1 } updateSearchQuery() {} }
There’s only one unmount event: componentWillUnmount(). You can take advantage of this event to clean up any manually attached event listeners and shut down any timers you may have running. This method runs only in the browser. To see all the code for the chapter, you can check out branch chapter-4-complete (git checkout chapter-4-complete).
Now that you understand the React lifecycle, let’s explore component architecture patterns that can help you build great React apps.
You can compose React components in user interfaces in two well-defined ways:
In the All Things Westies app, it’s beneficial to create reusable parts for the view and business logic. This has long-term maintainability benefits for developers and makes your code easier to reason about.
In some cases, you add reusability by creating a component that takes in another component and extends its functionality—a decorator. This happens in Redux when you wrap a view component with the Connect component. In other cases, you split your components into two types: components that focus on business logic and components that focus on what the app looks like. For example, the Products component focuses on the business logic of the view.
When building a modular, component-driven UI, you end up having a lot of components that need the same kind of data fetching or that have the same view with different data fetching. For example, you may have many views that use user data in some way. Or you may have many views that use a List component but with different data sets. In these cases, you want a way to pull out the data-fetching and manipulation logic, making it separate from the component that displays the data.
Even though you haven’t added any data fetching to the All Things Westies app yet, you’ll eventually need to do that. The products view will need to know about the products available for sale. Imagine you wanted to make a component that knew how to fetch all the products. It’d look something like this:
const ProductsDataFetcher = (Component) => { ... // fetches the products data ... // ensures data is compatible with the products component return <Component data={this.state.data} /> }
The most important part of this example function is that you pass in the component (the Products component in this example) to the ProductsDataFetcher function. In this case, the higher-order component (HOC) function knows how to get the product data and will then pass that data into the component (Figure 4.8). This abstracts away any state or logic from the Products View component, leaving it to focus on the UI concerns.
If you have a component and then pass it into the higher-order component, you’ll end up with the original component plus additional functionality. In React, this almost always results in offloading some sort of state management to the parent HOC. In the ListDataFetcher example, the HOC knows about the app state and fetching the data. That allows the List component to be a presentation component that’s highly reusable.
It’s possible to categorize React components into two distinct buckets: presenters and containers. By following this binary type pattern, you can maximize your code reuse and minimize unnecessary code coupling and complexity.
Earlier in this chapter, you built the Products page of the All Things Westies app. This has a component called Products that holds onto the state for its part of the application. Later in the book, it’ll also be responsible for managing data fetching via Redux. These responsibilities make it a container component.
On the other hand, the Item and App components are presentation components. Both contain display elements and rely on properties to determine their functionality. Presentation components determine how the app looks.
Table 4.1 lists the value of container and presentation components.
Container |
Presentation |
---|---|
Contains state | Limited state (for user interactions), ideally implemented as a functional component |
Responsible for how the app works | Responsible for how the app looks |
Children: container and presentation components | Children: container and presentation components |
Connect to rest of application (for example, Redux) | No dependencies on model or controller portions of the app (for example, Redux) |
Container components abstract state away from their children. They also handle layout and are generally responsible for the how of the application. Some higher-order components have this as their main purpose. They listen for data changes and then pass that state down as properties. Redux provides a higher-order component that helps with this (see chapter 6).
Presentation components contain only state related to user interactions. Whenever possible, they should be implemented as pure components. They’re concerned with what the application looks like.
One important note is that containers can have other containers and presentation components as children. Conversely, presentation components can have both containers and presentation components as children. These two types of component nesting should be kept flexible to maximize code composition. That may feel strange at first, but keeping the two component types clear will help you in the long run.
In this chapter, you learned how to set up and use React Router to have a complete single-page app experience. You also learned more about React by exploring the component lifecycle. Finally, you learned key patterns that are commonly used when building React apps.
18.117.103.5