Components and Props
Components let you to break the UI down into small pieces. This helps you think about each bit of functionality in isolation.
At a high level, components are like functions. They take arbitrary arguments and return elements to inject in the document.
Functional and Class Components
The simplest way to create a component is to write a function. We use props to pass down the data to the component. To do this, we wrap the function component's argument in curly braces. And when we pass the JSX tag to the render function, we pass the data to use inside curly braces again using the spread operator:
See the Pen Composi Tuts - Components and Props-1 by Robert Biggs (@rbiggs) on CodePen.
The above example is what is called a functional component. It's a function that returns markup to create. Another type of component is create using the Component class:
See the Pen Composi Tuts - Components and Props-2 by Robert Biggs (@rbiggs) on CodePen.
For a class-based component, the render
is defined directly on the component. We cause the class-based component to be rendered by passing data to its update
function.
In the above two examples, the result of the function component and class-based component are identical. Class-based component offer more functionality to create complex components.
Composing Components
A component can contain other components. A functional component can have other functional components as children. A class-based component can have functional components as children. However, a class-based component cannot have another class-based components as its children. Composi does not encourage creating a giant app with one overruling component that renders your entire app of many child components. Do not build components that have deeply nested trees of descendent components. Instead, break your app down into smaller, independent components. If they do need to communicate, you can use any pubsub library for that.
Here's an example of a functional component with other functional components as its children:
See the Pen Composi Tuts - Components and Props-3 by Robert Biggs (@rbiggs) on CodePen.
Notice that we enclose all our Welcome
tags in a div
tag. That's because there must always be one enclosing tag for any markup we want to produce.
Breaking Components Down
You should always try to break your components down into smaller pieces that get assembled into the final component. Take the following component as an example:
See the Pen Composi Tuts - Components and Props-4 by Robert Biggs (@rbiggs) on CodePen.
This component is too complicated, so we are going to see how to break it down into smaller pieces. This is tricky because we need to pass data down to the child elements.
The first thing we'll do is extract the avatar part:
function Avatar(props) {
return (
<img class="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
)
}
When breaking a component down, consider renaming the props to what makes sense at the component level. Since the above component knows nothing of the author, we changed its prop to user. With the above sub-component, we can simplify our main component a bit:
function Avatar(props) {
return (
<img class="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
)
}
function Comment(props) {
return (
<div class="Comment">
<div class="UserInfo">
<Avatar user={props.author} />
<div class="UserInfo-name">
{props.author.name}
</div>
</div>
<div class="Comment-text">
{props.text}
</div>
<div class="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
Next we extract the user info, which includes the avatar component:
function UserInfo(props) {
return (
<div class="UserInfo">
<Avatar user={props.user} />
<div class="UserInfo-name">
{props.user.name}
</div>
</div>
)
}
With this change, we can simplify the component setup ever more:
function Avatar(props) {
return (
<img class="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
)
}
function UserInfo(props) {
return (
<div class="UserInfo">
<Avatar user={props.user} />
<div class="UserInfo-name">
{props.user.name}
</div>
</div>
)
}
function Comment(props) {
return (
<div class="Comment">
<UserInfo user={props.author} />
<div class="Comment-text">
{props.text}
</div>
<div class="Comment-date">
{formatDate(props.date)}
</div>
</div>
)
}
And here's the working example:
See the Pen Composi Tuts - Components and Props-5 by Robert Biggs (@rbiggs) on CodePen.
If you wanted to, you could put the sub-components--Avatar and UserInfo--in separated files and import them into the Comment file. This would allow you to reuse them elsewhere in your project, or even reuse them in other projects.
Props Should Be Read-Only
Whether you create a functional or class-based component, it should never modify its own props. When functions don't modify their props, they are called pure. Take the following function as an example:
function sum(a, b) {
return a + b
}
Given the same data, this function will always return the same result. This is a pure function because it does not change what its inputs are. In contrast, the following function is impure because it changes its inputs:
function withdraw(account, amount) {
account.total -= amount
}
You should strive to make components act like pure functions with respect to their props. Of course, UI applications are dynamic and have user interactions. To handle these you can create stateful components. State allows components to change their output over time or due to user interaction without violating the pinciple of purity.
The Component Class
The Component class provides a useful API for makig more complex components than is possible with functional components. There are two ways to use the Component class: by creating an instance of it, or by extending it.
// Creating a new instance:
const title = new Comonent({})
// Extending the Component class:
class Title extends Component {
constuctor(props) {
super(props)
}
}
Next we're going to look at extending Component. To learn about the component instance, refer to the docs.
As you can see above, to extend the Component class we usually need to include a constructor. This needs to have props passed all the way down to the Class super
.
After that we need to tell the component where it should render. For that we use the container
property. We also need to define a render
funtion. Its purpose is to return the markup for the comonent.
See the Pen Composi Tuts - Components and Props-6 by Robert Biggs (@rbiggs) on CodePen.
If you wanted to be able to render a component in different containers, you can leave it out and provide it as an argument in the component instantiation. You're free to take either approach: have all components rendered in the same container, or enable them to be rendered in different containers at initialization time.
See the Pen Composi Tuts - Components and Props-7 by Robert Biggs (@rbiggs) on CodePen.
No Container
It is possible to define a class component without providing a container
. In such a case the component will be rendered in the body tag. If you will have no HTML shell in your document, you can use this technique to render your components. However, this is not a good practice. It means that when your document loads, the user may at first see nothing until the component renders. And if there is a JavaScript error, the user will get a blank page. Not good. It's therefore always best to provide a basic HTML shell into which you render you components. Always give your components a container
.
Querying the Component Tree
Often when you are implementing complex components with events, you need to reach down into the child components to get a value from some input. You can simply the query by using the component's element
property. This is the base element at the top of the markup hierarchy being returned by the component's render
function. So, if a component return markup as follows:
render(data) {
return (
<div class='list__container'>
<ul>
{
data.map(item => <li>{item}</li>)
}
</ul>
</div>
)
}
For the above component, this.element
with be <div class='list__container'></div>
. Using this.element
as the starting point, we can limit our DOM query to the component base:
componentWasCreated() {
// Use "this.element" as base for event:
this.element.addEventListener('click', this)
}
handleEvent(e) {
e.target.nodeName === 'BUTTON' && this.sayHello()
}
sayHello() {
alert('Hello!')
}
You could also do something like:
announce() {
// Use "this.element" as base for query:
const input = this.element.querySelector('input')
const value = input.value
if (value) {
this.setState(value)
}
}