Skip to content

Commit fa11794

Browse files
committed
feat(hmr): extend acceptHMRUpdate to allow for cleaning up side effects of existing store
1 parent 2974e20 commit fa11794

File tree

4 files changed

+135
-5
lines changed

4 files changed

+135
-5
lines changed

packages/docs/cookbook/hot-module-replacement.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
Pinia supports Hot Module replacement so you can edit your stores and interact with them directly in your app without reloading the page, allowing you to keep the existing state, add, or even remove state, actions, and getters.
44

5-
At the moment, only [Vite](https://vitejs.dev/guide/api-hmr.html#hmr-api) is officially supported but any bundler implementing the `import.meta.hot` spec should work (e.g. [webpack](https://webpack.js.org/api/module-variables/#importmetawebpackhot) seems to use `import.meta.webpackHot` instead of `import.meta.hot`).
6-
You need to add this snippet of code next to any store declaration. Let's say you have three stores: `auth.js`, `cart.js`, and `chat.js`, you will have to add (and adapt) this after the creation of the _store definition_:
5+
At the moment, only [Vite](https://vitejs.dev/guide/api-hmr.html#hmr-api) is officially supported but any bundler implementing the `import.meta.hot` spec should work (e.g. [webpack](https://webpack.js.org/api/module-variables/#importmetawebpackhot) seems to use `import.meta.webpackHot` instead of `import.meta.hot`).
6+
You need to add this snippet of code next to any store declaration. Let's say you have three stores: `auth.js`, `chat.js`, and `scroll.js`, you will have to add (and adapt) this after the creation of the _store definition_:
77

88
```js
99
// auth.js
@@ -18,3 +18,41 @@ if (import.meta.hot) {
1818
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
1919
}
2020
```
21+
22+
You can pass a cleanup function as an optional third argument in order to clean up side effects of the existing store before initializing the new store. This is useful if you have event listeners or other side effects that need to be cleaned up.
23+
24+
```js
25+
// scroll.js
26+
import { defineStore, acceptHMRUpdate } from 'pinia'
27+
import { ref } from 'vue'
28+
29+
export const useScroll = defineStore('scroll', () => {
30+
const scrollTop = ref(window.scrollY)
31+
32+
function onScroll () {
33+
scrollTop.value = window.scrollY
34+
}
35+
36+
function trackScroll () {
37+
window.addEventListener('scroll', onScroll, { passive: true })
38+
}
39+
40+
trackScroll()
41+
42+
function $cleanUp () {
43+
window.removeEventListener('scroll', onScroll)
44+
}
45+
46+
return {
47+
scrollTop,
48+
trackScroll,
49+
$cleanUp,
50+
}
51+
})
52+
53+
if (import.meta.hot) {
54+
import.meta.hot.accept(acceptHMRUpdate(useScroll, import.meta.hot, (existingStore) => {
55+
existingStore.$cleanUp()
56+
}))
57+
}
58+
```

packages/pinia/src/hmr.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isPlainObject,
55
StateTree,
66
StoreDefinition,
7+
Store,
78
StoreGeneric,
89
_GettersTree,
910
_Method,
@@ -77,13 +78,18 @@ export function patchObject(
7778
*
7879
* @param initialUseStore - return of the defineStore to hot update
7980
* @param hot - `import.meta.hot`
81+
* @param cleanup - function to clean up side effects of the existing store
8082
*/
8183
export function acceptHMRUpdate<
8284
Id extends string = string,
8385
S extends StateTree = StateTree,
8486
G extends _GettersTree<S> = _GettersTree<S>,
8587
A = _ActionsTree,
86-
>(initialUseStore: StoreDefinition<Id, S, G, A>, hot: any) {
88+
>(
89+
initialUseStore: StoreDefinition<Id, S, G, A>,
90+
hot: any,
91+
cleanup?: (existingStore: Store<Id, S, G, A>) => void
92+
) {
8793
// strip as much as possible from iife.prod
8894
if (!__DEV__) {
8995
return () => {}
@@ -111,15 +117,20 @@ export function acceptHMRUpdate<
111117
console.warn(
112118
`The id of the store changed from "${initialUseStore.$id}" to "${id}". Reloading.`
113119
)
114-
// return import.meta.hot.invalidate()
115120
return hot.invalidate()
116121
}
117122

118123
const existingStore: StoreGeneric = pinia._s.get(id)!
119124
if (!existingStore) {
120-
console.log(`[Pinia]: skipping hmr because store doesn't exist yet`)
125+
console.log(`[🍍]: Skipping HMR because store doesn't exist yet`)
121126
return
122127
}
128+
129+
// allow the old store to clean up side effects
130+
if (typeof cleanup === 'function') {
131+
cleanup(existingStore as Store<Id, S, G, A>)
132+
}
133+
123134
useStore(pinia, existingStore)
124135
}
125136
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defineStore, acceptHMRUpdate } from 'pinia'
2+
import { onScopeDispose, ref } from 'vue'
3+
4+
export const useScroll = defineStore('scroll', () => {
5+
const scrollTop = ref(window.scrollY)
6+
7+
function onScroll() {
8+
scrollTop.value = window.scrollY
9+
}
10+
11+
function trackScroll() {
12+
window.addEventListener('scroll', onScroll, { passive: true })
13+
}
14+
15+
trackScroll()
16+
17+
function $cleanUp() {
18+
console.log('Cleaning up old scroll event listeners')
19+
window.removeEventListener('scroll', onScroll)
20+
}
21+
22+
// if someone wants the scroll tracking only to happen on a certain route,
23+
// one can dispose the store before leaving the route.
24+
onScopeDispose(() => {
25+
console.log('onScopeDispose')
26+
$cleanUp()
27+
})
28+
29+
return {
30+
scrollTop,
31+
trackScroll,
32+
$cleanUp,
33+
}
34+
})
35+
36+
if (import.meta.hot) {
37+
import.meta.hot.accept(
38+
acceptHMRUpdate(useScroll, import.meta.hot, (existingStore) => {
39+
console.log('HMR update')
40+
existingStore.$cleanUp()
41+
})
42+
)
43+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts" setup>
2+
import { onBeforeUnmount, onMounted } from 'vue'
3+
import { getActivePinia } from 'pinia';
4+
import { onBeforeRouteLeave } from 'vue-router';
5+
import { useScroll } from '../stores/scroll'
6+
7+
const bodyStyle = document.body.style
8+
const appStyle = document.getElementById('app')!.style
9+
10+
const scrollStore = useScroll()
11+
12+
onMounted(() => {
13+
bodyStyle.setProperty('height', '300vh')
14+
appStyle.setProperty('position', 'sticky')
15+
appStyle.setProperty('top', '0')
16+
})
17+
18+
onBeforeUnmount(() => {
19+
bodyStyle.removeProperty('height')
20+
appStyle.removeProperty('position')
21+
appStyle.removeProperty('top')
22+
})
23+
24+
onBeforeRouteLeave(() => {
25+
scrollStore.$dispose()
26+
const pinia = getActivePinia()
27+
delete pinia!.state.value[scrollStore.$id]
28+
})
29+
</script>
30+
31+
<template>
32+
<div style="position: sticky; top: 0;">
33+
<h2>Scroll Store</h2>
34+
<p><strong>Scroll top:</strong> {{ scrollStore.scrollTop }}</p>
35+
<p>During development, after saving changes in <code>/stores/scroll.ts</code>, the <code>acceptHMRUpdate</code> function is configured to run the <code>$cleanUp</code> function on the existing store just before the new store is initialized.</p>
36+
<p>You can only verify this manually by making changes in <code>/stores/scroll.ts</code> and checking what scroll event listeners are on the <code>&lt;html&gt;</code> element. There should always only be one.</p>
37+
</div>
38+
</template>

0 commit comments

Comments
 (0)