Skip to content

fix: preserve structural sharing for no-op array-methods calls - #1289

Open
maximilliangrand wants to merge 1 commit into
immerjs:mainfrom
maximilliangrand:fix/array-methods-noop-structural-sharing
Open

fix: preserve structural sharing for no-op array-methods calls#1289
maximilliangrand wants to merge 1 commit into
immerjs:mainfrom
maximilliangrand:fix/array-methods-noop-structural-sharing

Conversation

@maximilliangrand

Copy link
Copy Markdown

Problem

With enableArrayMethods() enabled, mutating array methods that change nothing still produce a new state reference, breaking Immer's structural-sharing guarantee that a change-free producer returns the base.

enableArrayMethods()
const base = {list: [1, 2, 3]}
const next = produce(base, d => d.list.push(...[])) // empty spread
next === base // false  (expected: true)

Same for splice(i, 0), and pop()/shift() on an empty array. The common real-world trigger is draft.list.push(...incoming) when incoming happens to be empty, which silently invalidates memoization and forces re-renders. Native Immer (plugin disabled) returns the base in all these cases.

Cause

The plugin routes every mutating method through executeArrayMethod, which calls markChanged unconditionally, bypassing the no-op detection that core's set trap performs.

Fix

Detect these no-ops in O(1) and skip marking the array changed, so the producer returns the base and emits no patches, matching core behavior. Real mutations and their patches are unchanged.

sort()/reverse() are left untouched: they are genuine reorder operations where cheap no-op detection isn't possible.

Verification

  • New tests run in both plugin-enabled and native modes; they fail on baseline (source reverted) and pass with the fix.
  • Full src suite and build-config suite pass; prettier --check clean.

The enableArrayMethods() plugin routed every mutating array method through
executeArrayMethod, which unconditionally called markChanged. As a result
calls that change nothing - push()/unshift() with no arguments (e.g. the
common draft.list.push(...items) with an empty items), splice(i, 0), and
pop()/shift() on an empty array - produced a new state reference, breaking
the structural-sharing guarantee that native Immer keeps (produce returning
the base when nothing changed).

Detect these no-ops in O(1) and skip marking the array changed, so the
producer returns the base and emits no patches, matching core behavior.

Signed-off-by: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant