Events
Subscribing (an Event Handler)
You can listen/subscribe/wire to an Event
by attaching a event handler. An event handler
is simply a function that will be called when the event is raised/fired/emitted.
Handling events with React elements is very similar to handling events on DOM elements. There are some syntactic differences:
- React
events
are named usingcamelCase
, rather than lowercase. - With JSX you pass a function as the event handler, rather than a string.
- Because it is a function and not a string you don't want to invoke the function when you listen as this would result in your event handler function being called immediately when the component renders instead of waiting for the event to trigger it.
In Vanilla JavaScript
Setup
- Delete or comment out all current code in
main.js
Steps
Add a button to
index.html
<div id="root">
+ <button onclick="handleClick()">Click Me!</button>
</div>Add an event handler function to
main.js
//main.js
function handleClick() {
console.log('clicked');
}Refresh the page in your browser
Open
Chrome DevTools
to theConsole
tabClick the button on the page
You should see
clicked
being logged to the console.Note: the Chrome browser will place a number next to the log message if it is logged multiple times. The number indicates the number of times the message has been logged.
In a React Function Component
Setup
- Delete the button in
index.html
. - Be sure to leave the root
div
on the page so React can render into it.
Steps
Create a function component that renders a button and wire up the event handler you created in the last section as shown below
function handleClick() {
console.log('clicked');
}
function Button() {
return <button onClick={handleClick()}>Click Me!</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<Button />);If not already opened from the previous step, open
Chrome DevTools
switch to theConsole
tabRefresh the page in your browser
Notice that since the event handler is invoked with (), the function is run immediately. We want the function to only run when the button is clicked.
Remove the function invocation
()
so the function will no longer run immediately.function Button() {
return <button onClick={handleClick}>Click Me!</button>;
}Refresh the page in your browser
Notice the function is no longer run when the component renders
Click the button
You should see
clicked
being logged to the console again
Passing Parameters
Inside a loop (we'll learn about loops in the next section) it is common to want to pass a parameter to identify the item in the loop.
As we discussed earlier, we can't invoke a function when we subscribe to an event with parenthesis ()
because that would cause the function to be immediately called.
Comment or remove all code from
main.js
Paste the following code into
main.js
function FruitListItem(props) {
function handleClick(id) {
console.log(`removed ${id}`);
}
return <li onClick={handleClick(props.fruit.id)}>{props.fruit.name} </li>;
}
function FruitList(props) {
const fruitListItems = props.fruits.map((fruit) => (
<FruitListItem key={fruit.id} fruit={fruit} />
));
return <ul>{fruitListItems}</ul>;
}
const data = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'orange' },
{ id: 3, name: 'blueberry' },
{ id: 4, name: 'banana' },
{ id: 5, name: 'kiwi' },
];
ReactDOM.createRoot(document.getElementById('root')).render(
<FruitList fruits={data} />
);If not already opened from the previous step, open
Chrome DevTools
switch to theConsole
tabRefresh the page in your browser
In the
Console
notice that all items invoked thehandleClick
function when the page was loaded.Click any item on the list and note that
handleClick
is not called so no logging occurs in theConsole
.So how can we pass arguments to an event handler or callback?
There are two solutions to this problem:
Using Arrow Functions
Wrap
handleClick
in an arrow functionfunction FruitListItem(props) {
function handleClick(id) {
console.log(`removed ${id}`);
}
return (
- <li onClick={handleClick(props.fruit.id)}>{props.fruit.name} </li>
+ <li onClick={() => handleClick(props.fruit.id)}>{props.fruit.name} </li>
);
}
function FruitList(props) {
const fruitListItems = props.fruits.map((fruit) => (
<FruitListItem key={fruit.id} fruit={fruit} />
));
return <ul>{fruitListItems}</ul>;
}
const data = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'orange' },
{ id: 3, name: 'blueberry' },
{ id: 4, name: 'banana' },
{ id: 5, name: 'kiwi' },
];
ReactDOM.createRoot(document.getElementById("root")).render(
<FruitList fruits={data} />
);
Reference
How do I pass a parameter to an event handler or callback?
Accessing Event Information
Handling events requires us to prevent the default browser behavior.
Using Arrow Functions
Comment or remove all code from
main.js
Paste the following code into
main.js
function FruitListItem(props) {
function handleClick(e, id) {
console.log(e);
console.log(`removed ${id}`);
}
return (
<li onClick={(e) => handleClick(e, props.fruit.id)}>
{props.fruit.name}{' '}
</li>
);
}
function FruitList(props) {
const fruitListItems = props.fruits.map((fruit) => (
<FruitListItem key={fruit.id} fruit={fruit} />
));
return <ul>{fruitListItems}</ul>;
}
const data = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'orange' },
{ id: 3, name: 'blueberry' },
{ id: 4, name: 'banana' },
{ id: 5, name: 'kiwi' },
];
ReactDOM.createRoot(document.getElementById('root')).render(
<FruitList fruits={data} />
);If not already opened from the previous step, open
Chrome DevTools
switch to theConsole
tabRefresh the page in your browser
Click an item on the list
In the
Console
you should see something similar to the following:SyntheticBaseEvent {_reactName: 'onClick', _targetInst: null, type: 'click', nativeEvent: PointerEvent, target: li, …} main.js:4
removed 2
Note that with bind any further arguments including the event object is automatically forwarded.
Note: returning false does not prevent the default browser behavior as it does in vanilla JavaScript as shown in this example.
Synthetic Events
The event object we are returned in the previous example is an imitation of but not actually the same as the Event
object in the browsers' Document Object Model (DOM)
.
A SyntheticEvent
is a cross-browser wrapper around the browser’s native event. It has the same interface as the browser’s native event, including stopPropagation()
and preventDefault()
, except the events work identically across all browsers.
Subscribing in a Class Component
Setup
- Comment out the function component that renders a button
- Comment out the event handler
- If not already opened from the previous step, open
Chrome DevTools
switch to theConsole
tab
Steps
Create a class component that renders a button
Create a class method to be your event handler
Bind the class method in the constructor
class Button extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('clicked');
}
render() {
return <button onClick={this.handleClick}>Click Me!</button>;
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<Button />);Bind? The next section discusses why binding is necessary as well as alternative syntaxes that can be used to bind the event handler including which are considering the best practice.
It is mandatory to call
super(props);
in the constructor. It setsthis.props
in your constructor in case you want to access them there. They would beundefined
when accessingthis.props
in your constructor otherwise.Refresh the page in your browser
Click the button
You should see
clicked
being logged to the console again.
Binding (the Event Handler)
The event handler is a function that needs to get bound to the class instance if you are using class components instead of function components.
Why Binding is Necessary?
Binding is necessary in JavaScript because the value of the function context this
inside a method depends on how the method is invoked.
In the example below, code snippet A and B are not equivalent.
- A) returns an instance of the class
- B) returns undefined
Note: Because we are using Babel to transpile our JavaScript code we are in strict mode ('use strict'). If we were not in strict mode B) would have returned
window
class Logger {
log() {
console.log(this);
}
}
// code snippet A
let logger = new Logger();
logger.log();
// Logger{}
// code snippet B
let logFn = logger.log;
logFn();
// undefined
This next section is about the different ways to do the binding.
Binding Examples
Example A: just creating a method on a class (DOESN'T WORK)
// class method: no binding
// logs undefined
class ExplainBindingsComponent extends React.Component {
handleClick() {
console.log(this);
}
render() {
return (
<button onClick={this.handleClick} type="button">
Click Me
</button>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(
<ExplainBindingsComponent />
);Example B: binding the method in the constructor (to an instance of the class: this) (WORKS but VERBOSE so AVOID)
// class method: bind in constructor
// logs instance of the component
class ExplainBindingsComponent extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this);
}
render() {
return (
<button onClick={this.handleClick} type="button">
Click Me
</button>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(
<ExplainBindingsComponent />
);Best Practice
Why?
- Binding only happens once when the component is instantiated
Example C: Assign an arrow function to a class field (WORKS: RECOMMENDED)
// arrow function: assigned to class field
// logs instance of the component
class ExplainBindingsComponent extends React.Component {
handleClick = () => {
console.log(this);
};
render() {
return (
<button onClick={this.handleClick} type="button">
Click Me
</button>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(
<ExplainBindingsComponent />
);Best Practice (emerging)
Why?
- The binding in the constructor is repetitive and can be forgotten
- Although arrow functions are commonly used in ES6/ES2015, this technique also uses a class field declaration which is a Stage 3 Proposal and can require additional configuration to get setup.
- Class field declarations are setup/configured in a Create-React-App by default
- Facebook says they will support a
code-mod
(easy migration path, rewriting current syntax automatically)- Facebook has thousands of React components and does not want to do this manually
Example D: In a Function Component (WORKS: RECOMMENDED)
Binding to
this
is really about being able to access a member variable (loading, message, projects) or function (handleClick, loadProjects, setState).With function components there is no instance of a component and all member variables and functions can just be defined inside the function that creates the components so they are easily accessed without worrying about the value of
this
.function ExplainBindingsComponent() {
const memberValue = 'test';
function handleClick() {
console.log(memberValue);
}
return (
<button onClick={handleClick} type="button">
Click Me
</button>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(
<ExplainBindingsComponent />
);OR using an arrow function after you get comfortable with the syntax
function ExplainBindingsComponent() {
const memberValue = 'test';
const handleClick = () => {
console.log(memberValue);
};
return (
<button onClick={handleClick} type="button">
Click Me
</button>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(
<ExplainBindingsComponent />
);
Binding (the Event Handler): Other Approaches to Avoid
In the render method (to an instance of the class: this)
// class method: bind in render
// logs instance of the component
class ExplainBindingsComponent extends React.Component {
handleClick() {
console.log(this);
}
render() {
return (
<button onClick={this.handleClick.bind(this)} type="button">
Click Me
</button>
);
}
}
const element = <ExplainBindingsComponent />;- Avoid
- Why?
- Binds the class method every time the render method runs, meaning every time the component updates.
- Eventually impact application performance (how quickly it renders).
By defining it in the constructor
// class method: define in constructor w/ arrow function
// logs instance of the component
class ExplainBindingsComponent extends React.Component {
constructor() {
super();
this.handleClick = () => {
console.log(this);
};
}
render() {
return (
<button onClick={this.handleClick} type="button">
Click Me
</button>
);
}
}
const element = <ExplainBindingsComponent />;- Avoid
- Why?
- Clutters constructor with business logic
- Constructor only there to instantiate your class and initialize properties