Advanced Component Configuration with props, state, and children

Intro

In this chapter we're going to dig deep into the configuration of components.

A ReactComponent is a JavaScript object that, at a minimum, has a render() function. render() is expected to return a ReactElement.

Recall that ReactElement is a representation of a DOM element in the Virtual DOM.

In the chapter on JSX and the Virtual DOM we talked about ReactElement extensively. Checkout that chapter if you want to understand ReactElement better.

The goal of a ReactComponent is to

  • render() a ReactElement (which will eventually become the real DOM) and
  • attach functionality to this section of the page

"Attaching functionality" is a bit ambiguous; it includes attaching event handlers, managing state, interacting with children, etc. In this chapter we're going to cover:

  • render() - the one required function on every ReactComponent
  • props - the "input parameters" to our components
  • context - a "global variable" for our components
  • state - a way to hold data that is local to a component (that affects rendering)
  • Stateless components - a simplified way to write reusable components
  • children - how to interact and manipulate child components
  • statics - how to create "class methods" on our components

Let's get started!

ReactComponent

Creating ReactComponents - createClass or ES6 Classes

As discussed in the first chapter, there are two ways to define a ReactComponent:

  1. React.createClass() or
  2. ES6 classes

As we've seen, the two methods of creating components are roughly equivalent:


// React.createClass
const App = React.createClass({
  render: function() {} // required method
});

// ES6 class-style
class App extends React.Component {
  render() {} // required
}

Regardless of the method we used to define the ReactComponent, React expects us to define the render() function.

render() Returns a ReactElement Tree

The render() method is the only required method to be defined on a ReactComponent.

After the component is mounted and initialized, render() will be called. The render() function's job is to provide React a virtual representation of a native DOM component.

An example of using React.createClass with the render function might look like this:


const Heading = React.createClass({
  render: function() {
    return (
      <h1>Hello</h1>
    )
  }
});

The above code should look familiar. It describes a Heading component class with a single render() method that returns a simple, single Virtual DOM representation of a <h1> tag.

Remember that this render() method returns a ReactElement which isn't part of the "actual DOM", but instead a description of the Virtual DOM.

React expects the method to return a single child element. It can be a virtual representation of a DOM component or can return the falsy value of null or false. React handles the falsy value by rendering an empty element (a <noscript /> tag). This is used to remove the tag from the page.

Keeping the render() method side-effect free provides an important optimization and makes our code easier to understand.

Getting Data into render()

Of course, while render is the only required method, it isn't very interesting if the only data we can render is known at compile time. That is, we need a way to:

  • input "arguments" into our components and
  • maintain state within a component.

React provides ways to do both of these things, with props and state, respectively.

Understanding these are crucial to making our components dynamic and useable within a larger app.

In React, props are immutable pieces of data that are passed into child components from parents (if we think of our component as the "function" we can think of props as our component's "arguments").

Component state is where we hold data, local to a component. Typically, when our component's state changes, the component needs to be re-rendered. Unlike props, state is private to a component and is mutable.

We'll look at both props and state in detail below. Along the way we'll also talk about context, a sort of "implicit props" that gets passed through the whole component tree.

Let's look at each of these in more detail.

props are the parameters

props are the inputs to your components. If we think of our component as a "function", we can think of the props as the "parameters".

Let's look at an example:


<div>
  <Header headerText="Hello world" />
</div>

In the example code, we're creating both a <div> and a <Header> element, where the <div> is a usual DOM element, while <Header> is an instance of our Header component.

In this example, we're passing data from the component (the string "Hello world") through the attribute headerText to the component.

Passing data through attributes to the component is often called passing props.

When we pass data to a component through an attribute it becomes available to the component through the this.props property. So in this case, we can access our headerText through the property this.props.headerText:


const Header = React.createClass({
  render: function() {
    return (
      <h1>{this.props.headerText}</h1>
    )
  }
});

While we can access the headerText property, we cannot change it.

By using props we've taken our static component and allowed it to dynamically render whatever headerText is passed into it. The <Header> component cannot change the headerText, but it can use the headerText itself or pass it on to it's children.

We can pass any JavaScript object through props. We can pass primitives, simple JavaScript objects, atoms, functions etc. We can even pass other React elements and Virtual DOM nodes.

We can document the functionality of our components using props and we can specify the type of each prop by using PropTypes.

PropTypes

PropTypes are a way to validate the values that are passed in through our props. Well-defined interfaces provide us with a layer of safety at the run time of our apps. They also provide a layer of documentation to the consumer of our components.

We define PropTypes by passing them as an option to createClass():

const Component = React.createClass({
  propTypes: {
    name: React.PropTypes.string,
    totalCount: React.PropTypes.number
  },
  // ...
})

In the example above, our component will validate that name is a string and that totalCount is a number.

There are a number of built-in PropTypes, and we can define our own.

We've written a code example for many of the PropTypes validators here in the appendix on PropTypes. For more details on PropTypes, check out that appendix.

For now, we need to know that there are validators for scalar types:

We can also validate complex types such as:

We can also validate a particular shape of an input object, or validate that it is an instanceOf a particular class.

Checkout the appendix on PropTypes for more details and code examples on PropTypes

Default props with getDefaultProps()

Sometimes we want our props to have defaults. We can use the getDefaultProps() method to do this.

For instance, create a Counter component definition and tell the component that if no initialValue is set in the props to set it to 1 using getDefaultProps():

const Counter = React.createClass({
  getDefaultProps: function() {
    return {
      initialValue: 1
    }
  },
  // ...
});

Now the component can be used without setting the initialValue prop. The two usages of the component are functionally equivalent:

<Counter />
<Counter initialValue={1} />

The getDefaultProps() method is called once when the class is defined (and cached). The values in the mapped object returned by this method will be set on this.props if the prop is not specified by the parent component.

As the getDefaultProps() method invoked called before any instances are created, we cannot use any instance variables, such as this.props in this method. In addition, any complex objects returned by getDefaultProps() are shared across all instances, not copied.

context

Sometimes we might find that we have a prop which we want to expose "globally". In this case, we might find it cumbersome to pass this particular prop down from the root, to every leaf, through every intermediate component.

Instead, specifying context allows us to automatically pass down variables from component to component, rather than needing to pass down our props at every level,

The context feature is experimental and it's similar to using a global variable to handle state in an application - i.e. minimize the use of context as relying on it too frequently is a code smell.

That is, context works best for things that truly are global, such as the central store in Redux.

When we specify a context, React will take care of passing down context from component to component so that at any point in the tree hierarchy, any component can reach up to the "global" context where it's defined and get access to the parent's variables.

In order to tell React we want to pass context from a parent component to the rest of it's children we need to define two attributes in the parent class:

  • childContextTypes and
  • getChildContext

To retrieve the context inside a child component, we need to define the contextTypes in the child.

To illustrate, let's look at a possible message reader implementation:

const Messages = React.createClass({
  propTypes: {
    users: PropTypes.array.isRequired,
    messages: PropTypes.array.isRequired
  },
  render: function() {
    return (
      <div>
         <ThreadList />
         <ChatWindow />
      </div>
    )
  }
});
const ThreadList = React.createClass({
  render: function() {
    // ...
  }
});
const ChatWindow = React.createClass({
  render: function() {
    // ...
  }
});
const ChatMessage = React.createClass({
  render: function() {
    // ...
  }
});

Without context, our MessagesApp will have to pass the users along with the messages to the two child components (which in turn pass them to their children). Let's set up our hierarchy to accept context instead of needing to pass down this.props.users and this.props.messages along with every component.

In the MessagesApp component, we'll define the two required properties. First, we need to tell React what the types of our context.

We define this with the childContextTypes key. Similar to propTypes, the childContextTypes is a key-value object that lists the keys as the name of a context item and the value is a React.PropType.

Implementing childContextTypes in our MessagesApp component looks like the following:

const MessagesApp = React.createClass({
  childContextTypes: {
    users: PropTypes.array
  },
  // ...
});

Just like propTypes, the childContextTypes doesn't populate the context, it just defines it. In order to fill data the this.context object, we need to define the second required property function: getChildContext().

The getChildContext() function is akin to the getInitialState() function in that we can set the values of our context in the function. Back in our MessagesApp component, we will set our users context object to the value of the this.props.users given to the component.

const MessagesApp = React.createClass({
  childContextTypes: {
    users: PropTypes.array
  },
  getChildContext: function() {
    return {
      users: this.getUsers()
    }
  },
  // ...
});

Since the state and props of a component can change, the context can change as well. The getChildContext() method in the parent component gets called every time the state or props change on the parent component. If the context is updated, then the children will receive the updated context and will subsequently be re-rendered.

With the two required properties set on the parent component, React automatically passes the object down it's subtree where any component can reach into it. In order to grab the context in a child component, we need to tell React we want access to it. We communicate this to React using the contextTypes definition in the child.

Without the contextTypes property on the child React component, React won't know what to send our component. Let's give our child components access to the context of our MessagesApp.

const ThreadList = React.createClass({
  contextTypes: {
    users: PropTypes.array,
  },
  render: function() {
    // ...
  }
});
const ChatWindow = React.createClass({
  contextTypes: {
    users: PropTypes.array,
  },
  render: function() {
    // ...
  }
});
const ChatMessage = React.createClass({
  contextTypes: {
    users: PropTypes.array,
  },
  render: function() {
    // ...
  }
});

Now anywhere in any one of our child components (that have contextTypes defined), we can reach into the parent and grab the users without needing to pass them along manually via props. The context data is set on the this.context object of the component with contextTypes defined.

For instance, our complete ThreadList might look something like:

const ThreadList = React.createClass({
  contextTypes: {
    users: PropTypes.array,
  },

  render: function() {
    return (
      <div>
        <ul>
          {this.context.users.map((u, idx) => (
            <UserListing onClick={this.props.onClick}
                      key={idx}
                      index={idx}
                      user={u} />))}
        </ul>
      </div>
    )
  }
})

If contextTypes is defined on a component, then several of it's lifecycle methods will get passed an additional argument of nextContext:

contextTypes and Lifecycle methods

We talk about component lifecycle, such as componentDidUpdate in the Component Lifecycle Chapter.