Repository navigation
Conversation
clientOnly only accepted a module with a default export, so a named
export needed import(...).then(m => ({ default: m.Chart })). A loader can
now resolve to the component itself, which also works for named exports:
clientOnly(() => import("./chart").then(m => m.Chart))
Reading the export inside the loader lets the bundler leave the module's
other exports out of the chunk. An option that names the export, read
with a computed key inside clientOnly, would keep every export.
Closes #2064.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for solid-start-landing-page ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🦋 Changeset detectedLatest commit: aec0c9e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
Use import(...).then(({ Chart }) => Chart) in the JSDoc, the changeset
and the test route. On Vite 6, which the 1.x line uses, the
.then(m => m.Chart) form keeps every export of the module, because
Vite's preload wrapper ends up between import() and .then. The
destructured form tree-shakes on both Vite 6 and Vite 8.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brenelz
approved these changes
Oct 14, 2026
brenelz
left a comment
Contributor
There was a problem hiding this comment.
Approved but looks like the tests failed?
The default-export case visited /client-only. In bundled dev mode, once /client-only has been visited, its route module is no longer fetched lazily, and hydration-navigation.test.ts waits for that request, so it timed out when it ran after this file. Test the default export on its own /client-only/default route instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2064. Closes #1929. The 1.x version is #2347.
clientOnlyonly accepted a module with a default export, so a named export neededimport("./chart").then(m => ({ default: m.Chart })). A loader can now resolve to the component itself:Default exports work as before:
clientOnly(() => import("./map")).This also covers #1929, a component defined in the same file:
The component's file is still part of the server bundle, so this only fits when the file's top-level code is safe on the server. Browser-only imports still need a separate file, as Ryan explained in #1929.
Why not an
exportoptionThe issue proposes
clientOnly(() => import("./chart"), { export: "Chart" }). I measured both shapes with a Vite 8 (Rolldown) build of a module with a used exportA, an unused exportB, and a default export:AB{ export: "A" }, read asm[key]insideclientOnlyimport(...).then(m => m.A)import(...).then(({ A }) => A)async () => (await import(...)).AclientOnly(() => import(...))(default, today)A computed key read inside
clientOnlyhides which export is used, so the bundler keeps the whole module. Reading the export inside the loader keeps tree-shaking. This matters for the case the issue mentions, a component exported next to a route: with anexportoption, the whole route module would end up in the client-only chunk. The last row also shows that today's default form keeps every export of the imported module, which might be worth a note in the docs.Why the docs use the destructured form
On Vite 6, which the 1.x line uses, the
.then(m => m.A)form keeps every export in an app build. Vite 6 compiles it topreload(() => import(...), deps).then(m => m.A), so.thensits outside the preload wrapper and Rollup cannot see which export it reads. Vite 8 onmainputs the whole chain inside the wrapper (preload(() => import(...).then(m => m.A), deps)) and tree-shakes every form. The destructured.then(({ A }) => A)form tree-shakes on both, so the JSDoc, the changeset and the tests use it. Details are in #2347.Change
() => Promise<T | { default: T }>.load()uses the result directly when it is a function, and itsdefaultotherwise. A module namespace object is never a function and a component always is, so the two cannot be confused.{ default }form, the explicitclientOnly<typeof X>(...)generic, and the{ default: m.Chart }workaround keep working.Testing
apps/tests:/client-only/namedroute ande2e/client-only.test.ts. For both the existing default-export route and the named one, the test checks that the server HTML has the fallback and not the component, and that the component renders after hydration. The named test fails without this change, because the component never renders.treeshaking/client-only.server.test.ts: the named export's code is in the client bundle, and the module's unused export is not.tscagainst a temporary file: props are inferred for the default,.then(m => m.X), destructured andasyncforms.m.Missingis an error, and so are wrong or missing props. The explicit generic and the{ default }workaround compile. I did not commit this as a test, becausepackages/start'stsconfigeither skips spec files or would emit them intodist.@solidjs/starttest:ci(237 tests),typecheck,validate-importsandbuildpass.apps/testsunit tests (7), Playwright (44) and bundled-dev Playwright (44) pass. After switching to the destructured form, the build, unit tests and the client-only and hydration e2e tests were rerun and pass.🤖 Generated with Claude Code