the preview hung on "Connecting…"

`useClient()` rebuilds its verbs on every render, so `get` has a new identity each time. It was in the
effect's dependency list, so the effect re-ran on every render — and its first act is setReady(false),
which renders again, which builds another `get`, which re-runs the effect. The frame never mounted.

Through a ref instead, with the effect depending only on the port.

This trap is documented in CLAUDE.md and it is the same one that silently disabled every playback
report in the Jellyfin player. The platform's own Preview panel sidesteps it by depending on neither
the client nor the URL, which is what this now does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 02:08:54 +01:00
co-authored by Claude Opus 5
parent 82862ed340
commit 8a152f5b63
+18 -3
View File
@@ -490,7 +490,21 @@ const LinkRow = ({
* says so in its own comments.
*/
const LayoutPreview = ({ port }: { port: number | null }) => {
const { get } = useClient();
const client = useClient();
/*
Through a ref, and this is the whole reason the preview hung.
`useClient()` rebuilds its verbs on EVERY render, so `get` has a new identity each time. With it in
the dependency list the effect re-ran on every render, and its first act is `setReady(false)` —
which renders again, which makes another `get`, which re-runs the effect. The frame never mounted
and the panel said "Connecting…" forever.
It is a known trap here: the same thing silently disabled every playback report in the Jellyfin
player. The platform's own Preview panel avoids it by depending on neither the client nor the URL.
*/
const clientRef = useRef(client);
clientRef.current = client;
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState<string | null>(null);
@@ -499,13 +513,14 @@ const LayoutPreview = ({ port }: { port: number | null }) => {
let alive = true;
setReady(false);
setFailed(null);
void get<{ ok: boolean }>('/dev-proxy/session')
void clientRef.current
.get<{ ok: boolean }>('/dev-proxy/session')
.then(() => alive && setReady(true))
.catch((failure: unknown) => alive && setFailed(readableError(failure)));
return () => {
alive = false;
};
}, [port, get]);
}, [port]);
const message =
!port ? 'No development server is configured — set its port under Administration.'