HP5001 (lost-interaction) means a click made while the page was still loading
had no effect: the server-rendered button was visible before React could
handle its events. The same click after hydration works. Keep the control
disabled until the page is interactive, or make it work without JavaScript.
| Code | HP5001 |
|---|---|
| Name | lost-interaction |
| Default severity | Warning |
| Group | Interaction during hydration |
| What it means | A click made while the page was loading had no effect: the page looked ready before React could handle events. |
What the HP5001 lost interaction finding means
Server rendering shows the page before its JavaScript has loaded. Until React has hydrated and attached its event listeners, a button is only HTML: a click on it runs no handler, and the user has no way to tell.
The finding comes from the interaction checks, which run
with checks.interactions: true or --interactions. hydration-proof finds the
first button outside forms and links, then loads the page four times: twice
without a click (to make sure the page renders the same each time), once with
a click after hydration and once with a click while the page's scripts are held
back. If the late click changes the page and the early one does not, the click
was lost:
A click on this button while the page was loading did nothing; the same click after hydration changes the page.It is a warning: the page works, but it looks ready before it is. On a slow connection, that window can last seconds.
Likely causes
- A large JavaScript bundle for the first view, so hydration starts late.
- An important control inside a part of the page that hydrates last.
- Buttons that only work through a React
onClick, with no form or link behind them.
How to fix it
Disable the control until the page is interactive
Show it as disabled or loading. The server and the first client render agree, and an effect enables it:
"use client";
import { useEffect, useState } from "react";
export function AddToCart({ onAdd }: { onAdd: () => void }) {
const [ready, setReady] = useState(false);
useEffect(() => setReady(true), []);
return (
<button type="button" disabled={!ready} onClick={onAdd}>
{ready ? "Add to cart" : "Loading…"}
</button>
);
}Make it work without JavaScript
Links navigate and forms submit before hydration. With a Server Action, the same form works in both cases:
import { addToCart } from "./actions";
export function AddToCart({ productId }: { productId: string }) {
return (
<form action={addToCart}>
<input type="hidden" name="productId" value={productId} />
<button type="submit">Add to cart</button>
</form>
);
}Hydrate important controls first
Ship less JavaScript for the first view. Suspense boundaries hydrate in order of interaction, so splitting the page into boundaries lets React start with the part the user touches.
When it is acceptable
If a short dead window is fine for your app, add an ignore.issues rule with
code: "HP5001" and a reason; see ignoring findings.