Skip to content

Conversation

@markerikson
Copy link
Collaborator

@markerikson markerikson commented Dec 28, 2025

This PR:

  • Fixes an issue with getPath being broken in our pre-minified CJS/ESM production builds (but not the main immer.mjs artifact, which is minified at app build time)
  • Fixes an issue where a new plain object containing a draft value is inserted into a Map or Set and never gets finalized properly
  • Rewrites vitest.build.config.ts to ensure that it does actually run against a real minified production artifact
  • Rewrites the setup for arrayTraps to use for..in for setup instead of each(), to work around an odd issue with each() somehow not existing as proxy.ts is being executed in the production build
  • Updates package.json to only rebuild the library when test:build is executed, as the previous use of "pretest": "yarn build" meant that it didn't get run if you were specifically executing yarn test:build.

Fixes #1199
Fixes #1200

Background

After shipping RTK 2.11 with Immer 11, , a user reported an issue with RTKQ optimistic updates not working right when updating a nested array. They also said this only happened in prod builds:

After a bunch of discussion, we finally got a repro of that issue, and the repro showed that the patch contents had a very different path calculated when it failed.

Meanwhile, another user reported what they thought was a related problem, but turned out to be completely different. This involved updating nested Maps and Sets.

Fixes

After investigation, I found that:

  • Array patches not working: Immer's ESBuild config tries to minify "private" fields like key_, but the logic for patch path lookup had a if ("key_" in state) check. That check failed when key_ got renamed.
  • Map/Set handling: the repro takes an existing DraftSet instance, puts it in a new plain object, and puts the plain object into a DraftMap. That effectively cuts the chain of assignments, and we never tried to fix up the DraftSet in the plain object. We already have a handleCrossReference method that does this, it just wasn't being called inside of DraftMap.set() or DraftSet.add() (and also needed to be updated to handle working with Sets correctly)

Note that this meant that patches worked for most apps, if the build system picked up the normal immer.mjs artifact (which is unminified, has embedded NODE_ENV values, and will get minified by the app's build step). The problem was if the build system picked our our pre-minified artifacts (immer.cjs.production.js or immer.production.mjs), where ESBuild had already mangled the field names. Turns out that Expo does this, at least some of the time.

In the process of working on this, I also found that my attempt to port the "production build" tests to Vitest wasn't working right - it wasn't actually loading the production artifact, so the tests were just running against the source code still. I was able to fix that problem by updating the alias settings to be more specific.

While working on that, I also tweaked the test:build script to actually contain the build command.

Finally, while running the prod tests, I saw this very surprising error:

 FAIL  __tests__/map-set.js [ __tests__/map-set.js ]
TypeError: each is not a function
 ❯ src/core/proxy.ts:259:1
    257|
    258| const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
    259| each(objectTraps, (key, fn) => {
       | ^
    260|  // @ts-ignore
    261|  arrayTraps[key] = function() {
 ❯ src/internal.ts:25:31

I actually still have no explanation for why this is happening. Looking at the prod artifacts, I see a minified version of each(), it exists in scope, and it ought to work fine. But whatever, changing this to be a for..in loop works and passes tests.

@coveralls
Copy link

coveralls commented Dec 28, 2025

Pull Request Test Coverage Report for Build 20547109483

Details

  • 24 of 24 (100.0%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.07%) to 42.941%

Totals Coverage Status
Change from base Build 20465308647: 0.07%
Covered Lines: 1687
Relevant Lines: 4781

💛 - Coveralls

@mweststrate
Copy link
Collaborator

Looking at the prod artifacts, I see a minified version of each(), it exists in scope, and it ought to work fine.

Curious, was this minified to a const lambda? That might not hoist correctly like functions.


function getPath(state: ImmerState, path: PatchPath = []): PatchPath | null {
// Step 1: Check if state has a stored key
if ("key_" in state && state.key_ !== undefined) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, I should have spotted this in review

@mweststrate
Copy link
Collaborator

Thanks for following up on these!

@mweststrate mweststrate merged commit b208d58 into main Dec 28, 2025
2 checks passed
@markerikson
Copy link
Collaborator Author

@mweststrate I see a v11.1.1 tag here, but no release on NPM yet. Need to kick the bot manually?

@mweststrate
Copy link
Collaborator

Ok, scrambled the setup a bit more and triggered a new 11.1.2 release, that one should contain all changes now.

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

Labels

None yet

Projects

None yet

4 participants