Today we'll walk through how to add interactivity to our applications to make them engaging and dynamic.
Through this point, we've built our few handful of components without adding much user interaction. Today, we're going to change that.
User interaction
The browser is an event-driven application. Everything that a user does in the browser fires an event, from clicking buttons to even just moving the mouse. In plain JavaScript, we can listen for these events and attach a JavaScript function to interact with them.
For instance, we can attach a function to the mousemove
browser event with the JS:
const ele = document.getElementById('mousemove');
ele.innerHTML = 'Move your mouse over this text';
ele.addEventListener('mousemove', function(evt) {
const { screenX, screenY } = evt;
ele.innerHTML = '<div>Mouse is at: X: ' +
screenX + ', Y: ' + screenY +
'</div>';
})
This results in the following functionality:
In React, however we don't have to interact with the browser's event loop in raw JavaScript as React provides a way for us to handle events using props
.
For instance, to listen for the mousemove
event from the (rather unimpressive) demo above in React, we'll set the prop onMouseMove
(notice the camelcasing of the event name).
class MouseMover extends React.Component {
state = {
x: 0,
y: 0
};
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
});
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.state.x || this.state.y
? "The mouse is at x: " + this.state.x + ", y: " + this.state.y
: "Move the mouse over this box"}
</div>
);
}
}
React provides a lot of props
we can set to listen for different browser events, such as click, touch, drag, scroll, selection events, and many more (see the events documentation for a list of all of them).
To see some of these in action, the following is a small demo of some of the props
we can pass on our elements. Each text element in the list set the prop it lists. Try playing around with the list and seeing how the events are called and handled within the element (all events are set on the text, not the list item):
We'll be using the onClick
prop quite a bit all throughout our apps quite a bit, so it's a good idea to be familiar with it. In our activity list header, we have a search icon that we haven't hooked up yet to show a search box.
The interaction we want is to show a search <input />
when our users click on the search icon. Recall that our Header
component is implemented like this:
class Header extends React.Component {
render() {
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">
{this.props.title}
</span>
<input
type="text"
className="searchInput"
placeholder="Search ..." />
<div className="fa fa-search searchIcon"></div>
</div>
)
}
}
Let's update it a bit so that we can pass dynamic className
prop to the <input />
element
class Header extends React.Component {
render() {
// Classes to add to the <input /> element
let searchInputClasses = ["searchInput"];
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">
{this.props.title}
</span>
<input
type="text"
className={searchInputClasses.join(' ')}
placeholder="Search ..." />
<div className="fa fa-search searchIcon"></div>
</div>
)
}
}
When the user clicks on the <div className="fa fa-search searchIcon"></div>
element, we'll want to run a function to update the state of the component so the searchInputClasses
object gets updated. Using the onClick
handler, this is pretty simple.
Let's make this component stateful (it needs to track if the search field should be showing or not). We can convert our component to be stateful using the constructor()
function:
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
searchVisible: false
}
}
// ...
}
What is a
constructor
function?In JavaScript, the
constructor
function is a function that runs when an object is created. It returns a reference to the Object function that created the instance'sprototype
.In plain English, a constructor function is the function that runs when the JavaScript runtime creates a new object. We'll use the constructor method to set up instance variables on the object as it runs right when the object is created.
When using the
ES6
class syntax to create an object, we have to call thesuper()
method before any other method. Calling thesuper()
function calls the parent class'sconstructor()
function. We'll call it with the same arguments as theconstructor()
function of our class is called with.
When the user clicks on the button, we'll want to update the state to say that the searchVisible
flag gets updated. Since we'll want the user to be able to close/hide the <input />
field after clicking on the search icon for a second time, we'll toggle the state rather than just set it to true.
Let's create this method to bind our click event:
class Header extends React.Component {
// ...
showSearch() {
this.setState({
searchVisible: !this.state.searchVisible
})
}
// ...
}
Let's add an if statement to update searchInputClasses
if this.state.searchVisible
is true
class Header extends React.Component {
// ...
render() {
// ...
// Update the class array if the state is visible
if (this.state.searchVisible) {
searchInputClasses.push("active");
}
// ...
}
}
Finally, we can attach a click handler (using the onClick
prop) on the icon element to call our new showSearch()
method. The entire updated source for our Header
component looks like this:
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
searchVisible: false
}
}
// toggle visibility when run on the state
showSearch() {
this.setState({
searchVisible: !this.state.searchVisible
})
}
render() {
// Classes to add to the <input /> element
let searchInputClasses = ["searchInput"];
// Update the class array if the state is visible
if (this.state.searchVisible) {
searchInputClasses.push("active");
}
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">
{this.props.title}
</span>
<input
type="text"
className={searchInputClasses.join(' ')}
placeholder="Search ..." />
{/* Adding an onClick handler to call the showSearch button */}
<div
onClick={this.showSearch.bind(this)}
className="fa fa-search searchIcon"></div>
</div>
)
}
}
Try clicking on the search icon and watch the input field appear and disappear (the animation effect is handled by CSS animations).
Input events
Whenever we build a form in React, we'll use the input events offered by React. Most notably, we'll use the onSubmit()
and onChange()
props most often.
Let's update our search box demo to capture the text inside the search field when it updates. Whenever an <input />
field has the onChange()
prop set, it will call the function every time the field changes. When we click on it and start typing, the function will be called.
Using this prop, we can capture the value of the field in our state.
Rather than updating our <Header />
component, let's create a new child component to contain a <form />
element. By moving the form-handling responsibilities to it's own form, we can simplify the <Header />
code and we can call up to the parent of the header when our user submits the form (this is a usual React pattern).
Let's create a new component we'll call SearchForm
. This new component is a stateful component as we'll need to hold on to the value of the search input (track it as it changes):
class SearchForm extends React.Component {
// ...
constructor(props) {
super(props);
this.state = {
searchText: ''
}
}
// ...
}
Now, we already have the HTML for the form written in the <Header />
component, so let's grab that from our Header
component and return it from our SearchForm.render()
function:
class SearchForm extends React.Component {
// ...
render() {
const { searchVisible } = this.props;
let searchClasses = ["searchInput"];
if (searchVisible) {
searchClasses.push("active");
}
return (
<form>
<input
type="search"
className={searchClasses.join(" ")}
placeholder="Search ..."
/>
</form>
);
}
}
Now that we've moved some code from the Header
component to the SearchForm
, let's update its render
method to incorporate the SearchForm
class Header extends React.Component {
// ...
render() {
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">{this.props.title}</span>
<SearchForm />
{/* Adding an onClick handler to call the showSearch button */}
<div
onClick={this.showSearch.bind(this)}
className="fa fa-search searchIcon"
></div>
</div>
);
}
}
Notice that we lost the styles on our <input />
field. Since we no longer have the searchVisible
state in our new component, we can't use it to style the <input />
any longer. However, we can pass a prop from our Header
component that tells the SearchForm
to render the input as visible.
Let's define the searchVisible
prop (using PropTypes
, of course) and update the render
function to use the new prop value to show (or hide) the search <input />
. We'll also set a default value for the visibility of the field to be false (since our Header
shows/hides it nicely):
class SearchForm extends React.Component {
// ...
}
SearchForm.propTypes = {
searchVisible: PropTypes.bool
}
SearchForm.defaultProps = {
searchVisible: false
};
In case you forgot to include
PropTypes
package in your page just add the followingscript
tag in your page<script src="https://unpkg.com/[email protected]/prop-types.min.js"></script>
Finally, let's pass the searchVisible
state value from Header
as a prop to SearchForm
class Header extends React.Component {
render() {
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">{this.props.title}</span>
<SearchForm searchVisible={this.state.searchVisible} />
{/* Adding an onClick handler to call the showSearch button */}
<div
onClick={this.showSearch.bind(this)}
className="fa fa-search searchIcon"
></div>
</div>
);
}
}
Now we have our styles back on the <input />
element, let's add the functionality for when the user types in the search box, we'll want to capture the value of the search field. We can achieve this workflow by attaching the onChange
prop to the <input />
element and passing it a function to call every time the <input />
element is changed.
class SearchForm extends React.Component {
// ...
updateSearchInput(e) {
const val = e.target.value;
this.setState({
searchText: val
});
}
// ...
render() {
const { searchVisible } = this.state;
let searchClasses = ['searchInput']
if (searchVisible) {
searchClasses.push('active')
}
return (
<form>
<input
type="search"
className={searchClasses.join(" ")}
onChange={this.updateSearchInput.bind(this)}
placeholder="Search ..."
/>
</form>
);
}
}
When we type in the field, the updateSearchInput()
function will be called. We'll keep track of the value of the form by updating the state. In the updateSearchInput()
function, we can call directly to this.setState()
to update the state of the component.
The value is held on the
event
object's target asevent.target.value
.
class SearchForm extends React.Component {
// ...
updateSearchInput(e) {
const val = e.target.value;
this.setState({
searchText: val
});
}
// ...
}
Controlled vs. uncontrolled
We're creating what's known as an uncontrolled component as we're not setting the value of the
<input />
element. We can't provide any validation or post-processing on the input text value as it stands right now.If we want to validate the field or manipulate the value of the
<input />
component, we'll have to create what is called a controlled component, which really just means that we pass it a value using thevalue
prop. A controlled component version'srender()
function would look like:class SearchForm extends React.Component { render() { return ( <input type="search" value={this.state.searchText} className={searchInputClasses} onChange={this.updateSearchInput.bind(this)} placeholder="Search ..." /> ); } }
As of now, we have no way to actually submit the form, so our user's can't really search. Let's change this. We can capture the form submission by using the onSubmit
prop on the <form />
element.
Let's update the render()
function to reflect this change.
class SearchForm extends React.Component {
// ...
submitForm(event) {
event.preventDefault();
}
// ...
render() {
const { searchVisible } = this.props;
let searchClasses = ['searchInput']
if (searchVisible) {
searchClasses.push('active')
}
return (
<form onSubmit={this.submitForm.bind(this)}>
<input
type="search"
className={searchClasses.join(' ')}
onChange={this.updateSearchInput.bind(this)}
placeholder="Search ..." />
</form>
);
}
}
We immediately call
event.preventDefault()
on thesubmitForm()
function. This stops the browser from bubbling the event up which would causes the default behavior of the entire page to reload (the default function when a browser submits a form).
Now when we type into the <input />
field and press enter, the submitForm()
function gets called with the event object.
So... great, we can submit the form and stuff, but when do we actually do the searching? For demonstration purposes right now, we'll pass the search text up the parent-child component chain so the Header
can decide what to search.
The
SearchForm
component certainly doesn't know what it's searching, so we'll have to pass the responsibility up the chain. We'll use this callback strategy quite a bit.
In order to pass the search functionality up the chain, our SearchForm
will need to accept a prop function to call when the form is submitted. Let's define a prop we'll call onSubmit
that we can pass to our SearchForm
component. Being good developers, we'll also add a default prop
value and a propType
for this onSubmit
function. Since we'll want to make sure the onSubmit()
is defined, we'll set the onSubmit
prop to be a required prop:
class SearchForm extends React.Component {
// ...
}
SearchForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
searchVisible: PropTypes.bool
}
SearchForm.defaultProps = {
onSubmit: () => {},
searchVisible: false
}
When the form is submitted, we can call this function directly from the props
. Since we're keeping track of the search text in our state, we can call the function with the searchText
value in the state so the onSubmit()
function only gets the value and doesn't need to deal with an event.
class SearchForm extends React.Component {
// ...
submitForm(event) {
// prevent the form from reloading the entire page
event.preventDefault();
// call the callback with the search value
this.props.onSubmit(this.state.searchText);
}
}
Now, when the user presses enter we can call this onSubmit()
function passed in the props
by our Header
component.
Let's add the onSubmit
prop to the SearchForm
in the Header
component:
class Header extends React.Component {
// ...
render() {
return (
<div className="header">
<div className="menuIcon">
<div className="dashTop"></div>
<div className="dashBottom"></div>
<div className="circle"></div>
</div>
<span className="title">{this.props.title}</span>
<SearchForm searchVisible={this.state.searchVisible} onSubmit={this.props.onSearch}/>
{/* Adding an onClick handler to call the showSearch button */}
<div
onClick={this.showSearch.bind(this)}
className="fa fa-search searchIcon"
></div>
</div>
);
}
}
Now we have a search form component we can use and reuse across our app. Of course, we're not actually searching anything yet. Let's fix that and implement search.
Implementing search
To implement search in our component, we'll want to pass up the search responsibility one more level from our Header
component to a container component we'll call Panel
.
First things first, let's implement the same pattern of passing a callback to a parent component from within a child component from the Panel
to the Header
component.
On the Header
component, let's update the propTypes
for a prop we'll define as a prop called onSearch
:
class Header extends React.Component {
// ...
}
Header.propTypes = {
onSearch: PropTypes.func
}
Here's our Panel
component:
class Content extends React.Component {
constructor(props) {
super(props);
this.state = {
activities: data,
};
}
render() {
const { activities } = this.state; // ES6 destructuring
return (
<div>
<Header
title="Github activity" />
<div className="content">
<div className="line" />
{/* Timeline item */}
{activities.map(activity => (
<ActivityItem key={activity.id} activity={activity} />
))}
</div>
</div>
);
}
}
In any case, our
Panel
component is essentially a copy of ourContent
component we previously built on day 7. Make sure to include theActivityItem
component in your page. Also don't forget to includeMoment.js
in your file as it's used byActivityItem
to format dates. Add the followingscript
tag in your page<script src="https://unpkg.com/[email protected]/min/moment.min.js"></script>
Notice that our virtual tree looks like this:
<Panel> <Header> <SearchForm></SearchForm> </Header> </Panel>
When the
<SearchForm />
is updated, it will pass along it's awareness of the search input's change to it's parent, the<Header />
, when it will pass along upwards to the<Panel />
component. This method is very common in React apps and provides a good set of functional isolation for our components.
Back in our Panel
component, we'll pass a function to the Header
as the onSearch()
prop on the Header
. What we're saying here is that when the search form has been submitted, we want the search form to call back to the header component which will then call to the Panel
component to handle the search.
Since the Header
component doesn't control the content listing, the Panel
component does, we have to pass the responsibility one more level up, as we're defining here.
In order to actually handle the searching, we'll need to pass an onSearch()
function to our Header
component. Let's define an onSearch()
function in our Panel
component and pass it off to the Header
props in the render()
function:
class Panel extends React.Component {
constructor(props) {
super(props);
this.state = {
activities: data,
};
}
handleSearch(val) {
// handle search here
}
render() {
const { activities } = this.state; // ES6 destructuring
return (
<div>
<Header
title="Github activity"
onSearch={this.handleSearch.bind(this)}
/>
<div className="content">
<div className="line" />
{/* Timeline item */}
{activities.map(activity => (
<ActivityItem key={activity.id} activity={activity} />
))}
</div>
</div>
);
}
}
All we did here was add a handleSearch()
function and pass it to the header. Now when the user types in the search box, the handleSearch()
function on our Panel
component will be called.
Let's update our handleSearch
method to actually do the searching:
class Panel extends React.Component {
// ...
handleSearch(val) {
// resets the data if the search value is empty
if (val === "") {
this.setState({
activities: data
});
} else {
const { activities } = this.state;
const filtered = activities.filter(
a => a.actor && a.actor.login.match(val)
);
this.setState({
activities: filtered
});
}
}
// ...
}
All the activities.filter()
function does is run the function passed in for every element and it filters out the values that return falsy values, keeping the ones that return truthy ones. Our search function simply looks for a match on the Github activity's actor.login
(the Github user) to see if it regexp-matches the val
value.
With the handleSearch()
function updated, our search is complete.
Try searching for auser
.
Now we have a 3-layer app component that handles search from a nested child component. We jumped from beginner to intermediate with this post. Pat yourself on the back. This was some hefty material. Make sure you understand this because we'll use these concepts we covered today quite often.
In the next section, we'll jump out and look at building pure components.