Chapter 6. Props, State, and the Component Tree

In the last chapter, we talked about how to create components. We primarily focused on how to build a user interface by composing React components. This chapter is filled with techniques that you can use to better manage data and reduce time spent debugging applications.

Data handling within component trees is one of the key advantages of working with React. There are techniques that you can use when working with data in React components that will make your life much easier in the long run. Our applications will be easier to reason about and scale if we can manage data from a single location and construct the UI based on that data.

Property Validation

JavaScript is a loosely typed language, which means that the data type of a variable’s value can change. For example, you can initially set a JavaScript variable as a string, then change its value to an array later, and JavaScript will not complain. Managing our variable types inefficiently can lead to a lot of time spent debugging applications.

React components provide a way to specify and validate property types. Using this feature will greatly reduce the amount of time spent debugging applications. Supplying incorrect property types triggers warnings that can help us find bugs that may have otherwise slipped through the cracks.

React has built-in automatic property validation for the variable types, as shown in Table 6-1.

Table 6-1. React property validation
Type Validator
Arrays React.PropTypes.array
Boolean React.PropTypes.bool
Functions React.PropTypes.func
Numbers React.PropTypes.number
Objects React.PropTypes.object
Strings React.PropTypes.string

In this section, we will create a Summary component for our recipes. The Summary component will display the title of the recipe along with counts for both ingredients and steps (see Figure 6-1).

Summary Component Output for Baked Salmon
Figure 6-1. Summary component output for Baked Salmon

In order to display this data, we must supply the Summary component with three properties: a title, an array of ingredients, and an array of steps. We want to validate these properties to make sure the first is a string and the others are arrays, and supply defaults for when they are unavailable. How to implement property validation depends upon how components are created. Stateless functional components and ES6 classes have different ways of implementing property validation.

First, let’s look at why we should use property validation and how to implement it in components created with React.createClass.

Validating Props with createClass

We need to understand why it is important to validate component property types. Consider the following implementation for the Summary component:

const Summary = createClass({
    displayName: "Summary",
    render() {
        const {ingredients, steps, title} = this.props
        return (
            <div className="summary">
                <h1>{title}</h1>
                <p>
                    <span>{ingredients.length} Ingredients</span> |
                    <span>{steps.length} Steps</span>
                </p>
           </div>
        )
    }
})

The Summary component destructures ingredients, steps, and title from the properties object and then constructs a UI to display that data. Since we expect both ingredients and steps to be arrays, we use Array.length to count the array’s items.

What if we rendered this Summary component accidentally using strings?

render(
  <Summary title="Peanut Butter and Jelly"
           ingredients="peanut butter, jelly, bread" 
           steps="spread peanut butter and jelly between bread" />,
  document.getElementById('react-container')
)

JavaScript will not complain, but finding the length will count the number of characters in each string (Figure 6-2).

Summary Component Output for Peanut Butter and Jelly
Figure 6-2. Summary component output for Peanut Butter and Jelly

The output of this code is odd. No matter how fancy your peanut butter and jelly might be, it’s doubtful that you are going to have 27 ingredients and 44 steps. Instead of seeing the correct number of steps and ingredients, we are seeing the length in characters of each string. A bug like this is easy to miss. If we validated the property types when we created the Summary component, React could catch this bug for us:

const Summary = createClass({
    displayName: "Summary",
    propTypes: {
        ingredients: PropTypes.array,
        steps: PropTypes.array,
        title: PropTypes.string
    },
    render() {
        const {ingredients, steps, title} = this.props
        return (
            <div className="summary">
                <h1>{title}</h1>
                <p>
                    <span>{ingredients.length} Ingredients | </span>
                    <span>{steps.length} Steps</span>
                </p>
           </div>
        )
    }
})

Using React’s built-in property type validation, we can make sure that both ingredients and steps are arrays. Additionally, we can make sure that the title value is a string. Now when we pass incorrect property types, we will see an error (Figure 6-3).

Figure 6-3. Property type validation warning

What would happen if we rendered the Summary component without sending it any properties?

render(
  <Summary />,
  document.getElementById('react-container')
)

Rendering the Summary component without any properties causes a JavaScript error that takes down the web app (Figure 6-4).

Figure 6-4. Error generated from missing array

This error occurs because the type of the ingredients property is undefined, and undefined is not an object that has a length property like an array or a string. React has a way to specify required properties. When those properties are not supplied, React will trigger a warning in the console:

const Summary = createClass({
    displayName: "Summary",
    propTypes: {
        ingredients: PropTypes.array.isRequired,
        steps: PropTypes.array.isRequired,
        title: PropTypes.string
    },
    render() {
        ...
    }
})

Now when we render the Summary component without any properties, React directs our attention to the problem with a console warning just before the error occurs. This makes it easier to figure out what went wrong (Figure 6-5).

Figure 6-5. React warnings for missing properties

The Summary component expects an array for ingredients and an array for steps, but it only uses the length property of each array. This component is designed to display counts (numbers) for each of those values. It may make more sense to refactor our code to expect numbers instead, since the component doesn’t actually need arrays:

import { createClass, PropTypes } from 'react'

export const Summary = createClass({
    displayName: "Summary",
    propTypes: {
        ingredients: PropTypes.number.isRequired,
        steps: PropTypes.number.isRequired,
        title: PropTypes.string
    },
    render() {
        const {ingredients, steps, title} = this.props
        return (
            <div className="summary">
                <h1>{title}</h1>
                <p>
                    <span>{ingredients} Ingredients</span> |
                    <span>{steps} Steps</span>
                </p>
           </div>
        )
    }
})

Using numbers for this component is a more flexible approach. Now the Summary component simply displays the UI; it sends the burden of actually counting ingredients or steps further up the component tree to a parent or ancestor.

Default Props

Another way to improve the quality of components is to assign default values for properties.1 The validation behavior is similar to what you might expect: the default values you establish will be used if other values are not provided.

Let’s say we want the Summary component to work even when the properties are not supplied:

import { render } from 'react-dom'

render(<Summary />, document.getElementById('react-container'))

With createClass, we can add a method called getDefaultProps that returns default values for properties that are not assigned:

const Summary = createClass({
    displayName: "Summary",
    propTypes: {
        ingredients: PropTypes.number,
        steps: PropTypes.number,
        title: PropTypes.string
    },
    getDefaultProps() {
        return {
            ingredients: 0,
            steps: 0,
            title: "[untitled recipe]"
        }
    },
    render() {
        const {ingredients, steps, title} = this.props
        return (
            <div className="summary">
                <h1>{title}</h1>
                <p>
                    <span>{ingredients} Ingredients | </span>
                    <span>{steps} Steps</span>
                </p>
            </div>
        )
    }
}

Now when we try to render this component without properties, we will see some default data instead, as in Figure 6-6.

Figure 6-6. Summary component output with default properties

Using default properties can extend the flexibility of your component and prevent errors from occurring when your users do not explicitly require every property.

Custom Property Validation

React’s built-in validators are great for making sure that your variables are required and typed correctly. But there are instances that require more robust validation. For example, you may want to make sure that a number is within a specific range or that a value contains a specific string. React provides a way to build your own custom validation for such cases.

Custom validation in React is implemented with a function. This function should either return an error when a specific validation requirement is not met or null when the property is valid.

With basic property type validation, we can only validate a property based on one condition. The good news is that the custom validator will allow us to test the property in many different ways. In this custom function, we’ll first check that the property’s value is a string. Then we’ll limit its length to 20 characters (Example 6-2).

Example 6-2. Custom prop validation
propTypes: {
    ingredients: PropTypes.number,
    steps: PropTypes.number,
    title: (props, propName) => 
        (typeof props[propName] !== 'string') ?
            new Error("A title must be a string") : 
            (props[propName].length > 20) ?
                new Error(`title is over 20 characters`) :
                null
}

All property type validators are functions. To implement our custom validator, we will set the value of the title property, under the propTypes object, to a callback function. When rendering the component, React will inject the props object and the name of the current property into the function as arguments. We can use those arguments to check the specific value for a specific property.

In this case, we first check the title to make sure it is a string. If the title is not a string, the validator returns a new error with the message: “A title must be a string.” If the title is a string, then we check its value to make sure it is not longer than 20 characters. If the title is under 20 characters, the validator function returns null. If the title is over 20 characters, then the validator function returns an error. React will capture the returned error and display it in the console as a warning.

Custom validators allow you to implement specific validation criteria. A custom validator can perform multiple validations and only return errors when specific criteria are not met. Custom validators are a great way to prevent errors when using and reusing your components.

ES6 Classes and Stateless Functional Components

In the previous sections, we discovered that property validation and default property values can be added to our component classes using React.createClass. This type checking also works for ES6 classes and stateless functional components, but the syntax is slightly different.

When working with ES6 classes, propTypes and defaultProps declarations are defined on the class instance, outside of the class body. Once a class is defined, we can set the propTypes and defaultProps object literals (Example 6-3).

Example 6-3. ES6 class
class Summary extends React.Component {
  render() {
   const {ingredients, steps, title} = this.props
      return (
          <div className="summary">
              <h1>{title}</h1>
              <p>
                  <span>{ingredients} Ingredients | </span>
                  <span>{steps} Steps</span>
              </p>
          </div>
      )
  }
}

Summary.propTypes = {
  ingredients: PropTypes.number,
  steps: PropTypes.number,
  title: (props, propName) => 
    (typeof props[propName] !== 'string') ?
        new Error("A title must be a string") : 
        (props[propName].length > 20) ?
            new Error(`title is over 20 characters`) :
            null
}

Summary.defaultProps = {
    ingredients: 0,
    steps: 0,
    title: "[recipe]"
}

The propTypes and defaultProps object literals can also be added to stateless functional components (Example 6-4).

Example 6-4. Stateless functional component
const Summary = ({ ingredients, steps, title }) => {
  return <div>
    <h1>{title}</h1>
    <p>{ingredients} Ingredients | {steps} Steps</p>
  </div>
}

Summary.propTypes = {
  ingredients: React.PropTypes.number.isRequired,
  steps: React.PropTypes.number.isRequired
}

Summary.defaultProps = {
  ingredients: 1,
  steps: 1
}

With a stateless functional component, you also have the option of setting default properties directly in the function arguments. We can set default values for ingredients, steps, and title when we destructure the properties object in the function arguments as follows:

const Summary = ({ ingredients=0, steps=0, title='[recipe]' }) => {
  return <div>
    <h1>{title}</h1>
    <p>{ingredients} Ingredients | {steps} Steps</p>
  </div>
}

Property validation, custom property validation, and the ability to set default property values should be implemented in every component. This makes the component easier to reuse because any problems with component properties will show up as console warnings.

Refs

References, or refs, are a feature that allow React components to interact with child elements. The most common use case for refs is to interact with UI elements that collect input from the user. Consider an HTML form element. These elements are initially rendered, but the users can interact with them. When they do, the component should respond appropriately.

For the rest of this chapter, we are going to be working with an application that allows users to save and manage specific hexadecimal color values. This application, the color organizer, allows users to add colors to a list. Once a color is in the list, it can be rated or removed by the user.

We will need a form to collect information about new colors from the user. The user can supply the color’s title and hex value in the corresponding fields. The AddColorForm component renders the HTML with a text input and a color input for collecting hex values from the color wheel (Example 6-5).

Example 6-5. AddColorForm
import { Component } from 'react'

class AddColorForm extends Component {
  render() {
      return (
          <form onSubmit={e=>e.preventDefault()}>
              <input type="text" 
                     placeholder="color title..." required/>
              <input type="color" required/>
              <button>ADD</button>
          </form>
      )
  }
}

The AddColorForm component renders an HTML form that contains three elements: a text input for the title, a color input for the color’s hex value, and a button to submit the form. When the form is submitted, a handler function is invoked where the default form event is ignored. This prevents the form from trying to send a GET request once submitted.

Once we have the form rendered, we need to provide a way to interact with it. Specifically, when the form is first submitted, we need to collect the new color information and reset the form’s fields so that the user can add more colors. Using refs, we can refer to the title and color elements and interact with them (Example 6-6).

Example 6-6. AddColorForm with submit method
import { Component } from 'react'

class AddColorForm extends Component {
  constructor(props) {
    super(props)
    this.submit = this.submit.bind(this)
  }
  submit(e) {
    const { _title, _color } = this.refs
    e.preventDefault();
    alert(`New Color: ${_title.value} ${_color.value}`)
    _title.value = '';
    _color.value = '#000000';
    _title.focus();
  }
  render() {
      return (
          <form onSubmit={this.submit}>
              <input ref="_title"
                     type="text" 
                     placeholder="color title..." required/>
              <input ref="_color"
                     type="color" required/>
              <button>ADD</button>
          </form>
      )
  }
}

We needed to add a constructor to this ES6 component class because we moved submit to its own function. With ES6 component classes, we must bind the scope of the component to any methods that need to access that scope with this.

Next, in the render method, we’ve set the form’s onSubmit handler by pointing it to the component’s submit method. We’ve also added ref fields to the components that we want to reference. A ref is an identifier that React uses to reference DOM elements. Creating _title and _color ref attributes for each input means that we can access those elements with this.refs_title or this.refs_color.

When the user adds a new title, selects a new color, and submits the form, the component’s submit method will be invoked to handle the event. After we prevent the form’s default submit behavior, we send the user an alert that echoes back the data collected via refs. After the user dismisses the alert, refs are used again to reset the form values and focus on the title field.

Binding the ‘this’ Scope

When using React.createClass to create your components, there is no need to bind the this scope to your component methods. React.createClass automatically binds the this scope for you.

Inverse Data Flow

It’s nice to have a form that echoes back input data in an alert, but there is really no way to make money with such a product. What we need to do is collect data from the user and send it somewhere else to be handled. This means that any data collected may eventually make its way back to the server, which we will cover in Chapter 12. First, we need to collect the data from the form component and pass it on.

A common solution for collecting data from a React component is inverse data flow.2 It is similar to, and sometimes described as, two-way data binding. It involves sending a callback function to the component as a property that the component can use to pass data back as arguments. It’s called inverse data flow because we send the component a function as a property, and the component sends data back as function arguments.

Let’s say we want to use the color form, but when a user submits a new color we want to collect that information and log it to the console.

We can create a function called logColor that receives the title and color as arguments. The values of those arguments can be logged to the console. When we use the AddColorForm, we simply add a function property for onNewColor and set it to our logColor function. When the user adds a new color, logColor is invoked, and we’ve sent a function as a property:

const logColor = (title, color) => 
    console.log(`New Color: ${title} | ${value}`)

<AddColorForm onNewColor={logColor} />

To ensure that data is flowing properly, we will invoke onNewColor from props with the appropriate data:

submit() {
    const {_title, _color} = this.refs
    this.props.onNewColor(_title.value, _color.value)
    _title.value = ''
    _color.value = '#000000'
    _title.focus()
}

In our component, this means that we’ll replace the alert call with a call to this.props.onNewColor and pass the new title and color values that we have obtained through refs.

The role of the AddColorForm component is to collect data and pass it on. It is not concerned with what happens to that data. We can now use this form to collect color data from users and pass it on to some other component or method to handle the collected data:

<AddColorForm onNewColor={(title, color) => {
    console.log(`TODO: add new ${title} and ${color} to the list`)
    console.log(`TODO: render UI with new Color`)
}} />

When we are ready, we can collect the information from this component and add the new color to our list of colors.

Optional Function Properties

In order to make two-way data binding optional, you must first check to see if the function property exists before trying to invoke it. In the last example, not supplying an onNewColor function property would lead to a JavaScript error because the component will try to invoke an undefined value.

This can be avoided by first checking for the existence of the function property:

if (this.props.onNewColor) {
    this.props.onNewColor(_title.value, _color.value)
}

A better solution is to define the function property in the component’s propTypes and defaultProps:

AddColorForm.propTypes = {
    onNewColor: PropTypes.func
}

AddColorForm.defaultProps = {
    onNewColor: f=>f
}

Now when the property supplied is some type other than function, React will complain. If the onNewColor property is not supplied, it will default to this dummy function, f=>f. This is simply a placeholder function that returns the first argument sent to it. Although this placeholder function doesn’t do anything, it can be invoked by JavaScript without causing errors.

Refs in Stateless Functional Components

Refs can also be used in stateless functional components. These components do not have this, so it’s not possible to use this.refs. Instead of using string attributes, we will set the refs using a function. The function will pass us the input instance as an argument. We can capture that instance and save it to a local variable.

Let’s refactor AddColorForm as a stateless functional component:

const AddColorForm = ({onNewColor=f=>f}) => {
    let _title, _color
    const submit = e => {
        e.preventDefault()
        onNewColor(_title.value, _color.value)
        _title.value = ''
        _color.value = '#000000'
        _title.focus()
    }
    return (
        <form onSubmit={submit}>
            <input ref={input => _title = input}
                   type="text" 
                   placeholder="color title..." required/>
            <input ref={input => _color = input}
                   type="color" required/>
            <button>ADD</button>
        </form>
    )
}

In this stateless functional component, refs are set with a callback function instead of a string. The callback function passes the element’s instance as an argument. This instance can be captured and saved into a local variable like _title or _color. Once we’ve saved the refs to local variables, they are easily accessed when the form is submitted.

React State Management

Thus far we’ve only used properties to handle data in React components. Properties are immutable. Once rendered, a component’s properties do not change. In order for our UI to change, we would need some other mechanism that can rerender the component tree with new properties. React state is a built-in option for managing data that will change within a component. When application state changes, the UI is rerendered to reflect those changes.

Users interact with applications. They navigate, search, filter, select, add, update, and delete. When a user interacts with an application, the state of that application changes, and those changes are reflected back to the user in the UI. Screens and menus appear and disappear. Visible content changes. Indicators light up or are turned off. In React, the UI is a reflection of application state.

State can be expressed in React components with a single JavaScript object. When the state of a component changes, the component renders a new UI that reflects those changes. What can be more functional than that? Given some data, a React component will represent that data as the UI. Given a change to that data, React will update the UI as efficiently as possible to reflect that change.

Let’s take a look at how we can incorporate state within our React components.

Introducing Component State

State represents data that we may wish to change within a component. To demonstrate this, we will take a look at a StarRating component (Figure 6-7).

Figure 6-7. The StarRating component

The StarRating component requires two critical pieces of data: the total number of stars to display, and the rating, or the number of stars to highlight.

We’ll need a clickable Star component that has a selected property. A stateless functional component can be used for each star:

const Star = ({ selected=false, onClick=f=>f }) => 
    <div className={(selected) ? "star selected" : "star"}
         onClick={onClick}>
    </div>

Star.propTypes = {
    selected: PropTypes.bool,
    onClick: PropTypes.func
}

Every Star element will consist of a div that includes the class 'star'. If the star is selected, it will additionally add the class 'selected'. This component also has an optional onClick property. When a user clicks on any star div, the onClick property will be invoked. This will tell the parent component, the StarRating, that a Star has been clicked.

The Star is a stateless functional component. It says it right in the name: you cannot use state in a stateless functional component. Stateless functional components are meant to be the children of more complex, stateful components. It’s a good idea to try to keep as many of your components as possible stateless.

The Star Is in the CSS

Our StarRating component uses CSS to construct and display a star. Specifically, using a clip path, we can clip the area of our div to look like a star. The clip path is collection of points that make up a polygon:

.star {
    cursor: pointer;
    height: 25px;
    width: 25px;
    margin: 2px;
    float: left;
    background-color: grey;
    clip-path: polygon(
        50% 0%, 
        63% 38%,
        100% 38%, 
        69% 59%, 
        82% 100%,
        50% 75%, 
        18% 100%, 
        31% 59%,
        0% 38%, 
        37% 38%
    );
}

.star.selected {
  background-color: red;
}

A regular star has a background color of grey, but a selected star will have a background color of red.

Now that we have a Star, we can use it to create a StarRating. StarRating will obtain the total number of stars to display from the component’s properties. The rating, the value that the user can change, will be stored in the state.

First, let’s look at how to incorporate state into a component defined with createClass:

const StarRating = createClass({
    displayName: 'StarRating',
    propTypes: {
        totalStars: PropTypes.number
    }, 
    getDefaultProps() {
        return {
            totalStars: 5
        }
    },
    getInitialState() {
        return {
            starsSelected: 0
        }
    }, 
    change(starsSelected) {
        this.setState({starsSelected})
    },
    render() {
        const {totalStars} = this.props
        const {starsSelected} = this.state
        return (
            <div className="star-rating">
                {[...Array(totalStars)].map((n, i) => 
                    <Star key={i}
                          selected={i<starsSelected}
                          onClick={() => this.change(i+1)}
                    />
                )}
           <p>{starsSelected} of {totalStars} stars</p>
        </div>
    )
  }
})

When using createClass, state can be initialized by adding getInitialState to the component configuration and returning a JavaScript object that initially sets the state variable, starsSelected to 0.

When the component renders, totalStars is obtained from the component’s properties and used to render a specific number of Star elements. Specifically, the spread operator is used with the Array constructor to initialize a new array of a specific length that is mapped to Star elements.

The state variable starsSelected is destructured from this.state when the component renders. It is used to display the rating as text in a paragraph element. It is also used to calculate the number of selected stars to display. Each Star element obtains its selected property by comparing its index to the number of stars that are selected. If three stars are selected, the first three Star elements will set their selected property to true and any remaining stars will have a selected property of false.

Finally, when a user clicks a single star, the index of that specific Star element is incremented and sent to the change function. This value is incremented because it is assumed that the first star will have a rating of 1 even though it has an index of 0.

Initializing state in an ES6 component class is slightly different than using createClass. In these classes, state can be initialized in the constructor:

class StarRating extends Component {
  
    constructor(props) {
        super(props)
        this.state = {
            starsSelected: 0
        }
        this.change = this.change.bind(this)
    }
  
    change(starsSelected) {
        this.setState({starsSelected})
    }
  
    render() {
        const {totalStars} = this.props
        const {starsSelected} = this.state
        return (
            <div className="star-rating">
                {[...Array(totalStars)].map((n, i) => 
                    <Star key={i}
                          selected={i<starsSelected}
                          onClick={() => this.change(i+1)}
                    />
                )}
                <p>{starsSelected} of {totalStars} stars</p>
            </div>
        )
    }
  
}

StarRating.propTypes = {
    totalStars: PropTypes.number
}

StarRating.defaultProps = {
    totalStars: 5
}

When an ES6 component is mounted, its constructor is invoked with the properties injected as the first argument. Those properties are, in turn, sent to the superclass by invoking super. In this case, the superclass is React.Component. Invoking super initializes the component instance, and React.Component decorates that instance with functionality that includes state management. After invoking super , we can initialize our component’s state variables.

Once the state is initialized, it operates as it does in createClass components. State can only be changed by calling this.setState, which updates specific parts of the state object. After every setState call, the render function is called, updating the state with the new UI.

Initializing State from Properties

We can initialize our state values using incoming properties. There are only a few necessary cases for this pattern. The most common case for this is when we create a reusable component that we would like to use across applications in different component trees.

When using createClass, a good way to initialize state variables based on incoming properties is to add a method called componentWillMount. This method is invoked once when the component mounts, and you can call this.setState() from this method. It also has access to this.props, so you can use values from this.props to help you initialize state:

const StarRating = createClass({
    displayName: 'StarRating',
    propTypes: {
        totalStars: PropTypes.number
    }, 
    getDefaultProps() {
        return {
            totalStars: 5
        }
    },
    getInitialState() {
        return {
            starsSelected: 0
        }
    },
    componentWillMount() {
        const { starsSelected } = this.props
        if (starsSelected) {
          this.setState({starsSelected})
        }
    },
    change(starsSelected) {
        this.setState({starsSelected})
    },
    render() {
        const {totalStars} = this.props
        const {starsSelected} = this.state
        return (
            <div className="star-rating">
                {[...Array(totalStars)].map((n, i) => 
                    <Star key={i}
                          selected={i<starsSelected}
                          onClick={() => this.change(i+1)}
                    />
                )}
           <p>{starsSelected} of {totalStars} stars</p>
        </div>
    )
  }
})

render(
    <StarRating totalStars={7} starsSelected={3} />,
    document.getElementById('react-container')
)

componentWillMount is a part of the component lifecycle. It can be used to help you initialize state based on property values in components created with createClass or ES6 class components. We will dive deeper into the component lifecycle in the next chapter.

There is an easier way to initialize state within an ES6 class component. The constructor receives properties as an argument, so you can simply use the props argument passed to the constructor:

constructor(props) {
    super(props)
    this.state = {
        starsSelected: props.starsSelected || 0
    }
    this.change = this.change.bind(this)
}

For the most part, you’ll want to avoid setting state variables from properties. Only use these patterns when they are absolutely required. You should find this goal easy to accomplish because when working with React components, you want to limit the number of components that have state.3

Updating Component Properties

When initializing state variables from component properties, you may need to reinitialize component state when a parent component changes those properties. The componentWillRecieveProps lifecycle method can be used to solve this issue. Chapter 7 goes into greater detail on this issue and the available methods of the component lifecycle.

State Within the Component Tree

All of your React components can have their own state, but should they? The joy of using React does not come from chasing down state variables all over your application. The joy of using React comes from building scalable applications that are easy to understand. The most important thing that you can do to make your application easy to understand is limit the number of components that use state as much as possible.

In many React applications, it is possible to group all state data in the root component. State data can be passed down the component tree via properties, and data can be passed back up the tree to the root via two-way function binding. The result is that all of the state for your entire application exists in one place. This is often referred to as having a “single source of truth.”4

Next, we will look at how to architect presentation layers where all of the state is stored in one place, the root component.

Color Organizer App Overview

The color organizer allows users to add, name, rate, and remove colors in their customized lists. The entire state of the color organizer can be represented with a single array:

{
    colors: [
        {
            "id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
            "title": "ocean at dusk",
            "color": "#00c4e2",
            "rating": 5
        },
        {
            "id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
            "title": "lawn",
            "color": "#26ac56",
            "rating": 3
        },
        {
            "id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
            "title": "bright red",
            "color": "#ff0000",
            "rating": 0
        }
    ]
}

The array tells us that we need to display three colors: ocean at dusk, lawn, and bright red (Figure 6-8). It gives us the colors’ hex values and the current rating for each color in the display. It also provides a way to uniquely identify each color.

Figure 6-8. Color organizer with three colors in state

This state data will drive our application. It will be used to construct the UI every time this object changes. When users add or remove colors, they will be added to or removed from this array. When users rate colors, their ratings will change in the array.

Passing Properties Down the Component Tree

Earlier in this chapter, we created a StarRating component that saved the rating in the state. In the color organizer, the rating is stored in each color object. It makes more sense to treat the StarRating as a presentational component5 and declare it as a stateless functional component. Presentational components are only concerned with how things look in the application. They only render DOM elements or other presentational components. All data is sent to these components via properties and passed out of these components via callback functions.

In order to make the StarRating component purely presentational, we need to remove state. Presentational components only use props. Since we are removing state from this component, when a user changes the rating, that data will be passed out of this component via a callback function:

const StarRating = ({starsSelected=0, totalStars=5, onRate=f=>f}) =>
    <div className="star-rating">
        {[...Array(totalStars)].map((n, i) =>
            <Star key={i}
                  selected={i<starsSelected}
                  onClick={() => onRate(i+1)}/>
        )}
        <p>{starsSelected} of {totalStars} stars</p>
    </div>

First, starsSelected is no longer a state variable; it is a property. Second, an onRate callback property has been added to this component. Instead of calling setState when the user changes the rating, this component now invokes onRate and sends the rating as an argument.

State in Reusable Components

You may need to create stateful UI components for distribution and reuse across many different applications. It is not absolutely required that you remove every last state variable from components that are only used for presentation. It is a good rule to follow, but sometimes it may make sense to keep state in a presentation component.

Restricting state to a single location, the root component, means that all of the data must be passed down to child components as properties (Figure 6-9).

In the color organizer, state consists of an array of colors that is declared in the App component. Those colors are passed down to the ColorList component as a property:

class App extends Component {

    constructor(props) {
        super(props)
        this.state = {
            colors: []
        }
    }

    render() {
        const { colors } = this.state
        return (
            <div className="app">
                <AddColorForm />
                <ColorList colors={colors} />
            </div>
        )
    }

}
State is passed from the App component to child components as properties.
Figure 6-9. State is passed from the App component to child components as properties

Initially the colors array is empty, so the ColorList component will display a message instead of each color. When there are colors in the array, data for each individual color is passed to the Color component as properties:

const ColorList = ({ colors=[] }) =>
    <div className="color-list">
        {(colors.length === 0) ?
            <p>No Colors Listed. (Add a Color)</p> :
            colors.map(color =>
                <Color key={color.id} {...color} />
            )
        }
    </div>

Now the Color component can display the color’s title and hex value and pass the color’s rating down to the StarRating component as a property:

const Color = ({ title, color, rating=0 }) =>
    <section className="color">
        <h1>{title}</h1>
        <div className="color" 
             style={{ backgroundColor: color }}>
        </div>
        <div>
            <StarRating starsSelected={rating} />
        </div>
    </section>

The number of starsSelected in the star rating comes from each color’s rating. All of the state data for every color has been passed down the tree to child components as properties. When there is a change to the data in the root component, React will change the UI as efficiently as possible to reflect the new state.

Passing Data Back Up the Component Tree

State in the color organizer can only be updated by calling setState from the App component. If users initiate any changes from the UI, their input will need to be passed back up the component tree to the App component in order to update the state (Figure 6-10). This can be accomplished through the use of callback function properties.

Figure 6-10. Passing data back up to the root component when there are UI events

In order to add new colors, we need a way to uniquely identify each color. This identifier will be used to locate colors within the state array. We can use the uuid library to create absolutely unique IDs:

npm install uuid --save

All new colors will be added to the color organizer from the AddColorForm component that we constructed in “Refs”. That component has an optional callback function property called onNewColor. When the user adds a new color and submits the form, the onNewColor callback function is invoked with the new title and color hex value obtained from the user:

import { Component } from 'react'
import { v4 } from 'uuid'
import AddColorForm from './AddColorForm'
import ColorList from './ColorList'

export class App extends Component {

    constructor(props) {
        super(props)
        this.state = {
            colors: []
        }
        this.addColor = this.addColor.bind(this)
    }

    addColor(title, color) {
        const colors = [
            ...this.state.colors,
            {
                id: v4(),
                title,
                color,
                rating: 0
            }
        ]
        this.setState({colors})
    }

    render() {
        const { addColor } = this
        const { colors } = this.state
        return (
            <div className="app">
                <AddColorForm onNewColor={addColor} />
                <ColorList colors={colors} />
            </div>
        )
    }

}

All new colors can be added from the addColor method in the App component. This function is bound to the component in the constructor, which means that it has access to this.state and this.setState.

New colors are added by concatenating the current colors array with a new color object. The ID for the new color object is set using uuid’s v4 function. This creates a unique identifier for each color. The title and color are passed to the addColor method from the AddColorForm component. Finally, the initial value for each color’s rating will be 0.

When the user adds a color with the AddColorForm component, the addColor method updates the state with a new list of colors. Once the state has been updated, the App component rerenders the component tree with the new list of colors. The render method is invoked after every setState call. The new data is passed down the tree as properties and is used to construct the UI.

If the user wishes to rate or remove a color, we need to collect information about that color. Each color will have a remove button: if the user clicks the remove button, we’ll know they wish to remove that color. Also, if the user changes the color’s rating with the StarRating component, we want to change the rating of that color:

const Color = ({title,color,rating=0,onRemove=f=>f,onRate=f=>f}) =>
    <section className="color">
        <h1>{title}</h1>
        <button onClick={onRemove}>X</button>
        <div className="color"
             style={{ backgroundColor: color }}>
        </div>
        <div>
            <StarRating starsSelected={rating} onRate={onRate} />
        </div>
    </section>

The information that will change in this app is stored in the list of colors. Therefore, onRemove and onRate callback properties will have to be added to each color to pass those events back up the tree. The Color component will also have onRate and onRemove callback function properties. When colors are rated or removed, the ColorList component will need to notify its parent, the App component, that the color should be rated or removed:

const ColorList = ({ colors=[], onRate=f=>f, onRemove=f=>f }) =>
    <div className="color-list">
        {(colors.length === 0) ?
            <p>No Colors Listed. (Add a Color)</p> :
            colors.map(color =>
                <Color key={color.id}
                       {...color}
                       onRate={(rating) => onRate(color.id, rating)}
                       onRemove={() => onRemove(color.id)} />
            )
        }
    </div>

The ColorList component will invoke onRate if any colors are rated and onRemove if any colors are removed. This component manages the collection of colors by mapping them to individual Color components. When individual colors are rated or removed the ColorList identifies which color was rated or removed and passes that info to its parent via callback function properties.

ColorList’s parent is App. In the App component, rateColor and removeColor methods can be added and bound to the component instance in the constructor. Any time a color needs to be rated or removed, these methods will update the state. They are added to the ColorList component as callback function properties:

class App extends Component {

    constructor(props) {
        super(props)
        this.state = {
            colors: []
        }
        this.addColor = this.addColor.bind(this)
        this.rateColor = this.rateColor.bind(this)
        this.removeColor = this.removeColor.bind(this)
    }

    addColor(title, color) {
        const colors = [
            ...this.state.colors,
            {
                id: v4(),
                title,
                color,
                rating: 0
            }
        ]
        this.setState({colors})
    }

    rateColor(id, rating) {
        const colors = this.state.colors.map(color =>
            (color.id !== id) ?
                color :
                {
                    ...color,
                    rating
                }
        )
        this.setState({colors})
    }

    removeColor(id) {
        const colors = this.state.colors.filter(
            color => color.id !== id
        )
        this.setState({colors})
    }

    render() {
        const { addColor, rateColor, removeColor } = this
        const { colors } = this.state
        return (
            <div className="app">
                <AddColorForm onNewColor={addColor} />
                <ColorList colors={colors}
                           onRate={rateColor}
                           onRemove={removeColor} />
            </div>
        )
    }

}

Both rateColor and removeColor expect the ID of the color to rate or remove. The ID is captured in the ColorList component and passed as an argument to rateColor or removeColor. The rateColor method finds the color to rate and changes its rating in the state. The removeColor method uses Array.filter to create a new state array without the removed color.

Once setState is called, the UI is rerendered with the new state data. All data that changes in this app is managed from a single component, App. This approach makes it much easier to understand what data the application uses to create state and how that data will change.

React components are quite robust. They provide us with a clean way to manage and validate properties, communicate with child elements, and manage state data from within a component. These features make it possible to construct beautifully scalable presentation layers.

We have mentioned many times that state is for data that changes. You can also use state to cache data in your application. For instance, if you had a list of records that the user could search, the records list could be stored in state until they are searched.

Reducing state to root components is often recommended. You will encounter this approach in many React applications. Once your application reaches a certain size, two-way data binding and explicitly passing properties can become quite a nuisance. The Flux design pattern and Flux libraries like Redux can be used to manage state and reduce boilerplate in these situations.

React is a relatively small library, and thus far we’ve reviewed much of its functionality. The major features of React components that we have yet to discuss include the component lifecycle and higher-order components, which we will cover in the next chapter.

1 React Docs, “Default Prop Values”

2 Pete Hunt, “Thinking in React”.

3 React Docs, “Lifting State Up”.

4 Paul Hudson, “State and the Single Source of Truth”, Chapter 12 of Hacking with React.

5 Dan Abramov, “Presentational and Container Components”, Medium, March 23, 2015.

..................Content has been hidden....................

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