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
ReactElementextensively. Checkout that chapter if you want to understandReactElementbetter.
The goal of a ReactComponent is to
-
render()aReactElement(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 everyReactComponent -
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:
React.createClass()or- 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
PropTypesfor more details and code examples onPropTypes
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
contextfeature is experimental and it's similar to using a global variable to handle state in an application - i.e. minimize the use ofcontextas relying on it too frequently is a code smell.That is,
contextworks 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:
childContextTypesandgetChildContext
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:
contextTypesand Lifecycle methodsWe talk about component lifecycle, such as
componentDidUpdatein the Component Lifecycle Chapter.