Skip to content

Conversation

@doubledare704
Copy link
Contributor

@doubledare704 doubledare704 commented Sep 14, 2025

  • Add triggerRef import from Vue
  • Implement isShallowRef utility function to detect shallowRef using __v_isShallow
  • Modify method to detect shallowRef targets and trigger reactivity after patching
  • Add comprehensive tests for shallowRef reactivity scenarios
  • Ensure compatibility with existing patch behavior

close #2861

Summary by CodeRabbit

  • Bug Fixes

    • Restored reliable reactivity for shallowRef state when using $patch — nested, partial, multi-field, and direct-assignment updates now consistently notify watchers and computed values; public APIs unchanged.
  • Tests

    • Added comprehensive tests covering shallowRef behavior with object and function patch syntax, nested updates, partial/multi-ref patches, direct assignments, and reactivity observation to prevent regressions.

@netlify
Copy link

netlify bot commented Sep 14, 2025

Deploy Preview for pinia-official canceled.

Name Link
🔨 Latest commit 617b984
🔍 Latest deploy log https://app.netlify.com/projects/pinia-official/deploys/690a35910a3ddd00089b89df

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

Walkthrough

Detects and triggers Vue shallowRef reactivity when store.$patch merges plain objects into shallow refs; adds tests covering shallowRef, markRaw, nested/partial patches, and multiple shallowRefs to validate behavior.

Changes

Cohort / File(s) Summary
ShallowRef reactivity tests
packages/pinia/__tests__/store.patch.spec.ts
Adds imports (vi, shallowRef, markRaw, watch, vitest mock setup) and a new "shallowRef reactivity" test suite exercising $patch with shallow refs, markRaw values, object- and function-syntax patches, nested/partial updates, multiple shallowRefs, and direct assignments.
Store patch implementation
packages/pinia/src/store.ts
Imports isShallow and related types; uses toRaw(target) inside merge logic to inspect underlying values; detects shallow refs (isShallow(rawTarget[key])) and calls triggerRef(rawTarget[key]) after merging plain-object patches into shallow refs; minor formatting and hasOwnProperty/merge adjustments.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor C as Component / Watcher
  participant S as Store (reactive)
  participant P as store.$patch
  participant M as mergeReactiveObjects
  participant Raw as toRaw(store)
  participant SR as shallowRef
  participant V as Vue Reactivity

  C->>S: depend on shallowRef state
  S->>P: call $patch(partialState)
  P->>M: merge partial into reactive store state
  M-->>P: merge complete
  P->>Raw: inspect toRaw(store) for shallowRefs
  alt patched shallowRef found
    P->>SR: call triggerRef(rawTarget[key])
    SR->>V: notify dependents
    V-->>C: watchers/computeds rerun
  else no shallowRef affected
    P-->>S: patch completes without manual trigger
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to:
    • Correct use of toRaw + isShallow and edge cases (markRaw, nested refs).
    • Proper placement and conditions for triggerRef calls to avoid unnecessary triggers.
    • Merge logic correctness for nested/partial merges and hasOwnProperty handling.
    • New tests ensuring behavior across object- and function-syntax patches.

Poem

I twitch my whiskers at a patched abode,
shallow shells nudged, nudges softly spread.
triggerRef drums a bright, small cheering code,
watchers wake and tumble from their bed.
A rabbit hops—tests all pass, joy fed. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing reactivity triggering for shallowRef when using object syntax in $patch.
Linked Issues check ✅ Passed The PR implementation correctly addresses issue #2861 by adding triggerRef logic to detect and trigger reactivity for shallowRef instances during $patch operations with object syntax.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing shallowRef reactivity in $patch operations; no out-of-scope modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/pinia/src/store.ts (2)

96-102: Avoid direct .hasOwnProperty; use the safe prototype call.

target can be a proxy or an object without a prototype. Prefer Object.prototype.hasOwnProperty.call(target, key) to avoid edge cases. The extra guard here is also redundant because isPlainObject(targetValue) already implies “own” in this context.

Apply this diff:

-      target.hasOwnProperty(key) &&
+      Object.prototype.hasOwnProperty.call(target, key) &&

151-158: isShallowRef relies on a private flag; add a type and a defensive check.

Using __v_isShallow is internal API. It’s fine pragmatically, but please (a) narrow the return type to ShallowRef<any> and (b) guard with strict equality to avoid truthy pitfalls.

You can enhance it like this (type-only import outside this hunk):

// import type { ShallowRef } from 'vue'
function isShallowRef(value: any): value is ShallowRef<any> {
  return isRef(value) && (value as any).__v_isShallow === true
}
packages/pinia/__tests__/store.patch.spec.ts (1)

219-401: Great coverage for shallowRef + object‑syntax $patch.

Happy path, nested objects, function syntax baseline, direct assignment baseline, partial updates, and multi‑ref patches are all exercised.

Consider adding:

  • A negative test to ensure watchers don’t fire on unrelated keys in the same $patch.
  • A case where the shallowRef is patched with a non‑plain object (e.g., Date or Array) to assert intended behavior (trigger or not), documenting expectations.

Example to append:

it('does not trigger when patching unrelated key', async () => {
  const store = useShallowRefStore()
  const spy = vi.fn()
  const stop = watchEffect(() => { void store.counter.count; spy() })
  spy.mockClear()
  store.$patch({ nestedCounter: { simple: 123 } })
  await nextTick()
  expect(spy).not.toHaveBeenCalled()
  stop()
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57bec95 and 3cf2466.

📒 Files selected for processing (2)
  • packages/pinia/__tests__/store.patch.spec.ts (2 hunks)
  • packages/pinia/src/store.ts (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/pinia/__tests__/store.patch.spec.ts (1)
packages/pinia/src/store.ts (1)
  • defineStore (878-973)
packages/pinia/src/store.ts (1)
packages/pinia/src/types.ts (1)
  • isPlainObject (19-29)
🔇 Additional comments (2)
packages/pinia/src/store.ts (1)

23-24: Import triggerRef: LGTM.

Needed to manually notify dependents of shallow refs after object‑syntax patches.

packages/pinia/__tests__/store.patch.spec.ts (1)

1-2: Test imports: LGTM.

Covers the added shallowRef scenarios with computed, watchEffect, and nextTick.

@doubledare704
Copy link
Contributor Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

✅ Actions performed

Docstrings generation started.

coderabbitai bot added a commit that referenced this pull request Sep 14, 2025
Docstrings generation was requested by @doubledare704.

* #3041 (comment)

The following files were modified:

* `packages/pinia/src/store.ts`
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

Note

Generated docstrings for this pull request at #3043

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/pinia/src/store.ts (1)

96-103: Use safe hasOwnProperty to avoid prototype edge cases.

Direct calls can throw for objects with null prototypes and are weaker against prototype pollution scenarios.

-      target.hasOwnProperty(key) &&
+      Object.prototype.hasOwnProperty.call(target, key) &&

Optional consistency tweak for the guard above (outside the changed hunk but same function):

-    if (!patchToApply.hasOwnProperty(key)) continue
+    if (!Object.prototype.hasOwnProperty.call(patchToApply, key)) continue
🧹 Nitpick comments (1)
packages/pinia/src/store.ts (1)

528-528: Nit: remove leftover “no‑op” comment.

It’s self‑evident from the code and can be dropped to reduce noise.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cf2466 and 819f21c.

📒 Files selected for processing (1)
  • packages/pinia/src/store.ts (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/pinia/src/store.ts (1)
packages/pinia/src/types.ts (1)
  • isPlainObject (19-29)
🔇 Additional comments (1)
packages/pinia/src/store.ts (1)

323-339: ShallowRef reactivity trigger after object‑syntax patch: LGTM.

Good use of toRaw(store) and own‑key filtering; triggering once per patched shallowRef is correct.

@github-project-automation github-project-automation bot moved this to 🆕 Triaging in Pinia Roadmap Nov 1, 2025
@posva posva moved this from 🆕 Triaging to 📋 Backlog in Pinia Roadmap Nov 1, 2025
doubledare704 and others added 4 commits November 4, 2025 17:39
- Add triggerRef import from Vue
- Implement isShallowRef utility function to detect shallowRef using __v_isShallow
- Modify  method to detect shallowRef targets and trigger reactivity after patching
- Add comprehensive tests for shallowRef reactivity scenarios
- Ensure compatibility with existing patch behavior

close vuejs#2861
- Replace setupStoreRef with toRaw(store) to eliminate coupling and state drift
- Use Object.prototype.hasOwnProperty.call() for safer property checking
- Improve type safety with Ref[] instead of any[]
- Remove unnecessary setupStoreRef assignment and variable declaration
- Maintain same functionality with more robust implementation
- Replace internal __v_isShallow with public isShallow API for Vue version compatibility
- Fix hasOwnProperty usage in mergeReactiveObjects to prevent prototype pollution
- Remove unnecessary no-op comment for cleaner code
- Maintain all existing functionality while improving code quality
@posva posva force-pushed the fix/shallow-ref-patch-reactivity branch from a66b124 to 780fab8 Compare November 4, 2025 16:59
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 4, 2025

Open in StackBlitz

npm i https://pkg.pr.new/@pinia/nuxt@3041
npm i https://pkg.pr.new/pinia@3041
npm i https://pkg.pr.new/@pinia/testing@3041

commit: 617b984

@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.93%. Comparing base (0e9e7e7) to head (617b984).

Additional details and impacted files
@@            Coverage Diff             @@
##               v3    #3041      +/-   ##
==========================================
+ Coverage   90.90%   90.93%   +0.02%     
==========================================
  Files          19       19              
  Lines        1683     1687       +4     
  Branches      237      238       +1     
==========================================
+ Hits         1530     1534       +4     
  Misses        151      151              
  Partials        2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/pinia/__tests__/store.patch.spec.ts (2)

247-290: Remove unnecessary async keywords.

Tests 1-3 are declared as async functions but don't use await anywhere. Since these tests execute synchronously (using flush: 'sync' in watchers), the async keyword is unnecessary and potentially misleading.

Apply this diff to remove the unnecessary async keywords:

-    it('does not trigger reactivity when patching marked raw', async () => {
+    it('does not trigger reactivity when patching marked raw', () => {
       const store = useShallowRefStore()
       // ...
     })

-    it('triggers reactivity when patching shallowRef with object syntax', async () => {
+    it('triggers reactivity when patching shallowRef with object syntax', () => {
       const store = useShallowRefStore()
       // ...
     })

-    it('triggers reactivity when patching nested properties in shallowRef', async () => {
+    it('triggers reactivity when patching nested properties in shallowRef', () => {
       const store = useShallowRefStore()
       // ...
     })

292-357: Remove unnecessary async keywords.

Tests 4-7 (baseline and edge case tests) are declared as async functions but don't use await anywhere. Consistent with the earlier tests, these should not be async since they execute synchronously.

Apply this diff to remove the unnecessary async keywords:

-    it('works with function syntax (baseline test)', async () => {
+    it('works with function syntax (baseline test)', () => {
       const store = useShallowRefStore()
       // ...
     })

-    it('works with direct assignment (baseline test)', async () => {
+    it('works with direct assignment (baseline test)', () => {
       const store = useShallowRefStore()
       // ...
     })

-    it('handles partial updates correctly', async () => {
+    it('handles partial updates correctly', () => {
       const store = useShallowRefStore()
       // ...
     })

-    it('works with multiple shallowRefs in single patch', async () => {
+    it('works with multiple shallowRefs in single patch', () => {
       const store = useShallowRefStore()
       // ...
     })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a66b124 and 780fab8.

📒 Files selected for processing (2)
  • packages/pinia/__tests__/store.patch.spec.ts (2 hunks)
  • packages/pinia/src/store.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/pinia/src/store.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/pinia/__tests__/store.patch.spec.ts (1)
packages/pinia/__tests__/vitest-mock-warn.ts (1)
  • mockWarn (123-125)
🔇 Additional comments (3)
packages/pinia/__tests__/store.patch.spec.ts (3)

1-7: LGTM!

The new imports and mockWarn setup are appropriate for the shallowRef reactivity tests. The mockWarn() call at the describe block level will apply to all tests in the suite.


223-245: LGTM!

The test store setup is well-designed with multiple shallowRef instances to support independent test scenarios and edge cases. The inclusion of markedRaw for testing non-reactive behavior is particularly thoughtful.


222-358: Excellent test coverage for the shallowRef reactivity fix!

The new test suite comprehensively validates the fix for issue #2861. It includes:

✅ Core functionality (object syntax with shallowRef)
✅ Edge cases (markRaw non-reactivity, nested properties, partial updates)
✅ Baseline tests (function syntax and direct assignment still work)
✅ Batch operations (multiple shallowRefs in one patch)

The use of flush: 'sync' ensures synchronous watcher execution for predictable testing, and the spy pattern with mockClear() properly isolates each test from the initial watcher trigger.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 780fab8 and 51a91c0.

📒 Files selected for processing (1)
  • packages/pinia/src/store.ts (6 hunks)
🔇 Additional comments (3)
packages/pinia/src/store.ts (3)

11-56: LGTM! Import changes support the shallowRef fix.

The additions of isShallow and triggerRef enable the core fix, and converting runtime imports to type-only imports where appropriate is a good practice that reduces bundle size.


96-97: Good approach to access raw refs.

Using toRaw(target) allows inspection of the actual ref objects without Vue's automatic unwrapping, which is essential for detecting and triggering shallow refs.


120-124: Core fix correctly triggers shallow ref reactivity.

This logic properly addresses the issue where object-syntax $patch doesn't trigger reactivity for shallow refs:

  1. rawTarget[key] accesses the actual ref object (not unwrapped)
  2. isShallow() detects shallow refs using Vue's public API
  3. triggerRef() manually notifies Vue's reactivity system

The placement after assignment (line 118) is correct because the reactive proxy updates the ref's value, but the ref object itself remains the same, allowing us to trigger it.

Comment on lines +102 to +103
var subPatch = patchToApply[key]
var targetValue = target[key]
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use const instead of var for loop variables.

These variables are not reassigned within the loop body, so const is more appropriate and prevents accidental reassignment. Using var is a code smell in modern JavaScript and provides function scope rather than block scope.

Apply this diff:

-    var subPatch = patchToApply[key]
-    var targetValue = target[key]
+    const subPatch = patchToApply[key]
+    const targetValue = target[key]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var subPatch = patchToApply[key]
var targetValue = target[key]
const subPatch = patchToApply[key]
const targetValue = target[key]
🤖 Prompt for AI Agents
In packages/pinia/src/store.ts around lines 102 to 103, the loop variables are
declared with var (var subPatch = patchToApply[key]; var targetValue =
target[key]) but they are never reassigned; replace var with const for both
declarations to use block-scoped, read-only bindings and avoid accidental
reassignment. Update the two declarations to use const and run tests/lint to
ensure no other references rely on var semantics.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/pinia/__tests__/store.patch.spec.ts (1)

222-354: Consider removing unnecessary async keywords and adding edge case coverage.

The test suite is comprehensive and well-structured. A few optional improvements:

  1. All test functions are declared async but contain no await operations. While harmless, removing async would make the intent clearer.

  2. Consider adding edge case tests for completeness:

    • Patching a shallowRef with null or undefined
    • Patching with primitive values
    • Patching when the patch value itself is a ref or shallowRef

These are nice-to-have improvements; the current coverage validates the core functionality effectively.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51a91c0 and 617b984.

📒 Files selected for processing (1)
  • packages/pinia/__tests__/store.patch.spec.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/pinia/__tests__/store.patch.spec.ts (1)
packages/pinia/__tests__/vitest-mock-warn.ts (1)
  • mockWarn (123-125)
🔇 Additional comments (8)
packages/pinia/__tests__/store.patch.spec.ts (8)

1-7: LGTM! Imports and setup are appropriate.

The new imports (vi, shallowRef, markRaw, watch) and mockWarn setup align well with the test requirements for verifying shallowRef reactivity behavior with watchers.


222-241: LGTM! Store setup provides comprehensive test coverage.

The useShallowRefStore function creates a well-structured test store with:

  • Simple shallowRef (counter)
  • markRaw for negative testing (markedRaw)
  • Nested shallowRef structure (nestedCounter)

This setup effectively supports all the test scenarios in the suite.


243-255: LGTM! markRaw control test is correct.

This negative test correctly verifies that markRaw objects don't trigger reactivity when patched, ensuring the fix doesn't inadvertently affect markRaw behavior.


257-267: LGTM! Core shallowRef reactivity test is sound.

This test directly validates the fix for issue #2861, ensuring that $patch with object syntax correctly triggers reactivity for shallowRef state.


269-286: LGTM! Nested property test validates deep reactivity.

Correctly verifies that patching nested properties within a shallowRef triggers watchers on deeply nested paths.


288-312: LGTM! Baseline tests ensure no regressions.

These tests verify that the fix doesn't break existing functionality:

  • Function syntax patching (already worked)
  • Direct assignment (already worked)

Good defensive testing to catch potential regressions.


314-333: LGTM! Partial update test validates merge behavior.

This test correctly verifies that $patch merges partial objects into shallowRef values rather than replacing them entirely, which aligns with the overall $patch semantics. The explicit comment on Line 327 helps clarify the expected behavior.


335-353: LGTM! Multi-shallowRef test ensures batch handling.

Correctly verifies that a single $patch call triggers reactivity for all affected shallowRefs, ensuring the implementation handles multiple shallowRefs in one batch.

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

Labels

None yet

Projects

Status: 📋 Backlog

Development

Successfully merging this pull request may close these issues.

Mutating a shallowRef object using $patch doesn't trigger reactivity

2 participants