This blog post is archived. It's no longer maintained and may contain outdated information.

Tracing user interactions with React

May 8th, 2020 — 4 min read

by Luke Porter
by Luke Porter
No translations available.Add translation

This post has been archived. This API was removed in React 17.

In my post "React Production Performance Monitoring", I show you how to use React's Profiler component to monitor the performance of your application in production. The information you get from this is useful, but really all it can tell you is: "Hey, the mount/update for this tree of components took x amount of time." Then you can graph that data and identify spikes/regressions in performance for that part of your app.

It would be even useful to have information about what interactions the user performed to trigger that update. For example: "Based on the data we have, when the user clicks the dropdown toggle button, the dropdown update is fast, but when they type into the field, the dropdown update is slower."

Another benefit to interaction tracing is it adds some context to what you can visualize in the React DevTools Profiler tab. We'll get a look at that soon.

The data the Profiler calls your onRender method with has a property for interactions which is intended to provide this for you. And I want to show you how to use that API.

🚨 Please remember that this is an unstable/experimental API from React and may change when the feature is officially released.

Basic usage

React does all of its scheduling through the scheduler package. Normally you don't interact with this directly, but this package is where you're going to get the APIs to instrument your components for interaction tracing.

Let's say you have a simple greeting component:

function Greeting() {
  const [greeting, setGreeting] = React.useState('')

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setGreeting(`Hello ${name}`)
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">Name:</label>
        <input id="name" />
      </form>
      <div>{greeting}</div>
    </div>
  )
}

When setGreeting is called, that triggers a state update. Let's assume we have reason to measure this interaction and monitor it in the long term (you don't necessarily want to add this complexity for every interaction). Here's how we'd do that:

// your other imports
import {unstable_trace as trace} from 'scheduler/tracing'

function Greeting() {
  const [greeting, setGreeting] = React.useState('')

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    trace('form submitted', performance.now(), () => {
      setGreeting(`Hello ${name}`)
    })
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">Name:</label>
        <input id="name" />
      </form>
      <div>{greeting}</div>
    </div>
  )
}

Now the interactions for this update will include information for this specific interaction based on the name of "form submitted".

The API for trace is: trace(id, startTimestamp, callbackThatTrigersUpdates)

Async tracing

So what happens if this interaction is asynchronous? Should we have one trace for the state update and then another for the update when the response comes back? Wouldn't it be better to tie these two related updates together? Yes it would! Luckily there's support for that!

Say we have to fetch the greeting from a server. Let's rewrite this for that use case:

function Greeting() {
  const [greeting, setGreeting] = React.useState('')

  // please don't judge me, I'm leaving out loading and error states and cancelation
  // to simplify this example!
  const [name, setName] = React.useState('')

  React.useEffect(() => {
    if (!name) {
      return
    }
    const onSuccess = newGreeting => setGreeting(newGreeting)
    fetchGreeting(name).then(onSuccess)
  }, [name])

  function handleSubmit(event) {
    event.preventDefault()
    setName(event.target.elements.name.value)
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">Name:</label>
        <input id="name" />
      </form>
      <div>{greeting}</div>
    </div>
  )
}

To support tracing this, we'll use unstable_wrap:

// your other imports
import {unstable_trace as trace, unstable_wrap as wrap} from 'scheduler/tracing'

function Greeting() {
  const [greeting, setGreeting] = React.useState('')

  // please don't judge me, I'm leaving out loading and error states and cancelation
  // to simplify this example!
  const [name, setName] = React.useState('')

  React.useEffect(() => {
    if (!name) {
      return
    }
    trace('name updated', performance.now(), () => {
      const onSuccess = wrap(newGreeting => setGreeting(newGreeting))
      fetchGreeting(name).then(onSuccess)
    })
  }, [name])

  function handleSubmit(event) {
    event.preventDefault()
    setName(event.target.elements.name.value)
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">Name:</label>
        <input id="name" />
      </form>
      <div>{greeting}</div>
    </div>
  )
}

Cool? Yeah that's cool! And check it out, here's what that sort of thing looks like in your React DevTools:

Interactions view in DevTools

Go ahead and give it a try in your app. It definitely helps (especially the async stuff. Those little squares are clickable so you know which commits came from the interaction directly!).

You can learn more about the tracing API here.

Good luck!

Epic React

Get Really Good at React

Illustration of a Rocket
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

If you found this article helpful.

You will love these ones as well.