This site runs best with JavaScript enabled.

Tracing user interactions with React

Photo by Luke Porter


How to use the (EXPERIMENTAL) interactions tracing API in React.

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.

Alert: 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:

1function Greeting() {
2 const [greeting, setGreeting] = React.useState('')
3
4 function handleSubmit(event) {
5 event.preventDefault()
6 const name = event.target.elements.name.value
7 setGreeting(`Hello ${name}`)
8 }
9
10 return (
11 <div>
12 <form onSubmit={handleSubmit}>
13 <label htmlFor="name">Name:</label>
14 <input id="name" />
15 </form>
16 <div>{greeting}</div>
17 </div>
18 )
19}

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:

1// your other imports
2import {unstable_trace as trace} from 'scheduler/tracing'
3
4function Greeting() {
5 const [greeting, setGreeting] = React.useState('')
6
7 function handleSubmit(event) {
8 event.preventDefault()
9 const name = event.target.elements.name.value
10 trace('form submitted', performance.now(), () => {
11 setGreeting(`Hello ${name}`)
12 })
13 }
14
15 return (
16 <div>
17 <form onSubmit={handleSubmit}>
18 <label htmlFor="name">Name:</label>
19 <input id="name" />
20 </form>
21 <div>{greeting}</div>
22 </div>
23 )
24}

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:

1function Greeting() {
2 const [greeting, setGreeting] = React.useState('')
3
4 // please don't judge me, I'm leaving out loading and error states and cancelation
5 // to simplify this example!
6 const [name, setName] = React.useState('')
7
8 React.useEffect(() => {
9 if (!name) {
10 return
11 }
12 const onSuccess = newGreeting => setGreeting(newGreeting)
13 fetchGreeting(name).then(onSuccess)
14 }, [name])
15
16 function handleSubmit(event) {
17 event.preventDefault()
18 setName(event.target.elements.name.value)
19 }
20
21 return (
22 <div>
23 <form onSubmit={handleSubmit}>
24 <label htmlFor="name">Name:</label>
25 <input id="name" />
26 </form>
27 <div>{greeting}</div>
28 </div>
29 )
30}

To support tracing this, we'll use unstable_wrap:

1// your other imports
2import {unstable_trace as trace, unstable_wrap as wrap} from 'scheduler/tracing'
3
4function Greeting() {
5 const [greeting, setGreeting] = React.useState('')
6
7 // please don't judge me, I'm leaving out loading and error states and cancelation
8 // to simplify this example!
9 const [name, setName] = React.useState('')
10
11 React.useEffect(() => {
12 if (!name) {
13 return
14 }
15 trace('name updated', performance.now(), () => {
16 const onSuccess = wrap(newGreeting => setGreeting(newGreeting))
17 fetchGreeting(name).then(onSuccess)
18 })
19 }, [name])
20
21 function handleSubmit(event) {
22 event.preventDefault()
23 setName(event.target.elements.name.value)
24 }
25
26 return (
27 <div>
28 <form onSubmit={handleSubmit}>
29 <label htmlFor="name">Name:</label>
30 <input id="name" />
31 </form>
32 <div>{greeting}</div>
33 </div>
34 )
35}

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!

Discuss on TwitterEdit post on GitHub

Share article
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He'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.

Join the Newsletter



Kent C. Dodds