Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

已关闭
#4,000 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
62/100
Issue 类型
功能
描述清晰度
描述清楚
活跃度
活跃
技术栈
javascript, playwright, typescript
领域
frontend, testing

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
TypeScript
星标
36.1k
派生
1.1k
平均合并
11 小时 32 分钟
30 天内合并 PR
356

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

solidjs/solid 的其他 Issue

查看 solidjs/solid 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。