Now that you know how to use the ready-made DOM components, it’s time to learn how to make some of your own.
There are two ways to define a custom component, both accomplishing the same result but using different syntax:
Using a function (components created this way are referred to as functional components)
Using a class that extends React.Component
(commonly referred to as class components)
Here’s an example of a functional component:
const
MyComponent
=
function
()
{
return
'I am so custom'
;
};
But wait, this is just a function! Yes, this is it, the custom component is just a function that returns the UI that you want. In this case, the UI is only text but often you’ll need a little bit more, most likely a composition of other components. Here’s an example of using a span
to wrap the text:
const
MyComponent
=
function
()
{
return
React
.
createElement
(
'span'
,
null
,
'I am so custom'
);
};
Using your new shiny component in an application is similar to using the DOM components from Chapter 1, except you call the function that defines the component:
ReactDOM
.
render
(
MyComponent
()
,
document
.
getElementById
(
'app'
)
);
The result of rendering your custom component is shown in Figure 2-1.
The same example using JSX would look a little easier to read. Defining the component looks like this:
const
MyComponent
=
function
()
{
return
<
span
>
I
am
so
custom
</
span
>;
};
Using the component the JSX way looks like the following, regardless of how the component itself was defined (with JSX or not).
ReactDOM
.
render
(
<
MyComponent
/>,
document
.
getElementById
(
'app'
)
);
Notice that in the self-closing tag <MyComponent />
the slash is not optional. That applies to HTML elements used in JSX too. <br>
and <img>
are not going to work, you need to close them like <br/>
and <img/>
.
The second way to create a component is to define a class that extends React.Component
and implements a render()
function:
class
MyComponent
extends
React
.
Component
{
render
()
{
return
React
.
createElement
(
'span'
,
null
,
'I am so custom'
);
// or with JSX:
// return <span>I am so custom</span>;
}
}
Rendering the component on the page:
ReactDOM
.
render
(
React
.
createElement
(
MyComponent
)
,
document
.
getElementById
(
'app'
)
);
If you use JSX, you don’t need to know how the component was defined (using a class or a function), in both cases using the component is the same:
ReactDOM
.
render
(
<
MyComponent
/>,
document
.
getElementById
(
'app'
)
);
You may be wondering: with all these options (JSX vs. pure JavaScript, a class component vs. a functional one), which one to use? JSX is the most common. And, unless you dislike the XML syntax in your JavaScript, the path of least resistance and of less typing is to go with JSX. This book uses JSX from now on, unless to illustrate a concept. Why then even talk about a no-JSX way? Well, you should know that there is another way and also that JSX is not some special voodoo but rather a thin syntax layer that transforms XML into plain JavaScript function calls such as React.createElement()
before sending the code to the browser.
What about class vs functional components? This is a question of preference. If you’re comfortable with object-oriented programming (OOP) and you like how classes are laid out, then by all means, go for it. Functional components are a little less typing usually, they feel more native to JavaScript (classes in JavaScript were an afterthought and merely syntax sugar) and a little lighter on the computer’s CPU. Historically, functional components were not able to accomplish everything that classes could. Until the invention of hooks, which we’ll get to in due time. This book teaches you both ways and doesn’t decide for you. OK, maybe there’s a slight preference towards functional components.
Rendering hard-coded UI in your custom components is perfectly fine and has its uses. But the components can also take properties and render or behave differently, depending on the values of the properties. Think about the <a>
element in HTML and how it acts differently based on the value of the href
attribute. The idea of properties in React is similar (and so is the JSX syntax).
In class components all properties are available via the this.props
object. Let’s see an example:
class
MyComponent
extends
React
.
Component
{
render
()
{
return
<
span
>
My
name
is
<
em
>
{
this
.
props
.
name
}
</
em
></
span
>;
}
}
As demonstrated in this example, you can open curly braces and sprinkle JavaScript values (and expressions too) within your JSX. You’ll learn more about this behavior as you progress with the book.
Passing a value for the name
property when rendering the component looks like this:
ReactDOM
.
render
(
<
MyComponent
name
=
"Bob"
/>,
document
.
getElementById
(
'app'
)
);
The result is shown in Figure 2-2.
It’s important to remember that this.props
is read-only. It’s meant to carry on configuration from parent components to children, it’s not a general-purpose storage of values. If you feel tempted to set a property of this.props
, just use additional local variables or properties of your component’s class instead (meaning use this.thing
as opposed to this.props.thing
).
In functional components, there’s no this
(in JavaScript’s strict mode) or it refers to the global object (in non-strict, dare we say sloppy, mode). So instead of this.props
, you get a props
object passed to your function as the first argument.
const
MyComponent
=
function
(
props
)
{
return
<
span
>
My
name
is
<
em
>
{
props
.
name
}
</
em
></
span
>;
};
A common pattern is to use JavaScript’s destructuring assignment and assign the property values to local variables. In other words the example above becomes:
// 02.07.props.destructuring.html
const
MyComponent
=
function
({
name
})
{
return
<
span
>
My
name
is
<
em
>
{
name
}
</
em
></
span
>;
};
You can have as many properties as you want. If, for example, you need two properties name
and job
you can use them like:
// 02.08.props.destruct.multi.html
const
MyComponent
=
function
({
name
,
job
})
{
return
<
span
>
My
name
is
<
em
>
{
name
}
</
em
>,
the
{
job
}
</
span
>;
};
ReactDOM
.
render
(
<
MyComponent
name
=
"Bob"
job
=
"engineer"
/>,
document
.
getElementById
(
'app'
)
);
Your component may offer a number of properties, but sometimes a few of the properties may have default values that work well for the most common cases. You can specify default property values using defaultProps
property for both functional and class components.
Functional:
const
MyComponent
=
function
({
name
,
job
})
{
return
<
span
>
My
name
is
<
em
>
{
name
}
</
em
>,
the
{
job
}
</
span
>;
};
MyComponent
.
defaultProps
=
{
job
:
'engineer'
,
};
ReactDOM
.
render
(
<
MyComponent
name
=
"Bob"
/>,
document
.
getElementById
(
'app'
)
);
Class components:
class
MyComponent
extends
React
.
Component
{
render
()
{
return
(
<
span
>
My
name
is
<
em
>
{
this
.
props
.
name
}
</
em
>,
the
{
this
.
props
.
job
}
</
span
>
);
}
}
MyComponent
.
defaultProps
=
{
job
:
'engineer'
,
};
ReactDOM
.
render
(
<
MyComponent
name
=
"Bob"
/>,
document
.
getElementById
(
'app'
)
);
In both cases, the result is the output: “My name is Bob, the engineer”
Notice how the render()
method’s return
statement wraps the returned value in parentheses. This is just because of JavaScript’s automatic semi-colon insertion (ASI) mechanism. A return
statement followed by a new line is the same as return;
which is the same as return undefined;
which is the definitely not what you want. Wrapping the returned expression in parentheses allows for better code formatting while retaining the correctness.
The examples so far were pretty static (or “stateless”). The goal was just to give you an idea of the building blocks of composing your UI. But where React really shines (and where old-school browser DOM manipulation and maintenance gets complicated) is when the data in your application changes. React has the concept of state, which is any data that components want to use to render themselves. When state changes, React rebuilds the UI without you having to do anything. After you build your UI initially in your render()
method (or in the rendering function in case of a functional component) all you care about is updating the data. You don’t need to worry about UI changes at all. After all, your render method/function has already provided the blueprint of what the component should look like.
“Stateless” is not a bad word, not at all. Stateless components are much easier to manage and think about. In fact, whenever you can, prefer to go stateless. But applications are complicated and you do need state. So let’s proceed.
Similarly to how you access properties via this.props
, you read the state via the object this.state
. To update the state, you use this.setState()
. When this.setState()
is called, React calls the render method of your component (and all of its children) and updates the UI.
The updates to the UI after calling this.setState()
are done using a queuing mechanism that efficiently batches changes. Updating this.state
directly can have unexpected behavior and you shouldn’t do it. Just like with this.props
, consider the this.state
object read-only, not only because it’s semantically a bad idea, but because it can act in ways you don’t expect. Similarly, don’t ever call this.render()
yourself—instead, leave it to React to batch changes, figure out the least amount of work, and call render()
when and if appropriate.
Let’s build a new component—a textarea that keeps count of the number of characters typed in (Figure 2-3).
You (as well as other future consumers of this amazingly reusable component) can use the new component like so:
ReactDOM
.
render
(
<
TextAreaCounter
text
=
"Bob"
/>,
document
.
getElementById
(
'app'
)
);
Now, let’s implement the component. Start first by creating a “stateless” version that doesn’t handle updates; this is not too different from all the previous examples:
class
TextAreaCounter
extends
React
.
Component
{
render
()
{
const
text
=
this
.
props
.
text
;
return
(
<
div
>
<
textarea
defaultValue
=
{
text
}
/>
<
h3
>
{
text
.
length
}
</
h3
>
</
div
>
);
}
}
TextAreaCounter
.
defaultProps
=
{
text
:
'Count me as I type'
,
};
You may have noticed that the <textarea>
in the preceding snippet takes a defaultValue
property, as opposed to a text child node, as you’re accustomed to in regular HTML. This is because there are some slight differences between React and old-school HTML when it comes to form elements. These are discussed further in the book, but rest assured, there are not too many of them. Additionally , you’ll find that these differences make sense and make your life as a developer easier.
As you can see, the TextAreaCounter
component takes an optional text
string property and renders a textarea with the given value, as well as an <h3>
element that displays the string’s length
. If the text
property is not supplied, the default “Count me as I type” value is used.
The next step is to turn this stateless component into a stateful one. In other words, let’s have the component maintain some data (state) and use this data to render itself initially and later on update itself (re-render) when data changes.
First, you need to set the initial state in the class constructor using this.state
. Bear in mind that the constructor is the only place where it’s ok to set the state directly without calling this.setState()
.
Initializing this.state
is required, if you don’t do it, consecutive access to this.state
in the render()
method will fail.
In this case it’s not necessary to initialize this.state.text
with a value as you can fallback to the property this.prop.text
(try 02.12.this.state.html
in the book’s repo):
class
TextAreaCounter
extends
React
.
Component
{
constructor
()
{
super
();
this
.
state
=
{};
}
render
()
{
const
text
=
'text'
in
this
.
state
?
this
.
state
.
text
:
this
.
props
.
text
;
return
(
<
div
>
<
textarea
defaultValue
=
{
text
}
/>
<
h3
>
{
text
.
length
}
</
h3
>
</
div
>
);
}
}
Calling super()
in the constructor is required before you can use this
.
The data this component maintains is the contents of the textarea, so the state has only one property called text
, which is accessible via this.state.text
. Next you need a way to update the state. You can use a helper method for this purpose:
onTextChange
(
event
)
{
this
.
setState
({
text
:
event
.
target
.
value
,
});
}
You always update the state with this.setState()
, which takes an object and merges it with the already existing data in this.state
. As you might guess, onTextChange()
is an event handler that takes an event
object and reaches into it to get the contents of the textarea input.
The last thing left to do is update the render()
method to set up the event handler:
render
()
{
const
text
=
'text'
in
this
.
state
?
this
.
state
.
text
:
this
.
props
.
text
;
return
(
<
div
>
<
textarea
value
=
{
text
}
onChange
=
{
event
=>
this
.
onTextChange
(
event
)}
/>
<
h3
>
{
text
.
length
}
</
h3
>
</
div
>
);
}
Now whenever the user types into the textarea, the value of the counter updates to reflect the contents (Figure 2-4).
Note that <teaxarea defaultValue...>
in now <textarea value...>
. This is because of the way inputs work in HTML where their state is maintained by the browser. But React can do better. In this example implementing onChange
means that the textarea is now controlled by React. More on controlled components is coming further in the book.
To avoid any confusion, a few clarifications are in order regarding the line:
onChange
=
{
event
=>
this
.
onTextChange
(
event
)}
React uses its own synthetic events system for performance, as well as convenience and sanity reasons. To help understand why, you need to consider how things are done in the pure DOM world.
It’s very convenient to use inline event handlers to do things like this:
<button
onclick=
"doStuff"
>
While convenient and easy to read (the event listener is right there with the UI code), it’s inefficient to have too many event listeners scattered like this. It’s also hard to have more than one listener on the same button, especially if said button is in somebody else’s “component” or library and you don’t want to go in there and “fix” or fork their code. That’s why in the DOM world it’s common to use element.addEventListener
to set up listeners (which now leads to having code in two places or more) and event delegation (to address the performance issues). Event delegation means you listen to events at some parent node, say a <div>
that contains many buttons, and you set up one listener for all the buttons, instead of one listener per button. Hence you delegate the event handling to a parent authority.
With event delegation you do something like:
<div
id=
"parent"
>
<button
id=
"ok"
>
OK</button>
<button
id=
"cancel"
>
Cancel</button>
</div>
<script>
document
.
getElementById
(
'parent'
).
addEventListener
(
'click'
,
function
(
event
)
{
const
button
=
event
.
target
;
// do different things based on which button was clicked
switch
(
button
.
id
)
{
case
'ok'
:
console
.
log
(
'OK!'
);
break
;
case
'cancel'
:
console
.
log
(
'Cancel'
);
break
;
default
:
new
Error
(
'Unexpected button ID'
);
};
});
</script>
This works and performs fine, but there are drawbacks:
Declaring the listener is further away from the UI component, which makes code harder to find and debug
Using delegation and always switch
-ing creates unnecessary boilerplate code even before you get to do the actual work (responding to a button click in this case)
Browser inconsistencies (omitted here) actually require this code to be longer
Unfortunately, when it comes to taking this code live in front of real users, you need a few more additions if you want to support old browsers:
You need attachEvent
in addition to addEventListener
You need const event = event || window.event;
at the top of the listener
You need const button = event.target || event.srcElement;
All of these are necessary and annoying enough that you end up using an event library of some sort. But why add another library (and study more APIs) when React comes bundled with a solution to the event handling nightmares?
React uses synthetic events to wrap and normalize the browser events, which means no more browser inconsistencies. You can always rely on the fact that event.target
is available to you in all browsers. That’s why in the TextAreaCounter
snippet you only need event.target.value
and it just works. It also means the API to cancel events is the same in all browsers; in other words, event.stopPropagation()
and event.preventDefault()
work even in old versions of Internet Explorer.
The syntax makes it easy to keep the UI and the event listeners together. It looks like old-school inline event handlers, but behind the scenes it’s not. Actually, React uses event delegation for performance reasons.
React uses camelCase syntax for the event handlers, so you use onClick
instead of onclick
.
If you need the original browser event for whatever reason, it’s available to you as event.nativeEvent
, but it’s unlikely that you’ll ever need to go there.
And one more thing: the onChange
event (as used in the textarea example) behaves as you’d expect: it fires when the user types, as opposed to after they’ve finished typing and have navigated away from the field, which is the behavior in plain DOM.
The example above used an arrow function to call the helper onTextChange
event:
onChange
=
{
event
=>
this
.
onTextChange
(
event
)}
This is because the shorter onChange={this.onTextChange}
wouldn’t have worked.
Another option is to bind the method, like so:
onChange
=
{
this
.
onTextChange
.
bind
(
this
)}
And yet another option, and a common pattern, is to bind all the event handling methods in the constructor:
constructor
()
{
super
();
this
.
state
=
{};
this
.
onTextChange
=
this
.
onTextChange
.
bind
(
this
);
}
// ....
<
textarea
value
=
{
text
}
onChange
=
{
this
.
onTextChange
}
/>
It’s a bit of necessary boilerplate, but this way the event handler is bound only once, as opposed to every time the render()
method is called, which helps reduce the memory footprint of your app.
Now you know that you have access to this.props
and this.state
when it comes to displaying your component in your render()
method. You may be wondering when you should use versus the other.
Properties are a mechanism for the outside world (users of the component) to configure your component. State is your internal data maintenance. So if you consider an analogy with object-oriented programming, this.props
is like a collection of all the arguments passed to a class constructor, while this.state
is a bag of your private properties.
In general, prefer to split your application in a way that you have fewer stateful components and more stateless ones.
In the textarea example above it was tempting to use this.props
inside of the constructor to set this.state
:
this
.
state
=
{
text
:
this
.
props
.
text
,
};
This is considered an anti-pattern. Ideally, you use any combination of this.state
and this.props
as you see fit to build your UI in your render()
method. But sometimes you want to take a value passed to your component and use it to construct the initial state. There’s nothing wrong with this, except that the callers of your component may expect the property (text
in the preceding example) to always have the latest value and the code above would violate this expectation. To set expectation straight, a simple naming change is sufficient—for example, calling the property something like defaultText
or initialValue
instead of just text
:
Chapter 4 illustrates how React solves this for its implementation of inputs and textareas where people may have expectations coming from their prior HTML knowledge.
You don’t always have the luxury of starting a brand-new React app from scratch. Sometimes you need to hook into an existing application or a website and migrate to React one piece at a time. Luckily, React was designed to work with any pre-existing codebase you might have. After all, the original creators of React couldn’t stop the world and rewrite an entire huge application (Facebook.com) completely from scratch, especially in the early days when React was young.
One way to have your React app communicate with the outside world is to get a reference to a component you render with ReactDOM.render()
and use it from outside of the component:
const
myTextAreaCounter
=
ReactDOM
.
render
(
<
TextAreaCounter
text
=
"Bob"
/>,
document
.
getElementById
(
'app'
)
);
Now you can use myTextAreaCounter
to access the same methods and properties you normally access with this
when inside the component. You can even play with the component using your JavaScript console (Figure 2-5).
In this example, myTextAreaCounter.state
checks the current state (empty initially), myTextAreaCounter.props
checks the properties and this line sets a new state:
myTextAreaCounter
.
setState
({
text
:
"Hello outside world!"
});
This line gets a reference to the main parent DOM node that React created:
const
reactAppNode
=
ReactDOM
.
findDOMNode
(
myTextAreaCounter
);
This is the first child of the <div id="app">
, which is where you told React to do its magic.
You have access to the entire component API from outside of your component. But you should use your new superpowers sparingly, if at all. It may be tempting to fiddle with the state of components you don’t own and “fix” them, but you’d be violating expectations and cause bugs down the road because the component doesn’t anticipate such intrusions.
React offers several so-called lifecycle methods. You can use the lifecycle methods to listen to changes in your component as far as the DOM manipulation is concerned. The life of a component goes through three steps:
Mounting - the component is added to the DOM initially
Updating - the component is updated as a result of calling setState()
Unmounting - the component is removed from the DOM
React does part of its work before updating the DOM, this is also called rendering phase. Then it updates the DOM and this phase is called a commit phase. With this background let’s consider some lifecycle methods:
After the initial mounting and after the commit to the DOM, the method componentDidMount()
of your component is called, if it exists. This is the place to do any initialization work that requires the DOM. Any initialization work that does not require the DOM should be in the constructor. And most of your initialization shouldn’t require the DOM. But in this method you can, for example, measure the height of the component that was just rendered, add any event listeners (e.g. addEventListener('resize')
), or fetch data from the server.
Right before the component is removed from the DOM, the method componentWillUnmount()
is called. This is the place to do any cleanup work you may need. Any event handlers, or anything else that may leak memory, should be cleaned up here. After this, the component is gone forever.
Before the component is updated, e.g. as a result of setState()
, you can use getSnapshotBeforeUpdate()
. This method receives the previous properties and state as arguments. And it can return a “snapshot” value, which is any value you want to pass over to the next lifecycle method, which is…
componentDidUpdate(previousProps, previousState, snapshot)
. This is called whenever the component was updated. Since at this point this.props
and this.state
have updated values, you get a copy of the previous ones. You can use this information to compare the old and the new state and potentially make more network requests if necessary.
And then there’s shouldComponentUpdate(newProps, newState)
which is an opportunity for an optimization. You’re given the state-to-be which you can compare with the current state and decide not to update the component, so its render()
method is not called.
Of these, componentDidMount()
and componentDidUpdate()
are the most common ones.
To better understand the life of a component, let’s add some logging in the TextAreaCounter
component. Simply implement all of the lifecycle methods to log to the console when they are invoked, together with any arguments:
componentDidMount
()
{
console
.
log
(
'componentDidMount'
);
}
componentWillUnmount
()
{
console
.
log
(
'componentWillUnmount'
);
}
componentDidUpdate
(
prevProps
,
prevState
,
snapshot
)
{
console
.
log
(
'componentDidUpdate '
,
prevProps
,
prevState
,
snapshot
);
}
getSnapshotBeforeUpdate
(
prevProps
,
prevState
)
{
console
.
log
(
'getSnapshotBeforeUpdate'
,
prevProps
,
prevState
);
return
'hello'
;
}
shouldComponentUpdate
(
newProps
,
newState
)
{
console
.
log
(
'shouldComponentUpdate '
,
newProps
,
newState
);
return
true
;
}
After loading the page, the only message in the console is “componentDidMount”.
Next, what happens when you type “b” to make the text “Bobb”? (See Figure 2-6.) shouldComponentUpdate()
is called with the new props (same as the old) and the new state. Since this method returns true
, React proceeds with calling getSnapshotBeforeUpdate()
passing the old props and state. This is your chance to do something with them and with the old DOM and pass any resulting information as a snapshot to the next method. For example this is an opportunity to do some element measurements or a scroll position and snapshot them to see if they change after the update. Finally, componentDidUpdate()
is called with the old info (you have the new one in this.state
and this.props
) and any snapshot defined by the previous method.
Let’s update the textarea one more time, this time typing “y”. The result is shown on Figure 2-7.
Finally, to demonstrate componentWillUnmount()
in action (using the example 02.14.lifecycle.html
from this book’s GitHub repo) you can type in the console:
ReactDOM.render(React.createElement('p', null, 'Enough counting!'), app);
This replaces the whole textarea component with a new <p>
component. Then you can see the log message “componentWillUnmount” in the console (Figure 2-8).
Say you want to restrict the number of characters to be typed in the textarea. You should do this in the event handler onTextChange()
, which is called as the user types. But what if someone (a younger, more naive you?) calls setState()
from the outside of the component? (Which, as mentioned earlier, is a bad idea.) Can you still protect the consistency and well-being of your component? Sure. You can do the validation in componentDidUpdate()
and if the number of characters is greater than allowed, revert the state back to what it was. Something like:
componentDidUpdate
(
prevProps
,
prevState
)
{
if
(
this
.
state
.
text
.
length
>
3
)
{
this
.
setState
({
text
:
prevState
.
text
||
this
.
props
.
text
,
});
}
}
The condition prevState.text || this.props.text
is in place for the very first update when there’s no previous state.
This may seem overly paranoid, but it’s still possible to do. Another way to accomplish the same protection is by leveraging shouldComponentUpdate()
:
shouldComponentUpdate
(
_
,
newState
)
{
return
newState
.
text
.
length
>
3
?
false
:
true
;
}
See 02.15.paranoid.html
in the book’s repo to play with these concepts.
You know you can mix and nest React components as you see fit. So far you’ve only seen ReactDOM
components (as opposed to custom ones) in the render()
methods. Let’s take a look at another simple custom component to be used as a child.
Let’s isolate the counter part into its own component. After all, divide and conquer is what it’s all about!
First, let’s isolate the lifestyle logging into a separate class and have the two components inherit it. Inheritance is almost never warranted when it comes to React because for UI work composition is preferable and for non-UI work a regular JavaScript module would do. But this is just for education and for demonstration that it is possible. And also to avoid copy-pasting the logging methods.
This is the parent:
class
LifecycleLoggerComponent
extends
React
.
Component
{
static
getName
()
{}
componentDidMount
()
{
console
.
log
(
this
.
constructor
.
getName
()
+
'::componentDidMount'
);
}
componentWillUnmount
()
{
console
.
log
(
this
.
constructor
.
getName
()
+
'::componentWillUnmount'
);
}
componentDidUpdate
(
prevProps
,
prevState
,
snapshot
)
{
console
.
log
(
this
.
constructor
.
getName
()
+
'::componentDidUpdate'
);
}
}
The new Counter
component simply shows the count. It doesn’t maintain state, but displays the count
property given by the parent.
class
Counter
extends
LifecycleLoggerComponent
{
static
getName
()
{
return
'Counter'
;
}
render
()
{
return
<
h3
>
{
this
.
props
.
count
}
</
h3
>;
}
}
Counter
.
defaultProps
=
{
count
:
0
,
};
The textarea component sets up a static getName()
method:
class
TextAreaCounter
extends
LifecycleLoggerComponent
{
static
getName
()
{
return
'TextAreaCounter'
;
}
// ....
}
And finally, the textarea’s render()
gets to use <Counter/>
and use it conditionally; if the count is 0, nothing is displayed.
render
()
{
const
text
=
'text'
in
this
.
state
?
this
.
state
.
text
:
this
.
props
.
text
;
return
(
<
div
>
<
textarea
value
=
{
text
}
onChange
=
{
this
.
onTextChange
}
/>
{
text
.
length
>
0
?
<
Counter
count
=
{
text
.
length
}
/>
:
null
}
</
div
>
);
}
Notice the conditional statement in JSX. You wrap the expression in {}
and conditionally render either <Counter/>
or nothing (null
). And just for demonstration: another option is to move the condition outside the return
. Assigning the result of a JSX expression to a variable is perfectly fine.
render
()
{
const
text
=
'text'
in
this
.
state
?
this
.
state
.
text
:
this
.
props
.
text
;
let
counter
=
null
;
if
(
text
.
length
>
0
)
{
counter
=
<
Counter
count
=
{
text
.
length
}
/>;
}
return
(
<
div
>
<
textarea
value
=
{
text
}
onChange
=
{
this
.
onTextChange
}
/>
{
counter
}
</
div
>
);
}
Now you can observe the lifecycle methods being logged for both components. Open 02.16.child.html
from the book’s repo in your browser to see what happens when you load the page and then change the contents of the textarea.
During initial load, the child component is mounted and updated before the parent. You see in the console log:
Counter::componentDidMount TextAreaCounter::componentDidMount
After deleting two characters you see how the child is updated, then the parent:
Counter::componentDidUpdate TextAreaCounter::componentDidUpdate Counter::componentDidUpdate TextAreaCounter::componentDidUpdate
After deleting the last character, the child component is completely removed from the DOM:
Counter::componentWillUnmount TextAreaCounter::componentDidUpdate
Finally, typing a character brings back the counter component to the DOM:
Counter::componentDidMount TextAreaCounter::componentDidUpdate
You already know about shouldComponentUpdate()
and saw it in action. It’s especially important when building performance-critical parts of your app. It’s invoked before componentWillUpdate()
and gives you a chance to cancel the update if you decide it’s not necessary.
There is a class of components that only use this.props
and this.state
in their render()
methods and no additional function calls. These components are called “pure” components. They can implement shouldComponentUpdate()
and compare the state and the properties before and after an update and if there aren’t any changes, return false
and save some processing power. Additionally, there can be pure static components that use neither props
nor state
. These can straight out return false
.
React offers a way to make it easier to use the common (and generic) case of checking all props and state in shouldComponentUpdate()
. Instead of repeating this work you can have your components inherit React.PureComponent
instead of React.Component
. This way you don’t need to implement shouldComponentUpdate()
, it’s done for you. Let’s take advantage and tweak the previous example.
Since both components inherit the logger, all you need is:
class
LifecycleLoggerComponent
extends
React
.
PureComponent
{
// ... no other changes
}
Now both components are pure. Let’s also add a log message in the render()
methods:
render
()
{
console
.
log
(
this
.
constructor
.
getName
()
+
'::render'
);
// ... no other changes
}
Now loading the page (02.17.pure.html
from the repo) prints out:
TextAreaCounter::render Counter::render Counter::componentDidMount TextAreaCounter::componentDidMount
Changing “Bob” to “Bobb” gives us the expected result of rendering and updating.
TextAreaCounter::render Counter::render Counter::componentDidUpdate TextAreaCounter::componentDidUpdate
Now if you paste the string “LOLz” replacing “Bobb” (or any string with 4 characters), you see:
TextAreaCounter::render TextAreaCounter::componentDidUpdate
As you see there’s no reason to re-render <Counter>
, because its props
have not changed. The new string has the same number of characters.
You may have noticed that functional components dropped out of this chapter by the time this.state
got involved. They come back later in the book, when you’ll also learn the concept of hooks. Since there’s no this
in functions, there needs to be another to accomplish the same state management of a component. The good news is that once you understand the concepts of state and props, the functional component differences are just syntax.
As much “fun” as it was to spend all this time on a textarea, let’s move on to something more interesting, before introducing any new concepts. In the next chapter, you’ll see where React’s benefits come into play - namely focusing on your data and have the UI updates take care of themselves.
3.82.58.213