Skip to content

Fix HMR re-run plugins on existing store after HMR to refresh stale closures - #3169

Open
KavehKarami wants to merge 1 commit into
vuejs:v4from
KavehKarami:fix/991-subscriptions-lost-during-hmr
Open

Fix HMR re-run plugins on existing store after HMR to refresh stale closures#3169
KavehKarami wants to merge 1 commit into
vuejs:v4from
KavehKarami:fix/991-subscriptions-lost-during-hmr

Conversation

@KavehKarami

@KavehKarami KavehKarami commented Jul 26, 2026

Copy link
Copy Markdown

close #991

Fix: plugin subscriptions retain stale closures after HMR (#991)

Problem

When a pinia.use() plugin registers a $subscribe or $onAction callback, the callback closes over values from the store module at the time the plugin runs. After HMR fires, the module reloads with updated code, but Pinia was running plugins on the temporary hot store (__hot:) rather than on the existing store. This meant the existing store's plugin subscriptions were never refreshed — they kept stale closures from before the HMR cycle, silently ignoring any logic changes in the new module.

Root cause

In createSetupStore, the plugin application loop ran unconditionally — including when hot = true (i.e., when building the ephemeral hot store used as a diff target). After _hotUpdate applied the diff to the existing store, the hot store was discarded but the existing store's plugin subscriptions were never re-run with the fresh closures from the updated module.

Solution

  • Extract the plugin application loop into a runPlugins() function that wraps all plugin effects in a dedicated child effectScope (pluginScope). This allows old plugin effects to be cleanly stopped and replaced on each HMR cycle.
  • Skip runPlugins() for hot stores — they are temporary scaffolding and don't need plugin subscriptions.
  • Call runPlugins() at the end of _hotUpdate so the existing store's plugin subscriptions are refreshed with closures from the new module version.
  • Skip the watch inside $subscribe for hot stores (they watch a state key that is never set for hot stores, so the watcher is useless).

Tests

Two new regression tests in describe('both') cover the fix:

  • plugin $subscribe uses fresh closures after HMR — simulates a plugin whose $subscribe callback closes over a module-level value; verifies the callback uses the updated value after HMR.
  • plugin $onAction uses fresh closures after HMR — same check for $onAction.

Both tests fail on the unfixed code and pass with the fix. Two additional sanity tests verify that user-level $subscribe and $onAction subscriptions (not added via plugins) survive HMR unchanged.

Summary by CodeRabbit

  • Bug Fixes
    • Improved hot module replacement (HMR) behavior for stores.
    • Preserved store subscriptions and action listeners when modules update.
    • Refreshed plugin registrations after HMR so subscriptions and actions use the latest logic.
    • Prevented unnecessary subscription callbacks during temporary HMR updates.

@netlify

netlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Deploy Preview for pinia-playground ready!

Name Link
🔨 Latest commit f7d5831
🔍 Latest deploy log https://app.netlify.com/projects/pinia-playground/deploys/6a65de085101b90008166dc4
😎 Deploy Preview https://deploy-preview-3169--pinia-playground.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Pinia now preserves subscriptions during HMR and re-runs plugins on existing stores using refreshed effect scopes. HMR tests cover $subscribe, $onAction, and plugin-registered callbacks with updated closures.

Changes

HMR subscriptions and plugins

Layer / File(s) Summary
HMR subscription preservation
packages/pinia/src/store.ts, packages/pinia/__tests__/hmr.spec.ts
Hot stores avoid creating reactive watchers, while tests verify $subscribe and $onAction subscriptions remain active after HMR.
Plugin execution refresh
packages/pinia/src/store.ts, packages/pinia/__tests__/hmr.spec.ts
Plugin execution uses a managed effect scope and re-runs during HMR so plugin callbacks capture updated closures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HMR
  participant PiniaStore
  participant Plugins
  participant Subscriptions
  HMR->>PiniaStore: Apply _hotUpdate
  PiniaStore->>Plugins: Re-run plugins in fresh scope
  Plugins-->>PiniaStore: Return refreshed extensions and callbacks
  PiniaStore->>Subscriptions: Preserve existing subscriptions
  Subscriptions-->>PiniaStore: Trigger updated callbacks
Loading

Possibly related PRs

  • vuejs/pinia#3144: Changes $subscribe watcher creation logic in the same store implementation.

Suggested reviewers: posva, posva

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #991 by preserving and refreshing plugin-registered subscriptions after HMR.
Out of Scope Changes check ✅ Passed The added tests and store refactor are directly tied to the HMR subscription fix and stay in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: re-running plugins on existing stores during HMR to refresh stale closures.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

583-639: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert call counts, not just the last captured value.

Both tests only check the final capturedValue. If runPlugins() leaves the old plugin subscription registered alongside the new one, both callbacks fire and the last-registered one wins, so these assertions still pass while subscriptions silently accumulate on every HMR cycle. Adding a vi.fn() per plugin run (or asserting the number of registered handlers) would actually pin the intended behavior.

🧪 Suggested strengthening
       let closureValue = 'original'
       let capturedValue = ''
+      const handler = vi.fn()
       pinia.use(({ store }) => {
         const current = closureValue // captured at plugin-run time
         store.$subscribe(() => {
+          handler(current)
           capturedValue = current
         })
       })
@@
       store.$patch({ n: 2 })
       // _hotUpdate re-runs plugins on the existing store with the new closure → capturedValue becomes 'updated'
       expect(capturedValue).toBe('updated')
+      // fails if the stale plugin subscription is still registered
+      expect(handler).toHaveBeenCalledTimes(2)
+      expect(handler).toHaveBeenLastCalledWith('updated')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pinia/__tests__/hmr.spec.ts` around lines 583 - 639, Strengthen both
HMR plugin tests around the `pinia.use` callbacks and their
`$subscribe`/`$onAction` registrations by tracking invocation counts with a
fresh `vi.fn()` for each plugin run. After the initial action, assert only the
original handler ran; after HMR and the second action, assert the new handler
ran once and the old handler did not run again, proving subscriptions are
replaced rather than accumulated while retaining the closure-value assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/pinia/__tests__/hmr.spec.ts`:
- Around line 583-639: Strengthen both HMR plugin tests around the `pinia.use`
callbacks and their `$subscribe`/`$onAction` registrations by tracking
invocation counts with a fresh `vi.fn()` for each plugin run. After the initial
action, assert only the original handler ran; after HMR and the second action,
assert the new handler ran once and the old handler did not run again, proving
subscriptions are replaced rather than accumulated while retaining the
closure-value assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28eacffb-6456-4906-b4a7-f2c4abb40c69

📥 Commits

Reviewing files that changed from the base of the PR and between a94de27 and f7d5831.

📒 Files selected for processing (2)
  • packages/pinia/__tests__/hmr.spec.ts
  • packages/pinia/src/store.ts

@KavehKarami KavehKarami changed the title fix(hmr): re-run plugins on existing store after HMR to refresh stale closures Fix HMR re-run plugins on existing store after HMR to refresh stale closures Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🆕 Triaging

Development

Successfully merging this pull request may close these issues.

subscriptions are lost during HMR

1 participant