1. Learn
  2. Learn React By Itself

Everything About Events

Familiarize yourself with React Events through exercises. Start with the basics before learning about custom events and components.

James K Nelson

James is the editor of React Armory, and has been creating things with JavaScript for over 15 years.

Read more by JamesFollow @james_k_nelson on Twitter

By now, you’ve got the hang of React. You’ve even animated a fractal tree! There is just one problem: you still haven’t built anything that users can interact with. And while animations are nice, the thing that makes React amazing is that it helps you build things that are useful.

So let’s move on to the one thing that all useful apps have in common: buttons!

Controls that don’t work

There’s no point trying to run before you learn to walk, so let’s start by adding some controls that don’t work.

But how do you add controls? The same way as you do with HTML! Just like React lets you create div and span elements, you can also create button, input, and any other type of HTML element.

To start you off, I’ve added some buttons and a disabled input. Your task is to add a “Start/stop animation” button above them, just like the one from back in part one.

As always, you can check the solution if you get stuck, but you may not check the solution until you’ve tried it yourself. Otherwise I’ll get angry.

// The `AnimatedPythagorasTree` component renders its children on the bottom
// left of the tree. It sways when the `animated` prop is `true`.
import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'

let animating = false
let sway = 0.1

function renderApp() {
  ReactDOM.render(
    React.createElement(AnimatedPythagorasTree, { animating, sway },
      React.createElement('h5', {}, 'amount of sway'),
      React.createElement('button', {}, '<'),
      React.createElement('input', {value: sway, disabled: true }),
      React.createElement('button', {}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()
import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'

let animating = false
let sway = 0.1

function renderApp() {
  const toggleText = `${animating ? 'Stop' : 'Start'} animation`

  ReactDOM.render(
    React.createElement(AnimatedPythagorasTree, { animating, sway },
      React.createElement('button', {}, toggleText),
      React.createElement('h5', {}, 'amount of sway'),
      React.createElement('button', {}, '<'),
      React.createElement('input', {value: sway, disabled: true }),
      React.createElement('button', {}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()

Now that you have some controls, the obvious next step is to make them work. And what better way to do so than with functions?

Callback functions

It goes without saying, but when the user interacts with a control, React won’t know what to do unless you tell it what to do. So when you want to handle an event (like a click or a key press), you’ll need to define a function that should be run in response to that event.

Why are they called callbacks? Because when you call a callback it is like calling back to the function that called with the callback... I'm sorry.

In the React community, these handler functions are called callback functions (or callbacks for short). But this is really just a fancy way of saying function. In fact, you could handle a mouse click with a function as simple as this:

function handleClick() {
  confirm('Are you sure you want to do the Macarena?')
}

Of course, just creating a callback isn’t enough. You also need to tell React that it exists!

Event props

In order for React to actually respond to click events on an element, you’ll need to tell React which callback to use. And to do so, you’ll need to set a callback on the element’s props.

For example, this example tells React to listen for click events on a button element, and to handle these clicks with the handleClick function:

ReactDOM.render(
  React.createElement('div', {},
    React.createElement('h1', {}, 'Macarena?'),
    React.createElement('button', {
      onClick: function handleClick() {
        alert('Are you sure you want to do the Macarena?')
      }
    }, 'CLICK ME'),
  ),
  document.getElementById('app')
)

But wait a minute - we just passed a function as a prop! HTML attributes can’t be functions?!

Actually, that is one of the neat things about React. Since React Elements are plain old JavaScript objects, you can add anything to an element – strings, numbers, other elements, or even functions. But while you can pass anything as a prop, there isn’t much point in doing so unless React understands why you’ve passed it. And that brings us to the name onClick.

In React, onClick is a special prop that can be used with any DOM React element (i.e. div, a, etc.). When you pass a function to an onClick prop, React knows to call that function each time the user clicks the element. In contrast, if you called the prop onInitiateMacarena, React wouldn’t do anything.

Of course, onClick is not the only special prop. React actually has special props to match most JavaScript events; a few common examples are onKeyDown, onKeyUp and onMouseMove.

You can see the full list of events in React’s official documentation, including the parameters that callbacks for each event will receive – which we’ll get to in a moment. But first, it’s time for some practice!

Make the buttons work

While discussing callbacks and events is a good start, the best way to really drill it into your head is to try it out. And with that in mind, it’s time for an exercise!

I’ve started you off with the read-only controls from the first exercise. Your task is to make the buttons work. This will involve creating three callback functions and adding them to the button’s props.

You don’t need to worry about the number input just yet! We’ll get to that in a moment. And don’t check the solution until you’ve given it a decent shot. Seriously.

import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'

// `createElement` is just a function. You can give it a new name to reduce
// the amount you'll need to type.
const createElement = React.createElement

let animating = false
let sway = 0.1

function renderApp() {
  const toggleText = `${animating ? 'Stop' : 'Start'} animation`

  ReactDOM.render(
    createElement(AnimatedPythagorasTree, { animating, sway },
      createElement('button', {}, toggleText),
      createElement('h5', {}, 'amount of sway'),
      createElement('button', {}, '<'),
      createElement('input', {value: sway, disabled: true }),
      createElement('button', {}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()
import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

// Variables that store current "state" of the animation.
let animating = false
let sway = 0.1

// This functions updates our state before re-rendering. It helps make the
// actual event handler functions a little simpler.
function setAndRerender(nextAnimating, nextSway) {
  animating = nextAnimating
  sway = nextSway
  renderApp()
}

// These are the functions that are called when the user clicks a button.
const toggle = () => setAndRerender(!animating, sway)
const decreaseSway = () => setAndRerender(animating, sway - 0.02)
const increaseSway = () => setAndRerender(animating, sway + 0.02)

function renderApp() {
  const toggleText = `${animating ? 'Stop' : 'Start'} animation`
  
  ReactDOM.render(
    createElement(AnimatedPythagorasTree, { animating, sway },
      createElement('button', {onClick: toggle}, toggleText),
      createElement('h5', {}, 'amount of sway'),
      createElement('button', {onClick: decreaseSway}, '<'),
      createElement('input', {value: sway, disabled: true }),
      createElement('button', {onClick: increaseSway}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()

And with that, you now know how to respond to clicks! In fact, you can now respond to any event that React has a special prop for. But what if just knowing that the event occurred isn’t enough?

Event objects

Whenever React calls a callback function, it passes it an object that contains details about the event. This object can be ignored if you don’t need it – just like we did in the above examples. But if you do need to know more about how the event occurred then the event object is where to look.

You may have noticed in the earlier examples that the handler functions didn't specify any arguments. This is because in JavaScript, you only have to name the arguments that you'll actually use.

So what kind of details does the event object hold? The answer depends on the type of event! For example, keyboard events will have a key property that specifies the key that was pressed, while mouse events will hold clientX and clientY properties that tell you where the mouse was at the time of the event.

let left = undefined
let top = undefined

// The event object will be passed to your callback as its first argument.
// You can call it anything, but it doesn't hurt to stick with `event`.
function moveButton(event) {
  left = event.clientX + 2
  top = event.clientY + 2
  renderApp()
}

function renderApp() {
  ReactDOM.render(
    React.createElement('button', {
      onMouseMove: moveButton,
      style: {left, top, position: left?'fixed':'absolute', color: 'red'},
    }, "please don't click me!"),
    document.getElementById('app')
  )
}

renderApp()
For more info on the raw JavaScript events, head over to MDN.

If you’ve ever used JavaScript events in jQuery or some other framework, names like clientX, clientY and key may feel familiar. This is because React’s events mirror the standard JavaScript events wherever possible.

But maybe you don’t know much about raw JavaScript events. And that’s absolutely OK! Because most apps only use two properties of the event object:

  • preventDefault(), a method that will cancel the browser’s default behavior for the event
  • target, which lets you see which DOM node caused the event

The target property in particular is incredibly useful, and we’ll take a more detailed look in a moment.

But what if you do need access to other properties, like mouse buttons and positions and key codes and focus and submit and others? You could look in the React events reference, but this is sparse information. So to make your life easier, I’ve put together a live events cheatsheet – just for you. And what’s more, React Armory members get exclusive access to a bunch of print-optimized cheatsheets. But I digress. Let’s take another look at that disabled number input

Join React Armory,
Get Cool Stuff

Exclusive access to resources.

Early access to new content.

No spam, ever.

One-way binding

React input elements are just elements. They take a value prop, and they’ll display what that value is. What they don’t let you do is change the value prop.

This design is sometimes called “one-way binding”, because data can only flow through a prop in one direction: parent to child.

A component can set its child element’s props. Child elements cannot directly change the values of their props.

But this begs the question: what happens when you type into an input element with a value prop? Well, you can find out for yourself by trying it below!

import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

let animating = false
let sway = 0.1

function renderApp() {
  const toggleText = `${animating ? 'Stop' : 'Start'} animation`

  ReactDOM.render(
    createElement(AnimatedPythagorasTree, { animating, sway },
      createElement('button', {
        onClick: () => { animating = !animating; renderApp() }
      }, toggleText),
      createElement('h5', {}, 'amount of sway'),
      createElement('input', {value: sway }),
    ),
    document.getElementById('app')
  )
}

renderApp()

Did you try it? Then take a moment to let the result sink in – if you type into an input with a value prop, nothing changes. But this isn’t to say that the value can’t change. You just need to give the input another way to let its parent know about changes. And can you think a way that might work?

You can pass the input a callback!

Housekeeping

I probably should mention that React is a broad church. In addition to inputs with value props, React also lets you leave out the value input and have the input change as people type. These valueless inputs are called uncontrolled components, but nobody uses them. Or at least nobody should.

Even if an element can’t change its props directly, it can always ask its parent to change its props for it.

In fact, you can see this in action with the above “start/stop” button. When you click on the button, React will call its onClick callback, which toggles the value of animating, which changes the text that is passed to the button.

Of course, unless you’re creating a joke UX, the onClick callback is not very useful for an input element. Instead, you’ll want to use an event that is triggered each time the user tries to change the value. And for that, you’ll want the onChange event.

The React `onChange` event acts like the vanilla JavaScript `onInput` event: it is triggered immediately after each change -- including on changes that don't originate from the keyboard.

And now that you can be notified each time the user tries to enter a value, all that is left is to find out exactly what change they tried to make. And for that, you’ll need to understand the event object’s target property.

Event targets

Under the hood, React events are just a wrapper around JavaScript events. And one of the peculiarities about JavaScript events is that they always have to originate from some DOM node.

This originating node is called the event’s target.

Not all events originate on the node that the callback was set on! For example an onClick event’s target will be whatever node was clicked – even if it is a child of the element with the onClick prop.

But what is the target in practice? Well, given you’ve set a callback on an element’s props, you may expect that the target will point to the element’s corresponding DOM node. And in the case of an onChange event you’d be right!

As it happens, the DOM node for an <input> element has a value property that holds its latest value. You can access this from within an onChange callback as follows:

ReactDOM.render(
  React.createElement('input', {
    placeholder: 'Type something...',
    value: '',
    onChange: (event) => {
      alert(`The current value of the input is "${event.target.value}".`)
    }
  }),
  document.getElementById('app')
)

If event.target.value actually holds the value that the user typed, how can it be that this value doesn’t appear after typing it? Actually, the input does update when the user types – but React changes it back to the old value before the screen repaints. Tricky, huh?

Making the input work

Hooray, you now know what the user has typed! But what shall you do with this event.target.value? The answer, of course, is that you’ll need to use it to re-render the input with a new value.

And this is a perfect opportunity for an exercise!

I’ve started you off with the the solution to the previous exercise – the buttons already work, but the input is still read-only. Your task is to add an onChange callback to the input and use it to render a new sway.

A hint: event.target.value is a string. You’ll need to use parseFloat in places to ensure the buttons continue to work as expected, and you’ll also want to handle the case where event.target.value is '' (i.e. the empty string).

Good luck, and remember that if you check the solution before trying yourself then you’re a dirty cheat!

import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

let animating = false
let sway = 0.1

function setAndRerender(nextAnimating, nextSway) {
  animating = nextAnimating
  sway = nextSway
  renderApp()
}

const toggle = () => setAndRerender(!animating, sway)
const decreaseSway = () => setAndRerender(animating, sway - 0.02)
const increaseSway = () => setAndRerender(animating, sway + 0.02)

function renderApp() {
  const buttonText = `${animating ? 'Stop' : 'Start'} animating`
  ReactDOM.render(
    React.createElement(AnimatedPythagorasTree,
      { animating, sway },
      
      createElement('button', {type: 'button', onClick: toggle}, buttonText),
      
      createElement('h5', {}, 'amount of sway'),
      createElement('button', {type: 'button', onClick: decreaseSway}, '<'),
      createElement('input', {value: sway}),
      createElement('button', {type: 'button', onClick: increaseSway}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()
import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

let sway = '0.1'
let animating = false

const getSway = () => (sway === '' ? 0 : parseFloat(sway))
const setAndRerender = (nextAnimating, nextSwayString) => {
  animating = nextAnimating
  sway = nextSwayString
  renderApp()
}

const toggle = () => setAndRerender(!animating, sway)
const setSway = (event) => setAndRerender(animating, event.target.value)
const decreaseSway = () => setAndRerender(animating, getSway() - 0.02)
const increaseSway = () => setAndRerender(animating, getSway() + 0.02)

function renderApp() {
  const buttonText = `${animating ? 'Stop' : 'Start'} animating`
  ReactDOM.render(
    React.createElement(AnimatedPythagorasTree,
      { animating, sway: getSway() },
      
      createElement('button', {type: 'button', onClick: toggle}, buttonText),
      
      createElement('h5', {}, 'amount of sway'),
      createElement('button', {type: 'button', onClick: decreaseSway}, '<'),
      createElement('input', {value: sway, onChange: setSway}),
      createElement('button', {type: 'button', onClick: increaseSway}, '>'),
    ),
    document.getElementById('app')
  )
}

renderApp()

Did you complete the exercise? Then congratulations! You now can make working controls! There’s just one problem…

Events and components

So far, this guide’s examples have not used any custom components – they’ve consisted entirely of React.createElement and ReactDOM.render. And while this is (hopefully) a fine way to learn, it won’t scale in the real world.

But say you were to wrap the example’s React.createElement calls in a function component. Even then, you’d still face the problem that all of the variables and callback functions are globals. There’d be no way to give different animations to different trees!

In fact, this is the same problem we ran into back in part two. And luckily, we can take the same approach to fixing it: instead of directly using globals as props, we can pass them in via props. Which brings us back to our First Rule.

The First Rule Of React: If it is practical to pass the data you need through props, it must be passed through props.

In JavaScript, callback functions are just data. They can be passed through props. And this means that if your component uses any callbacks (and follows the first Rule), they must be passed through props.

Custom events

Adding an event to your component is as simple as passing it a callback function via props, then calling it when something of interest happens. Unlike React’s inbuilt events, you can name the prop anything, and you can call the function with any arguments – or even no arguments!

But how do you call that function? Well, you could pass the function on to another element’s onClick or onChange prop and let React do the calling. Or, you could create an event handler and call it from within that.

It is probably easier to see this in action, so let’s do a demo.

While creating event handlers within function components works, it can sometimes cause performance issues. We’ll cover a faster way to create event handlers like these in Component Instances and State.

In this example, I’ve extracted the sway control (with its two buttons) into a separate component. This component defines handlers for its input and buttons that call the component’s own onChange prop. And importantly, instead of passing an event object, they just pass the new value.

Have a play around with this example for a bit before moving on – it’ll help to get a feel for exactly how callbacks work.

import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

let animating = false
let sway = '0.1'

const toggle = () => { animating = !animating; renderApp() }
const setSway = (value) => { sway = value; renderApp() }

// This Component expects to receive a function on its `onChange` prop.
// It then calls that function from its own event handlers.
function NumericInput({ value, onChange }) {
  const numericValue = !value ? 0 : parseFloat(value)
  const setSway = (event) => onChange(event.target.value)
  const decreaseSway = () => onChange(String(numericValue - 0.02))
  const increaseSway = () => onChange(String(numericValue + 0.02))
  
  return (
    createElement('div', {},
      createElement('button', {type: 'button', onClick: decreaseSway}, '<'),
      createElement('input', {value: value, onChange: setSway}),
      createElement('button', {type: 'button', onClick: increaseSway}, '>'),
    )
  )
}

function renderApp() {
  const toggleText = `${animating ? 'Stop' : 'Start'} animating`
  ReactDOM.render(
    React.createElement(AnimatedPythagorasTree,
      { animating, sway: !sway ? 0 : parseFloat(sway) },
      createElement('button', {type: 'button', onClick: toggle}, toggleText), 
      createElement('h5', {}, 'amount of sway'),
      
      // Components with events can be used just as any other component
      createElement(NumericInput, { value: sway, onChange: setSway })
    ),
    document.getElementById('app')
  )
}

renderApp()

You’ve now covered basically everything there is to know about React events! But this is a lot to take in, so to really hammer in that knowledge you’re going to need to practice it.

And that’s why I have an exercise for you!

The Exercise

Your task is to write a component that wraps the entire tree and its controls. This component should accept four props:

  • animating, a boolean that indicates whether the animation is active
  • sway, a string that holds the current value of the sway input
  • onToggle, a function that is called when the user toggles the animation
  • onChangeSway, a function that accepts new values from the sway input

To make your life a little easier, I’ve imported the NumericInput from the above example. You should use this in your solution.

The body of your component function should actually be pretty simple – my solution puts it at just 7 lines (without comments). And with this in mind, the thing you’ll want to focus on is understand what callbacks are, and how they can be used.

One last thing: I usually tell you that you can’t look at the solution until you’ve given it a shot. But for this exercise, I will recommend that you don’t look at the solution until you’ve actually solved it. If you get stuck, you’ll want to go back and look at some of the previous examples. Then once you do figure it out, use the solution to compare your approach with mine.

// The `NumericInput` component from the last example is available for use!
import { NumericInput } from 'react-armory-pythagoras-tree'
import { AnimatedPythagorasTree } from 'react-armory-pythagoras-tree'
const createElement = React.createElement

function PythagorasTree({ sway, animating, onChangeSway, onToggle }) {
  // Replace this with your solution
  return createElement('h1', {style:{color: 'red'}}, "Now it's your turn")  
}

let animating = false
let sway = 0.1
function renderApp() {
  ReactDOM.render(
    createElement(PythagorasTree, {
      animating,
      sway,
      onToggle: () => { animating = !animating; renderApp() },
      onChangeSway: (value) => { sway = value; renderApp() },
    }),
    document.getElementById('app')
  )
}
renderApp()
import {
  AnimatedPythagorasTree,
  NumericInput
} from 'react-armory-pythagoras-tree'
const createElement = React.createElement

// This PythagorasTree component can be re-used anywhere, so long as it
// receives the appropriate callbacks!
function PythagorasTree({ sway, animating, onChangeSway, onToggle }) {
  const toggleText = `${animating ? 'Stop' : 'Start'} animating`

  return createElement(AnimatedPythagorasTree,
    { animating, sway: !sway ? 0 : parseFloat(sway) },
    
    // Because `onToggle` is a function, it can be directly passed to
    // another element that expects a callback
    createElement('button', {type: 'button', onClick: onToggle}, toggleText), 
    createElement('h5', {}, 'amount of sway'),
    createElement(NumericInput, { value: sway, onChange: onChangeSway })
  )
}

// With most of the application's code split out into a component, it is
// easier to see exactly how the app is structured.
let animating = false
let sway = 0.1
function renderApp() {
  ReactDOM.render(
    createElement(PythagorasTree, {
      animating,
      sway,
      onToggle: () => { animating = !animating; renderApp() },
      onChangeSway: (value) => { sway = value; renderApp() },
    }),
    document.getElementById('app')
  )
}
renderApp()

If you’ve gotten this far through the guides, I want to take a moment to say congratulations! You’re now capable of making almost anything with React, ranging from small utilities to enormous applications.

But while you may now have the knowledge to find some solution to a problem, not all solutions are created equal.

In particular, there is a tool that we’re yet to cover that can vastly simplify your React applications. This tool gives you access to seriously improved performance, while also allowing you to create components that are far more powerful than those you can create with functions alone.

So what’s the secret? Actually, React has two types of components. There’s the function components that you know, and then there are class components. And while function components can only do what they’re told, class components can have a mind of their own.

In fact, you’ve been working with a class component throughout this guide… so did you ever wonder how AnimatedPythagorasTree actually animates?

Continue to: Self-contained Components

Join React Armory,
Get Cool Stuff

Exclusive access to resources.

Early access to new content.

No spam, ever.

React Armory is growing. Join in to be the first to experience new lessons, examples and resources.