From a90479224bfa813a4bf26bfdbf09b042eaedf08b Mon Sep 17 00:00:00 2001 From: Shridhar Gupta Date: Mon, 27 Apr 2026 13:00:53 -0600 Subject: [PATCH] docs(state): clarify observable() input is the underlying data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third sub-section under "Observables are mutable" explaining that the value passed to observable() is not cloned — it becomes the underlying data and is mutated in place as fields are updated. Calls out the most common gotcha (reusing a shared initialState constant as a reset target — the constant becomes structurally equal to the current state, so set() is a no-op via deep-equality) and shows the factory pattern as the fix. Refs LegendApp/legend-state#647 — when investigating that issue I audited my own codebase and found 8 stores with silently broken reset functions from this exact pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/content/state/v3/usage/observable.mdx | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/content/state/v3/usage/observable.mdx b/docs/content/state/v3/usage/observable.mdx index 5962840..feb8f85 100644 --- a/docs/content/state/v3/usage/observable.mdx +++ b/docs/content/state/v3/usage/observable.mdx @@ -557,3 +557,32 @@ const list = list$.get() const idx = list.findIndex((item) => item.id === itemId) list$[idx].delete() ``` + +#### 3. The initial value passed to `observable()` is the underlying data + +Following from above: when you pass an object or array to `observable()`, that value becomes the underlying data — it is not cloned. As you call `.set()` on fields, the original object is mutated in place. This is intentional and what makes Legend-State fast, but it surprises people coming from libraries that treat their input as immutable (Zustand/Redux/MobX). + +A common pitfall is reusing a shared `initialState` constant as a "reset target": + +```js +const initialState = { onboardingCompleted: false, name: '' } +const state$ = observable(initialState) + +state$.onboardingCompleted.set(true) +console.log(initialState) +// { onboardingCompleted: true, name: '' } ← mutated in place + +state$.set(initialState) +// ❌ no-op: `initialState` IS the current value, so deep-equality check skips notify +``` + +If you want a stable reset target, use a factory so each call produces a fresh object: + +```js +const createInitialState = () => ({ onboardingCompleted: false, name: '' }) + +const state$ = observable(createInitialState()) +state$.onboardingCompleted.set(true) + +state$.set(createInitialState()) // ✅ fresh object, set fires +```