Diagnostics

LUM2001: Stale derived value

A value derived from a signal is computed once in setup and never updates.

Severity: warning · Category: reactivity (2xxx)

Reported by: the compiler. A warning. Does not apply to async components, whose body re-runs.

#What it means

A component's body runs once. const doubled = count() * 2 reads the signal at setup time and stores a number, so JSX that uses doubled shows the first value forever.

The compiler treats a zero-argument call of a local or imported binding (count(), user.name()) as a signal read, and so does a read of the component's props. Calls that can only run on the server are not reads, so server pages can store request data in constants:

  • lumiose's request APIs: url(), params(), locals(), cookies(), request(), response() and publicEnv(). Inside SPA routes, url() and params() from lumiose/router are reactive and still count.
  • lumiose's control-flow APIs, which never return: notFound(), redirect() and error(), so const project = getProject(slug) ?? notFound() is fine.
  • Anything imported from a server-only module (*.server.ts, src/server/**), such as queries().getBook(isbn). Data functions from a workspace package (getDashboard() from @acme/data) are not known to be server-only: re-export them from a module under src/server/ (export { getDashboard } from "@acme/data";) and import them from there.
tsx
// src/routes/books/[isbn]/page.stsx (a server page): no warning
const user = locals().user ?? null;
const next = url().searchParams.get("next");
const featured = queries().featuredBooks(6);

Pages and layouts whose route mode is static or server render once per request (or build), so their own props are plain values too: deriving from them is fine. Routes in spa mode, islands and other components re-render with new props and still get the warning. @lumiose/vite, lumiose check and the language server read the route modes the same way (export const mode, the nearest layout's, then lumiose({ mode })).

tsx
// src/routes/books/[slug]/page.stsx (mode `static` or `server`): no warning
export default function Book(props: { params: { slug: string } }) {
  const title = props.params.slug.replaceAll("-", " ");
  return <h1>{title}</h1>;
}

#Example

tsx
export function Counter() {
  const count = signal(1);
  const doubled = count() * 2;
  return <button onClick={() => count.update((n) => n + 1)}>{doubled}</button>;
}

#How to fix it

Derive with a function or computed, and call it where you need the value:

tsx
const doubled = () => count() * 2; // cheap derivations
const total = computed(() => expensive(count())); // cached
return <button>{doubled()}</button>;

If you really want the initial value only, read it with count.peek() or untrack(count) to make that explicit.