Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[2.0 rc.14 / next] Feature request: public onHydrationEnd, so E2E tests can tell when hydration has finished

Chiusa
#4,000 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
62/100
Tipo di issue
Funzionalità
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
javascript, playwright, typescript
Ambito
frontend, testing

Direzione di ricerca

Start with the onHydrationEnd exports and server stub in packages/solid/src/index.ts and packages/solid/src/server/index.ts, then read packages/solid/test/internal-surface.spec.ts and published-declarations.spec.ts. Check how the declarations currently exclude this helper and update the public API coverage and documentation described in the issue. Done means the callback is documented as a public export, including when it fires and its single-root limitation, and the declaration checks reflect that.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Our Playwright tests need a documented end-of-hydration signal before they check state the client writes: history state after a navigation, attributes on <html>, no CSP violations once the client code has run. A few tests do the reverse: they hold the client script request and assert that the page has not hydrated yet. Solid 2 has this signal in onHydrationEnd, but it is @internal, so what a test can read from outside the bundle today is a side effect with no contract.

The changelog entry that took onHydrationEnd out of the public API (CHANGELOG.md#L2034) says snapshots make hydration timing transparent. That holds for code running inside the app. A page.evaluate call runs outside it and only sees the DOM and globals, so snapshots don't help there.

Today
// Playwright, against 2.0.0-rc.14
const hydrated = () =>
  page.evaluate(() => (globalThis as any)._$HY?.events === null);

// script request held: the page must not be hydrated
expect(await hydrated()).toBe(false);

// script released
await expect.poll(hydrated).toBe(true);
await expect(page.locator("html")).toHaveAttribute("lang", "pt-BR");

We use this check in six E2E test files. It reads internals that each mean "done" at a different moment:

  • drainHydrationCallbacks runs the onHydrationEnd callbacks synchronously and sets _$HY.done = true only in a later setTimeout (hydration.ts#L387-L401).
  • _$HY.events goes to null in the microtask that runHydrationEvents() queues, only if sharedConfig.done is already true when it runs and every queued event could be replayed (client.ts#L2636-L2672). That is the only place it gets nulled.
  • runHydrationEvents() is emitted after a hydratable template root that has a delegated handler or a spread (babel-plugin element.ts#L337-L344, compiler events.rs#L21-L28), plus dynamic().

So on a page where nothing in the hydrated markup has a delegated handler, a spread or a dynamic() element, the check never turns true. A minimal SSR page shows it on rc.14 in headless Chromium: with <main><p>static {n()}</p></main> as the whole app, _$HY.done is true and _$HY.events is still [] five seconds after load. Swap the paragraph for a <button onClick={...}> and _$HY.events goes to null.

_$HY.done would be a better proxy, but server-components-principles.md lists it among the things that dissolve (L656), so we'd rather not move to it. sharedConfig.onHydrationEnd is reachable through solid-js/internal, but that entry says it is not public API and may change or vanish in any release (internal.ts#L1-L4), and the sharedConfig docs call it cross-package wiring (hydration.ts#L187-L198). #3718 moved Solid Router and TanStack off that object for the same reason, and a test hook in an app looks like the same case to us.

Proposal

Drop @internal from onHydrationEnd, maybe as @experimental like setLinkClaim in #3921. #3718 made isHydrating public again after the changelog entry quoted above had removed it. Ruling 3 already names onHydrationEnd and isHydrationInProgress() as the single meaning of done (frames-rulings.md#L127-L135), so exposing it keeps a single channel.

On next it is already a runtime export of solid-js on both entries (index.ts#L138-L155, and a stub on the server at server/index.ts#L203-L206), so the bare export adds no runtime bytes. It sits in an @internal block with other helpers, so a PR would move it out of that block, drop the tag at the declaration and on the server stub, and update internal-surface.spec.ts (which lists onHydrationEnd among the seams that must stay out of the declarations, #L60-L76) and published-declarations.spec.ts, plus a changeset and a line in the docs.

The app would set its own marker and the tests wait for it:

import { onHydrationEnd } from "solid-js";
import { hydrate } from "@solidjs/web";

hydrate(() => <App />, document);
// register after hydrate(), see below
onHydrationEnd(() => {
  document.documentElement.dataset.hydrated = "";
});
await expect(page.locator("html[data-hydrated]")).toBeAttached();

Before the entry runs the attribute isn't there, which covers the "client hasn't run yet" assertions too. Registered before hydration starts, the callback is queued for the next microtask and doesn't wait for hydration work started afterward (hydration.ts#L379-L381). In the same repro, with a streamed <Loading> boundary, a callback registered before hydrate() runs before the boundary content arrives, and one registered after hydrate() waits for it. A public version should document this.

The marker would mean the hydration phase ended. Whether it succeeded is a separate question: when a document root's module preload fails, hydrate() sets hydrating to false and keeps the server markup (client.ts#L2455-L2478), and that drains the callbacks. In the same repro, pointing _$HY.r._assets at a missing chunk sets the marker and _$HY.done, and a click on the button does nothing. Our tests check errors and the expected behavior anyway, but the docs would need to say this, or a later version could report how hydration ended, at some cost in bytes. Either would work for us.

We saw that isHydrationInProgress is kept off the exports so app code isn't invited to branch on hydration state (hydration.ts#L327-L333). We'd only use the callback to set a test marker, and our components would never read hydration state. If you'd rather keep it away from app code, a documented marker meant for test tooling would work for us as well. What we need is a documented way to observe this phase without reading _$HY.events.

Our pages have a single hydration root. With several roots GH5 still applies: done fires before a root deferred behind a module preload has claimed anything (preload-hold.spec.tsx#L89, a test.fails). Until that's fixed, the docs shouldn't call this page-wide completion.

Checked against 2.0.0-rc.14 from npm and next at 84dada7. Related: #3718, #3813, #3921.

Lingua principale
TypeScript
Stelle
36.1k
Fork
1.1k
Merge medio
10h 42m
PR unite (30g)
361

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.