diff --git a/packages/docs/cookbook/hot-module-replacement.md b/packages/docs/cookbook/hot-module-replacement.md index af5d6bd5fa..2fce8fc1b4 100644 --- a/packages/docs/cookbook/hot-module-replacement.md +++ b/packages/docs/cookbook/hot-module-replacement.md @@ -2,8 +2,8 @@ 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. -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`). -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_: +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`). +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_: ```js // auth.js @@ -18,3 +18,41 @@ if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot)) } ``` + +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. + +```js +// scroll.js +import { defineStore, acceptHMRUpdate } from 'pinia' +import { ref } from 'vue' + +export const useScroll = defineStore('scroll', () => { + const scrollTop = ref(window.scrollY) + + function onScroll () { + scrollTop.value = window.scrollY + } + + function trackScroll () { + window.addEventListener('scroll', onScroll, { passive: true }) + } + + trackScroll() + + function $cleanUp () { + window.removeEventListener('scroll', onScroll) + } + + return { + scrollTop, + trackScroll, + $cleanUp, + } +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useScroll, import.meta.hot, (existingStore) => { + existingStore.$cleanUp() + })) +} +``` \ No newline at end of file diff --git a/packages/pinia/src/hmr.ts b/packages/pinia/src/hmr.ts index b5798d49e9..2e3d268032 100644 --- a/packages/pinia/src/hmr.ts +++ b/packages/pinia/src/hmr.ts @@ -4,6 +4,7 @@ import { isPlainObject, StateTree, StoreDefinition, + Store, StoreGeneric, _GettersTree, _Method, @@ -77,13 +78,18 @@ export function patchObject( * * @param initialUseStore - return of the defineStore to hot update * @param hot - `import.meta.hot` + * @param cleanup - function to clean up side effects of the existing store */ export function acceptHMRUpdate< Id extends string = string, S extends StateTree = StateTree, G extends _GettersTree = _GettersTree, A = _ActionsTree, ->(initialUseStore: StoreDefinition, hot: any) { +>( + initialUseStore: StoreDefinition, + hot: any, + cleanup?: (existingStore: Store) => void +) { // strip as much as possible from iife.prod if (!__DEV__) { return () => {} @@ -111,15 +117,20 @@ export function acceptHMRUpdate< console.warn( `The id of the store changed from "${initialUseStore.$id}" to "${id}". Reloading.` ) - // return import.meta.hot.invalidate() return hot.invalidate() } const existingStore: StoreGeneric = pinia._s.get(id)! if (!existingStore) { - console.log(`[Pinia]: skipping hmr because store doesn't exist yet`) + console.log(`[🍍]: Skipping HMR because store doesn't exist yet`) return } + + // allow the old store to clean up side effects + if (typeof cleanup === 'function') { + cleanup(existingStore as Store) + } + useStore(pinia, existingStore) } } diff --git a/packages/playground/src/stores/scroll.ts b/packages/playground/src/stores/scroll.ts new file mode 100644 index 0000000000..5a002bbe4e --- /dev/null +++ b/packages/playground/src/stores/scroll.ts @@ -0,0 +1,43 @@ +import { defineStore, acceptHMRUpdate } from 'pinia' +import { onScopeDispose, ref } from 'vue' + +export const useScroll = defineStore('scroll', () => { + const scrollTop = ref(window.scrollY) + + function onScroll() { + scrollTop.value = window.scrollY + } + + function trackScroll() { + window.addEventListener('scroll', onScroll, { passive: true }) + } + + trackScroll() + + function $cleanUp() { + console.log('Cleaning up old scroll event listeners') + window.removeEventListener('scroll', onScroll) + } + + // if someone wants the scroll tracking only to happen on a certain route, + // one can dispose the store before leaving the route. + onScopeDispose(() => { + console.log('onScopeDispose') + $cleanUp() + }) + + return { + scrollTop, + trackScroll, + $cleanUp, + } +}) + +if (import.meta.hot) { + import.meta.hot.accept( + acceptHMRUpdate(useScroll, import.meta.hot, (existingStore) => { + console.log('HMR update') + existingStore.$cleanUp() + }) + ) +} diff --git a/packages/playground/src/views/ScrollStore.vue b/packages/playground/src/views/ScrollStore.vue new file mode 100644 index 0000000000..806debca44 --- /dev/null +++ b/packages/playground/src/views/ScrollStore.vue @@ -0,0 +1,38 @@ + + +