Components
As we alluded to in part one of this tutorial, the key building block in Virtual DOM applications is the Component. A Component is a self-contained piece of an application that can be rendered as part of the Virtual DOM tree just like an HTML element. You can think of a Component like a function call: both are mechanisms that allow code reuse and indirection.
To illustrate, let's create a simple component called MyButton
that
returns a Virtual DOM tree describing an HTML <button>
element:
function MyButton(props) {
return <button class="my-button">{props.text}</button>
}
We can use this component in an application by referencing it in JSX:
let vdom = <MyButton text="Click Me!" />
// remember createElement? here's what the line above compiles to:
let vdom = createElement(MyButton, { text: "Click Me!" })
Anywhere you use JSX to describe trees of HTML, you can also describe trees of Components. The difference is that a component is described in JSX using a name beginning with an uppercase character that corresponds to the component's name (a JavaScript variable).
As Preact renders the Virtual DOM tree described by your JSX, each component
function it encounters will be invoked at that spot in the tree. As an example,
we can render our MyButton
component into the body of a web page by passing
a JSX element describing that component to render()
:
import { render } from 'preact';
render(<MyButton text="Click me!" />, document.body)
Nesting Components
Components can reference other components in Virtual DOM tree they return. This creates a tree of components:
function MediaPlayer() {
return (
<div>
<MyButton text="Play" />
<MyButton text="Stop" />
</div>
)
}
render(<MediaPlayer />, document.body)
We can use this technique to render different trees of components for
different scenarios. Let's make that MediaPlayer
show a "Play" button
when no sound is playing, and a "Stop" button when sound is playing:
function MediaPlayer(props) {
return (
<div>
{props.playing ? (
<MyButton text="Stop" />
) : (
<MyButton text="Play" />
)}
</div>
)
}
render(<MediaPlayer playing={false} />, document.body)
// renders <button>Play</button>
render(<MediaPlayer playing={true} />, document.body)
// renders <button>Stop</button>
Remember:
{curly}
braces in JSX let us jump back into plain JavaScript. Here we're using a ternary expression to show different buttons based on the value of theplaying
prop.
Component Children
Components can also be nested just like HTML elements. One of the reasons Components are a powerful primitive is because they let us apply custom logic to control how Virtual DOM elements nested within a component should be rendered.
The way this works is deceptively simple: any Virtual DOM elements nested
within a component in JSX are passed to that component as a special children
prop. A component can choose where to place its children by referencing them in
JSX using a {children}
expression. Or, components can simply return the
children
value, and Preact will render those Virtual DOM elements right where
that Component was placed in the Virtual DOM tree.
<Foo>
<a />
<b />
</Foo>
function Foo(props) {
return props.children // [<a />, <b />]
}
Thinking back to the previous example, our MyButton
component expected
a text
prop that was inserted into a <button>
element as its display
text. What if we wanted to display an image instead of text?
Let's rewrite MyButton
to allow nesting using the children
prop:
function MyButton(props) {
return <button class="my-button">{props.children}</button>
}
function App() {
return (
<MyButton>
<img src="icon.png" />
Click Me!
</MyButton>
)
}
render(<App />, document.body)
Now that we've seen a few examples of components rendering other components, hopefully it's starting to become clear how nested components let us assemble complex applications out of many smaller individual pieces.
Types of Components
So far, we've seen Components that are functions. Function components
take in props
as their input, and return a Virtual DOM tree as
their output. Components can also be written as JavaScript classes,
which get instantiated by Preact and provide a render()
method that
works much like a function component.
Class components are created by extending Preact's Component
base class.
In the example below, notice how render()
takes props
as its input and
returns a Virtual DOM tree as its output - just like a function component!
import { Component } from 'preact';
class MyButton extends Component {
render(props) {
return <button class="my-button">{props.children}</button>
}
}
render(<MyButton>Click Me!</MyButton>, document.body)
The reason we might use a class to define a component is to keep track of
the lifecycle of our component. Each time Preact encounters a component
when rendering a Virtual DOM tree, it will create a new instance of our
class (new MyButton()
).
However, if you recall from chapter one - Preact can be repeatedly given new Virtual DOM trees. Each time we give Preact a new tree, it gets compared against the previous tree to determine what changed between the two, and those changes are applied to the page.
When a component is defined using a class, any updates to that component
in the tree will reuse the same class instance. That means it's possible to
store data inside a class component that will be available the next time
its render()
method is called.
Class components can also implement a number of lifecycle methods, which Preact will call in response to changes in the Virtual DOM tree:
class MyButton extends Component {
componentDidMount() {
console.log('Hello from a new <MyButton> component!')
}
componentDidUpdate() {
console.log('A <MyButton> component was updated!')
}
render(props) {
return <button class="my-button">{props.children}</button>
}
}
render(<MyButton>Click Me!</MyButton>, document.body)
// logs: "Hello from a new <MyButton> component!"
render(<MyButton>Click Me!</MyButton>, document.body)
// logs: "A <MyButton> component was updated!"
The lifecycle of class components makes them a useful tool for building
pieces of an application that respond to changes, rather than strictly
mapping props
to trees. They also provide a way to store information
separately at each location where they're placed in the Virtual DOM tree.
In the next chapter, we'll see how components can update their section of
the tree whenever they want to change it.
Try it!
To practise, let's combine what we've learned about components with our event skills from the previous two chapters!
Create a MyButton
component that accepts style
, children
and onClick
props, and returns an HTML <button>
element with those props applied.