Skip to content

Solid 2: <Head> writes a signal during server render ([SERVER_WRITE] in dev SSR) #80

Open

Description

@ethan-huo

<Head> calls a signal setter while its children render on the server, so every SSR render that uses <Head> logs [SERVER_WRITE] in dev. Solid says this warning will become an error.

Versions

  • @solidjs/meta 1.0.0-next.2 (latest)
  • solid-js, @solidjs/web, @solidjs/signals: 2.0.0-rc.19 and 2.0.0-rc.21 (same result)
  • Node 26, dev export condition

Repro

No JSX or bundler is needed. package.json is { "type": "module" } with the four packages above installed:

// repro.mjs — run: node --conditions=development repro.mjs
import { createComponent } from "solid-js";
import { renderToString, ssr, escape } from "@solidjs/web";
import { Head, Title } from "@solidjs/meta";

const Tags = () =>
  createComponent(Head, {
    get children() {
      return createComponent(Title, { children: "Hello" });
    }
  });
const Doc = () =>
  ssr(["<html><head></head><body>", "</body></html>"], escape(createComponent(Tags, {})));

console.log(renderToString(() => createComponent(Doc, {})));

Actual

[SERVER_WRITE] Writing a signal on the server is deprecated and will become an error. Server render is pure: ...
  in <Doc> › <Tags> › <Head> › <provider> › <Title>
<html><head><title data-dh="title">Hello</title></head><body></body></html>

Expected

The same HTML and no SERVER_WRITE. The page doesn't write any state itself; the write comes from <Head>'s own registration.

Cause

src/index.ts L74–L86 (the v1.0.0-next.2 commit): Head keeps a dirty signal (ownedWrite: true), and register calls setDirty(undefined) as each child tag registers (L77), and again in its onCleanup (L80). On the server, the plain-value setter in solid-js's server build calls warnServerWrite("signal") (packages/solid/src/server/signals.ts, L1388–L1389 at solid-js@2.0.0-rc.21). ownedWrite only silences the client's owned-scope check, so it doesn't prevent this warning.

The write does nothing on the server. Server signals are inert, and the server useHead resolves a function group's membership when the owning boundary flushes. By then tags already holds every registered tag, so dirty adds nothing.

Suggested fix

Skip the write on the server, for example if (!isServer) setDirty(undefined) in both places, or create the signal only on the client. I tested the guard against the published dist/index.js with the repro above (rc.21): the warning disappears and the HTML is byte-identical.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions