Since JavaScript is a dynamically typed language, we don’t really have a way to enforce the type of a variable at compile time. If we pass invalid types, they will fail at runtime or give weird results if the types are compatible but not what we expect.
Flow and TypeScript help a lot, but React has a way to directly help with props types. Even before running the code, our tools (editors, linters) can detect when we are passing the wrong values:
import PropTypes from 'prop-types';
import React from 'react'
class BlogPostExcerpt extends Component {
render() {
return (
<div>
<h1>{this.props.title}</h1>
<p>{this.props.description}</p>
</div>
)
}
}
BlogPostExcerpt.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
export default BlogPostExcerpt
Which types can we use
These are the fundamental types we can accept:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.symbol
We can accept one of two types:
PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
We can accept one of many values:
PropTypes.oneOf(['Test1', 'Test2']),
We can accept an instance of a class:
PropTypes.instanceOf(Something)
We can accept any React node:
PropTypes.node
or even any type at all:
PropTypes.any
Arrays have a special syntax that we can use to accept an array of a particular type:
PropTypes.arrayOf(PropTypes.string)
We can compose an object property by using:
PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
Requiring properties
Appending isRequired
to any PropTypes option will cause React to return an error if that property is missing:
PropTypes.arrayOf(PropTypes.string).isRequired, PropTypes.string.isRequired,
Default values for props
If any value is not required, we need to specify a default value for it if it’s missing when the Component is initialized.
BlogPostExcerpt.propTypes = {
title: PropTypes.string,
description: PropTypes.string
}
BlogPostExcerpt.defaultProps = {
title: '',
description: ''
}
Some tooling, like ESLint, has the ability to enforce defining the defaultProps for a Component with some propTypes not explicitly required.
How props are passed
When initializing a component, pass the props in a way similar to HTML attributes:
const desc = 'A description'
//...
<BlogPostExcerpt title="A blog post" description={desc} />
We passed the title as a plain string (something we can only do with strings!), and the description as a variable.
Children
A special prop is children
. That contains the value of anything that is passed in the body
of the component. For example:
<BlogPostExcerpt title="A blog post" description={desc}>
Something
</BlogPostExcerpt>
In this case, inside BlogPostExcerpt
we could access “Something” by looking up this.props.children
.
While Props allow a Component to receive properties from its parent (they could be “instructed” to print some data for example), state allows a component to take on a life of its own, and be independent from the surrounding environment.
Remember: only class-based Components can have a state. So if you need to manage state in a stateless (function-based) Component, you first need to “upgrade” it to a Class component:
const BlogPostExcerpt = () => {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
)
}
becomes:
import React, { Component } from 'react'
class BlogPostExcerpt extends Component {
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
)
}
}
Setting the default state
In the Component constructor, initialize this.state
. For example, the BlogPostExcerpt component might have a clicked
state:
class BlogPostExcerpt extends Component {
constructor(props) {
super(props)
this.state = { clicked: false }
}
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
)
}
}
Accessing the state
The clicked state can be accessed by referencing this.state.clicked
:
class BlogPostExcerpt extends Component {
constructor(props) {
super(props)
this.state = { clicked: false }
}
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
<p>Clicked: {this.state.clicked}</p>
</div>
)
}
}
Mutating the state
A state should never be mutated by using
this.state.clicked = true
Instead, you should always use setState()
instead, passing it as an object:
this.setState({ clicked: true })
The object can contain a subset, or a superset, of the state. Only the properties you pass will be mutated. The ones omitted will be left in their current state.
Why you should always use setState()
The reason is that using this method, React knows that the state has changed. It will then start the series of events that will lead to the Component being re-rendered, along with any DOM updates.
State is encapsulated
A parent of a Component cannot tell if the child is stateful or stateless. The same goes for children of a Component.
Being stateful or stateless (functional or class-based) is entirely an implementation detail that other components don’t need to care about.
This leads us to Unidirectional Data Flow
Unidirectional Data Flow
A state is always owned by one Component. Any data that’s affected by this state can only affect Components below it: its children.
Changing a state on a Component will never affect its parent, or its siblings, or any other Component in the application — just its children.
This is the reason that, many times, the state is moved up in the Components tree.
Moving the State Up in the Tree
Because of the Unidirectional Data Flow rules, if two components need to share a state, the state needs to be moved up to a common ancestor.
Often, the closest ancestor is the best place to manage the state, but it’s not a mandatory rule.
The state is passed down to the components that need that value via props:
class Converter extends React.Component {
constructor(props) {
super(props)
this.state = { currency: '€' }
}
render() {
return (
<div>
<Display currency={this.state.currency} />
<CurrencySwitcher currency={this.state.currency} />
</div>
)
}
}
The state can be mutated by a child component by passing a mutating function down as a prop:
class Converter extends React.Component {
constructor(props) {
super(props)
this.state = { currency: '€' }
}
handleChangeCurrency = (event) => {
this.setState({
currency: this.state.currency === '€' ? '$' : '€'
})
}
render() {
return (
<div>
<Display currency={this.state.currency} />
<CurrencySwitcher currency={this.state.currency} handleChangeCurrency={this.handleChangeCurrency} />
</div>
)
}
}
const CurrencySwitcher = (props) => {
return (
<button onClick={props.handleChangeCurrency}>
Current currency is {props.currency}. Change it!
</button>
)
}
const Display = (props) => {
return (
<p>Current currency is {props.currency}.</p>
)
}