React's Profiler can tell you how long a mount or update took for a component tree, and how that changes over time. What it can't tell you is what the user did to cause that work. Interaction tracing fills that gap by tagging the commits that stem from a given user action — for example, distinguishing a dropdown update triggered by a button click from one triggered by typing into a field. The same data also adds context to the React DevTools Profiler tab.
🚨 This tracing API is unstable and experimental; it may change before official release. Additionally, it was removed in React 17.
The mechanism lives in the Profiler's onRender callback, which receives data with an interactions property. Scheduling itself goes through React's scheduler package, which is not something you normally touch, but it exposes the instrumentation APIs used here.
Tracing a synchronous update
Take a 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>
)
}
Calling setGreeting triggers a state update. When that update is worth measuring long term (not every interaction deserves the added complexity), instrument it like this:
// 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>
)
}
The resulting interactions data for this update carries information tied to the id "form submitted". The trace API itself takes the form:
trace(id, startTimestamp, callbackThatTriggersUpdates)
Tying asynchronous updates together
When the interaction spans an async boundary, splitting it into one trace for the initial state update and another for the response would lose the relationship between the two. Instead, use unstable_wrap so both updates belong to a single interaction.
Consider a version that fetches the greeting from a server:
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>
)
}
Wrapping the async continuation keeps both related updates in the same trace:
// 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>
)
}
The DevTools view reflects this. Each square is clickable, so you can see exactly which commits were produced by the interaction:
The tracing API is documented here.



