Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Cerrado
#4,000 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
62/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
javascript, playwright, typescript
Área
frontend, testing

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
TypeScript
Estrellas
36.1k
Forks
1.1k
Merge medio
11 h 49 min
PR fusionados (30 d)
346

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de solidjs/solid

Todos los issues de solidjs/solid

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.