From: Eduardo San Martin Morote Date: Thu, 31 Dec 2020 14:18:07 +0000 (+0100) Subject: fix: correct lifespan of stores X-Git-Tag: v2.0.0-alpha.6~3 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=483335c6660d593cf33468c1ab8c95da82cc392a;p=thirdparty%2Fvuejs%2Fpinia.git fix: correct lifespan of stores Fix #255 BREAKING CHANGE: `setActiveReq()` has been renamed to `setActivePinia()`. And now receives the application's pinia as the first parameter instead of an arbitrary object (like a Node http request). **This affects particularily users doing SSR** but also enables them to write universal code. --- diff --git a/README.md b/README.md index a82ff243..e4b938ea 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ export const useMainStore = defineStore({ // use getters in other getters doubleCountPlusOne() { return this.doubleCount * 2 - } + }, }, // optional actions actions: { @@ -143,9 +143,9 @@ export default defineComponent({ }) ``` -Note: the SSR implementation is yet to be decided on Pinia, but if you intend having SSR on your application, you should avoid using `useStore` functions at the root level of a file to make sure the correct store is retrieved for your request. Here is an example: +Note: the SSR implementation on Pinia might change, but if you intend having SSR on your application, you should avoid using `useStore` functions at the root level of a file to make sure the correct store is retrieved for your currently running application instance. Here is an example: -**Avoid doing this\***: +**Avoid doing this**: ```ts import { createRouter } from 'vue-router' @@ -175,10 +175,13 @@ export default defineComponent({ }) // In a different file... +const pinia = createPinia() +app.use(pinia) router.beforeEach((to) => { - // ✅ This will work (requires an extra param for SSR, see below) - const main = useMainStore() + // ✅ This will work (requires pinia param when outside of setup on both + // Client and Server. See the SSR section below for more information) + const main = useMainStore(pinia) if (to.meta.requiresAuth && !main.isLoggedIn) return '/login' }) diff --git a/__tests__/actions.spec.ts b/__tests__/actions.spec.ts index f1fdd324..7e90bdec 100644 --- a/__tests__/actions.spec.ts +++ b/__tests__/actions.spec.ts @@ -1,9 +1,9 @@ -import { defineStore, setActiveReq } from '../src' +import { createPinia, defineStore, setActivePinia } from '../src' describe('Actions', () => { const useStore = () => { // create a new store - setActiveReq({}) + setActivePinia(createPinia()) return defineStore({ id: 'main', state: () => ({ @@ -82,14 +82,14 @@ describe('Actions', () => { expect(store.$state.nested.foo).toBe('bar') }) - it('supports being called between requests', () => { - const req1 = {} - const req2 = {} - setActiveReq(req1) + it('supports being called between piniauests', () => { + const pinia1 = createPinia() + const pinia2 = createPinia() + setActivePinia(pinia1) const aStore = useA() - // simulate a different request - setActiveReq(req2) + // simulate a different piniauest + setActivePinia(pinia2) const bStore = useB() bStore.$state.b = 'c' @@ -99,19 +99,19 @@ describe('Actions', () => { expect(bStore.$state.b).toBe('c') }) - it('can force the req', () => { - const req1 = {} - const req2 = {} - const aStore = useA(req1) + it('can force the pinia', () => { + const pinia1 = createPinia() + const pinia2 = createPinia() + const aStore = useA(pinia1) - let bStore = useB(req2) + let bStore = useB(pinia2) bStore.$state.b = 'c' aStore.swap() expect(aStore.$state.a).toBe('b') // a different instance of b store was used expect(bStore.$state.b).toBe('c') - bStore = useB(req1) + bStore = useB(pinia1) expect(bStore.$state.b).toBe('a') }) }) diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts index 29aac184..2e066a67 100644 --- a/__tests__/getters.spec.ts +++ b/__tests__/getters.spec.ts @@ -1,9 +1,9 @@ -import { defineStore, setActiveReq } from '../src' +import { createPinia, defineStore, setActivePinia } from '../src' describe('Getters', () => { const useStore = () => { // create a new store - setActiveReq({}) + setActivePinia(createPinia()) return defineStore({ id: 'main', state: () => ({ @@ -52,14 +52,14 @@ describe('Getters', () => { expect(store.upperCaseName).toBe('ED') }) - it('supports changing between requests', () => { - const req1 = {} - const req2 = {} - setActiveReq(req1) + it('supports changing between piniauests', () => { + const pinia1 = createPinia() + const pinia2 = createPinia() + setActivePinia(pinia1) const aStore = useA() - // simulate a different request - setActiveReq(req2) + // simulate a different piniauest + setActivePinia(pinia2) const bStore = useB() bStore.b = 'c' diff --git a/__tests__/lifespan.spec.ts b/__tests__/lifespan.spec.ts new file mode 100644 index 00000000..0e25e93b --- /dev/null +++ b/__tests__/lifespan.spec.ts @@ -0,0 +1,105 @@ +import { createPinia, defineStore, setActivePinia } from '../src' +import { mount } from '@vue/test-utils' +import { watch, nextTick, ref } from 'vue' + +describe('Store Lifespan', () => { + function defineMyStore() { + return defineStore({ + id: 'main', + state: () => ({ + a: true, + n: 0, + nested: { + foo: 'foo', + a: { b: 'string' }, + }, + }), + getters: { + double() { + return this.n * 2 + }, + notA() { + return !this.a + }, + }, + }) + } + + const pinia = createPinia() + // let pinia: object + + // const useStore = () => { + // // create a new store + // pinia = {} + // setActivePinia(pinia) + // return defineMyStore()() + // } + + it('bug report', async () => { + const inComponentWatch = jest.fn() + + const n = ref(0) + + const wrapper = mount( + { + render: () => null, + setup() { + watch(() => n.value, inComponentWatch) + n.value++ + }, + }, + { + global: { + plugins: [pinia], + }, + } + ) + + await wrapper.unmount() + + expect(inComponentWatch).toHaveBeenCalledTimes(1) + + // store!.n++ + n.value++ + await nextTick() + expect(inComponentWatch).toHaveBeenCalledTimes(1) + }) + + it('state reactivity outlives component life', async () => { + const useStore = defineMyStore() + setActivePinia(createPinia()) + + const inComponentWatch = jest.fn() + + let store: ReturnType + + const n = ref(0) + + const wrapper = mount( + { + render: () => null, + setup() { + store = useStore() + // watch(() => store.n, inComponentWatch) + watch(() => n.value, inComponentWatch) + store.n++ + n.value++ + }, + }, + { + global: { + plugins: [pinia], + }, + } + ) + + await wrapper.unmount() + + expect(inComponentWatch).toHaveBeenCalledTimes(1) + + // store!.n++ + n.value++ + await nextTick() + expect(inComponentWatch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/__tests__/rootState.spec.ts b/__tests__/rootState.spec.ts index 5c9519ae..6a8e056f 100644 --- a/__tests__/rootState.spec.ts +++ b/__tests__/rootState.spec.ts @@ -1,4 +1,4 @@ -import { defineStore, getRootState } from '../src' +import { createPinia, defineStore, getRootState } from '../src' describe('Root State', () => { const useA = defineStore({ @@ -12,35 +12,35 @@ describe('Root State', () => { }) it('works with no stores', () => { - expect(getRootState({})).toEqual({}) + expect(getRootState(createPinia())).toEqual({}) }) it('retrieves the root state of one store', () => { - const req = {} - useA(req) - expect(getRootState(req)).toEqual({ + const pinia = createPinia() + useA(pinia) + expect(getRootState(pinia)).toEqual({ a: { a: 'a' }, }) }) - it('does not mix up different requests', () => { - const req1 = {} - const req2 = {} - useA(req1) - useB(req2) - expect(getRootState(req1)).toEqual({ + it('does not mix up different piniauests', () => { + const pinia1 = createPinia() + const pinia2 = createPinia() + useA(pinia1) + useB(pinia2) + expect(getRootState(pinia1)).toEqual({ a: { a: 'a' }, }) - expect(getRootState(req2)).toEqual({ + expect(getRootState(pinia2)).toEqual({ b: { b: 'b' }, }) }) it('can hold multiple stores', () => { - const req1 = {} - useA(req1) - useB(req1) - expect(getRootState(req1)).toEqual({ + const pinia1 = createPinia() + useA(pinia1) + useB(pinia1) + expect(getRootState(pinia1)).toEqual({ a: { a: 'a' }, b: { b: 'b' }, }) diff --git a/__tests__/state.spec.ts b/__tests__/state.spec.ts index 3a3ba497..57349134 100644 --- a/__tests__/state.spec.ts +++ b/__tests__/state.spec.ts @@ -1,10 +1,10 @@ -import { defineStore, setActiveReq } from '../src' -import { computed } from 'vue' +import { createPinia, defineStore, setActivePinia } from '../src' +import { computed, nextTick, watch } from 'vue' describe('State', () => { const useStore = () => { // create a new store - setActiveReq({}) + setActivePinia(createPinia()) return defineStore({ id: 'main', state: () => ({ @@ -28,4 +28,25 @@ describe('State', () => { store.name = 'Ed' expect(upperCased.value).toBe('ED') }) + + // it('watch', () => { + // setActivePinia(createPinia()) + // defineStore({ + // id: 'main', + // state: () => ({ + // name: 'Eduardo', + // counter: 0, + // }), + // })() + // }) + + it('state can be watched', async () => { + const store = useStore() + const spy = jest.fn() + watch(() => store.name, spy) + expect(spy).not.toHaveBeenCalled() + store.name = 'Ed' + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + }) }) diff --git a/__tests__/store.patch.spec.ts b/__tests__/store.patch.spec.ts index b3a1abdb..4469a45b 100644 --- a/__tests__/store.patch.spec.ts +++ b/__tests__/store.patch.spec.ts @@ -1,9 +1,9 @@ -import { defineStore, setActiveReq } from '../src' +import { createPinia, defineStore, setActivePinia } from '../src' describe('store.$patch', () => { const useStore = () => { // create a new store - setActiveReq({}) + setActivePinia(createPinia()) return defineStore({ id: 'main', state: () => ({ diff --git a/__tests__/store.spec.ts b/__tests__/store.spec.ts index dde45999..601c16f6 100644 --- a/__tests__/store.spec.ts +++ b/__tests__/store.spec.ts @@ -1,18 +1,19 @@ import { createPinia, defineStore, - setActiveReq, + setActivePinia, setStateProvider, + Pinia, } from '../src' import { mount } from '@vue/test-utils' -import { getCurrentInstance } from 'vue' +import { getCurrentInstance, nextTick, watch } from 'vue' describe('Store', () => { - let req: object + let pinia: Pinia const useStore = () => { // create a new store - req = {} - setActiveReq(req) + pinia = createPinia() + setActivePinia(pinia) return defineStore({ id: 'main', state: () => ({ @@ -60,7 +61,7 @@ describe('Store', () => { }) it('can hydrate the state', () => { - setActiveReq({}) + setActivePinia(createPinia()) const useStore = defineStore({ id: 'main', state: () => ({ @@ -156,43 +157,50 @@ describe('Store', () => { ) }) - it('should outlive components', () => { - let store: ReturnType | undefined + it('should outlive components', async () => { + const pinia = createPinia() + const useStore = defineStore({ + id: 'main', + state: () => ({ n: 0 }), + }) const wrapper = mount( { setup() { - store = useStore() + const store = useStore() return { store } }, - template: `a: {{ store.a }}`, + template: `n: {{ store.n }}`, }, { global: { - plugins: [createPinia()], + plugins: [pinia], }, } ) - expect(wrapper.html()).toBe('a: true') + expect(wrapper.html()).toBe('n: 0') - if (!store) throw new Error('no store') + const store = useStore(pinia) const spy = jest.fn() - store.$subscribe(spy) + watch(() => store.n, spy) expect(spy).toHaveBeenCalledTimes(0) - store.a = !store.a + store.n++ + await nextTick() expect(spy).toHaveBeenCalledTimes(1) + expect(wrapper.html()).toBe('n: 1') - wrapper.unmount() - store.a = !store.a + await wrapper.unmount() + store.n++ + await nextTick() expect(spy).toHaveBeenCalledTimes(2) }) - it.skip('should not break getCurrentInstance', () => { + it('should not break getCurrentInstance', () => { let store: ReturnType | undefined let i1: any = {} diff --git a/__tests__/subscriptions.spec.ts b/__tests__/subscriptions.spec.ts index 859f3cc2..e1bd2b80 100644 --- a/__tests__/subscriptions.spec.ts +++ b/__tests__/subscriptions.spec.ts @@ -1,9 +1,9 @@ -import { defineStore, setActiveReq } from '../src' +import { createPinia, defineStore, setActivePinia } from '../src' describe('Subscriptions', () => { const useStore = () => { // create a new store - setActiveReq({}) + setActivePinia(createPinia()) return defineStore({ id: 'main', state: () => ({ diff --git a/old test ssr/app/main.ts b/old test ssr/app/main.ts index ea8fa362..dab1f199 100644 --- a/old test ssr/app/main.ts +++ b/old test ssr/app/main.ts @@ -3,20 +3,21 @@ import Vue from 'vue' import App from './App' import { useStore } from './store' import { setActiveReq } from '../../../src' +import { createPinia } from '../../src' // Done in setup.ts // Vue.use(VueCompositionApi) export function createApp() { // create router and store instances - setActiveReq({}) + setActiveReq(createPinia()) const store = useStore() store.state.counter++ // create the app instance, injecting both the router and the store const app = new Vue({ - render: h => h(App), + render: (h) => h(App), }) // expose the app, the router and the store. diff --git a/src/devtools.ts b/src/devtools.ts index 29f86c9b..28d7f7a9 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -5,7 +5,7 @@ import { } from '@vue/devtools-api' import { App } from 'vue' import { getRegisteredStores, registerStore } from './rootStore' -import { GenericStore, NonNullObject } from './types' +import { GenericStore } from './types' function formatDisplay(display: string) { return { @@ -17,7 +17,7 @@ function formatDisplay(display: string) { let isAlreadyInstalled: boolean | undefined -export function addDevtools(app: App, store: GenericStore, req: NonNullObject) { +export function addDevtools(app: App, store: GenericStore) { registerStore(store) setupDevtoolsPlugin( { diff --git a/src/index.ts b/src/index.ts index e14d3744..2a82b601 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ export { - setActiveReq, + setActivePinia, setStateProvider, getRootState, createPinia, + Pinia, } from './rootStore' export { defineStore } from './store' export { createStore } from './deprecated' diff --git a/src/rootStore.ts b/src/rootStore.ts index f99d17b2..9ad60984 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -1,23 +1,43 @@ -import { App, InjectionKey, Plugin } from 'vue' +import { App, InjectionKey, Plugin, Ref, ref, warn } from 'vue' import { IS_CLIENT } from './env' -import { NonNullObject, StateTree, GenericStore } from './types' +import { + StateTree, + GenericStore, + StoreWithState, + StateDescriptor, +} from './types' /** - * setActiveReq must be called to handle SSR at the top of functions like `fetch`, `setup`, `serverPrefetch` and others + * setActivePinia must be called to handle SSR at the top of functions like + * `fetch`, `setup`, `serverPrefetch` and others */ -export let activeReq: NonNullObject = {} -export const setActiveReq = (req: NonNullObject | undefined) => - req && (activeReq = req) +export let activePinia: Pinia | undefined +export const setActivePinia = (pinia: Pinia | undefined) => + (activePinia = pinia) + +export const getActivePinia = () => { + if (__DEV__ && !activePinia) { + warn( + `[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n\n` + + `const pinia = createPinia()\n` + + `app.use(pinia)\n\n` + + `This will fail in production.` + ) + } -export const getActiveReq = () => activeReq + return activePinia! +} /** * The api needs more work we must be able to use the store easily in any * function by calling `useStore` to get the store Instance and we also need to - * be able to reset the store instance between requests on the server + * be able to reset the store instance between piniauests on the server */ -export const storesMap = new WeakMap>() +export const storesMap = new WeakMap< + Pinia, + Map, StateDescriptor]> +>() /** * A state provider allows to set how states are stored for hydration. e.g. setting a property on a context, getting a property from window @@ -29,33 +49,24 @@ interface StateProvider { /** * Map of initial states used for hydration */ -export const stateProviders = new WeakMap() +export const stateProviders = new WeakMap() export function setStateProvider(stateProvider: StateProvider) { - stateProviders.set(getActiveReq(), stateProvider) + stateProviders.set(getActivePinia(), stateProvider) } export function getInitialState(id: string): StateTree | undefined { - const provider = stateProviders.get(getActiveReq()) + const provider = stateProviders.get(getActivePinia()) return provider && provider()[id] } /** * Gets the root state of all active stores. This is useful when reporting an application crash by * retrieving the problematic state and send it to your error tracking service. - * @param req - request key + * @param pinia - piniauest key */ -export function getRootState(req: NonNullObject): Record { - const stores = storesMap.get(req) - if (!stores) return {} - const rootState = {} as Record - - // forEach is the only one that also works on IE11 - stores.forEach((store) => { - rootState[store.$id] = store.$state - }) - - return rootState +export function getRootState(pinia: Pinia): Record { + return pinia.state.value } /** @@ -67,15 +78,19 @@ export const getClientApp = () => clientApp export interface Pinia { install: Exclude - store any>(useStore: F): ReturnType + + /** + * root state + */ + state: Ref } declare module '@vue/runtime-core' { export interface ComponentCustomProperties { /** - * Instantiate a store anywhere + * Access to the application's Pinia */ - $pinia: Pinia['store'] + $pinia: Pinia } } @@ -84,21 +99,20 @@ export const piniaSymbol = (__DEV__ : Symbol()) as InjectionKey export function createPinia(): Pinia { + const state = ref({}) + const pinia: Pinia = { install(app: App) { app.provide(piniaSymbol, pinia) - app.config.globalProperties.$pinia = pinia.store + app.config.globalProperties.$pinia = pinia // TODO: write test // only set the app on client if (__BROWSER__ && IS_CLIENT) { setClientApp(app) } }, - store GenericStore>( - useStore: F - ): ReturnType { - return useStore(pinia) as ReturnType - }, + + state, } return pinia diff --git a/src/store.ts b/src/store.ts index 2cfbb43d..f9648d2b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,12 +1,4 @@ -import { - ref, - watch, - computed, - Ref, - reactive, - inject, - getCurrentInstance, -} from 'vue' +import { watch, computed, Ref, inject, getCurrentInstance, reactive } from 'vue' import { StateTree, StoreWithState, @@ -16,19 +8,20 @@ import { StoreWithGetters, Store, StoreWithActions, + StateDescriptor, Method, } from './types' import { - getActiveReq, - setActiveReq, + getActivePinia, + setActivePinia, storesMap, getInitialState, getClientApp, piniaSymbol, + Pinia, } from './rootStore' import { addDevtools } from './devtools' import { IS_CLIENT } from './env' -import { withScope } from './withScope' function innerPatch( target: T, @@ -49,16 +42,26 @@ function innerPatch( return target } -function toComputed(refObject: Ref) { +/** + * Create an object of computed properties referring to + * + * @param rootStateRef - pinia.state + * @param id - unique name + */ +function computedFromState( + rootStateRef: Ref>, + id: Id +) { // let asComputed = computed() const reactiveObject = {} as { [k in keyof T]: Ref } - for (const key in refObject.value) { + const state = rootStateRef.value[id] + for (const key in state) { // @ts-ignore: the key matches reactiveObject[key] = computed({ - get: () => refObject.value[key as keyof T], - set: (value) => (refObject.value[key as keyof T] = value), + get: () => rootStateRef.value[id][key as keyof T], + set: (value) => (rootStateRef.value[id][key as keyof T] = value), }) } @@ -66,118 +69,155 @@ function toComputed(refObject: Ref) { } /** - * Creates a store instance + * Creates a store with its state object. This is meant to be augmented with getters and actions + * * @param id - unique identifier of the store, like a name. eg: main, cart, user + * @param buildState - function to build the initial state * @param initialState - initial state applied to the store, Must be correctly typed to infer typings */ -export function buildStore< - Id extends string, - S extends StateTree, - G extends Record, - A extends Record ->( +function initStore( $id: Id, buildState: () => S = () => ({} as S), - getters: G = {} as G, - actions: A = {} as A, initialState?: S | undefined -): Store { - const state: Ref = ref(initialState || buildState()) - // TODO: remove req part? - const _r = getActiveReq() +): [StoreWithState, { get: () => S; set: (newValue: S) => void }] { + const _p = getActivePinia() + _p.state.value[$id] = initialState || buildState() + // const state: Ref = toRef(_p.state.value, $id) let isListening = true let subscriptions: SubscriptionCallback[] = [] - watch( - () => state.value, - (state) => { - if (isListening) { - subscriptions.forEach((callback) => { - callback({ storeName: $id, type: '🧩 in place', payload: {} }, state) - }) - } - }, - { - deep: true, - flush: 'sync', - } - ) - function $patch(partialState: DeepPartial): void { isListening = false - innerPatch(state.value, partialState) + innerPatch(_p.state.value[$id], partialState) isListening = true // because we paused the watcher, we need to manually call the subscriptions subscriptions.forEach((callback) => { callback( { storeName: $id, type: '⤵️ patch', payload: partialState }, - state.value + _p.state.value[$id] ) }) } function $subscribe(callback: SubscriptionCallback) { subscriptions.push(callback) + + // watch here to link the subscription to the current active instance + // e.g. inside the setup of a component + const stopWatcher = watch( + () => _p.state.value[$id], + (state) => { + if (isListening) { + subscriptions.forEach((callback) => { + callback( + { storeName: $id, type: '🧩 in place', payload: {} }, + state + ) + }) + } + }, + { + deep: true, + flush: 'sync', + } + ) + return () => { const idx = subscriptions.indexOf(callback) if (idx > -1) { subscriptions.splice(idx, 1) + stopWatcher() } } } function $reset() { subscriptions = [] - state.value = buildState() + _p.state.value[$id] = buildState() } const storeWithState: StoreWithState = { $id, - _r, - // @ts-ignore, `reactive` unwraps this making it of type S - $state: computed({ - get: () => state.value, - set: (newState) => { - isListening = false - state.value = newState - isListening = true - }, - }), + _p, + + // $state is added underneath $patch, $subscribe, $reset, - } + } as StoreWithState + + return [ + storeWithState, + { + get: () => _p.state.value[$id] as S, + set: (newState: S) => { + isListening = false + _p.state.value[$id] = newState + isListening = true + }, + }, + ] +} + +/** + * Creates a store bound to the lifespan of where the function is called. This + * means creating the store inside of a component's setup will bound it to the + * lifespan of that component while creating it outside of a component will + * create an ever living store + * + * @param partialStore - store with state returned by initStore + * @param descriptor - descriptor to setup $state property + * @param $id - unique name of the store + * @param getters - getters of the store + * @param actions - actions of the store + */ +function buildStoreToUse< + Id extends string, + S extends StateTree, + G extends Record, + A extends Record +>( + partialStore: StoreWithState, + descriptor: StateDescriptor, + $id: Id, + getters: G = {} as G, + actions: A = {} as A +) { + const _p = getActivePinia() const computedGetters: StoreWithGetters = {} as StoreWithGetters for (const getterName in getters) { computedGetters[getterName] = computed(() => { - setActiveReq(_r) + setActivePinia(_p) // eslint-disable-next-line @typescript-eslint/no-use-before-define return getters[getterName].call(store, store) }) as StoreWithGetters[typeof getterName] } - // const reactiveGetters = reactive(computedGetters) - const wrappedActions: StoreWithActions = {} as StoreWithActions for (const actionName in actions) { wrappedActions[actionName] = function () { - setActiveReq(_r) + setActivePinia(_p) // eslint-disable-next-line return actions[actionName].apply(store, (arguments as unknown) as any[]) } as StoreWithActions[typeof actionName] } const store: Store = reactive({ - ...storeWithState, + ...partialStore, // using this means no new properties can be added as state - ...toComputed(state), + ...computedFromState(_p.state, $id), ...computedGetters, ...wrappedActions, }) as Store + // use this instead of a computed with setter to be able to create it anywhere + // without linking the computed lifespan to wherever the store is first + // created. + Object.defineProperty(store, '$state', descriptor) + return store } @@ -202,29 +242,29 @@ export function defineStore< }) { const { id, state, getters, actions } = options - return function useStore(reqKey?: object | null): Store { + return function useStore(pinia?: Pinia | null): Store { // avoid injecting if `useStore` when not possible - reqKey = reqKey || (getCurrentInstance() && inject(piniaSymbol)) - if (reqKey) setActiveReq(reqKey) - // TODO: worth warning on server if no reqKey as it can leak data - const req = getActiveReq() - let stores = storesMap.get(req) - if (!stores) storesMap.set(req, (stores = new Map())) - - let store = stores.get(id) as Store - if (!store) { - stores.set( + pinia = pinia || (getCurrentInstance() && inject(piniaSymbol)) + if (pinia) setActivePinia(pinia) + // TODO: worth warning on server if no piniaKey as it can leak data + pinia = getActivePinia() + let stores = storesMap.get(pinia) + if (!stores) storesMap.set(pinia, (stores = new Map())) + + let storeAndDescriptor = stores.get(id) as + | [StoreWithState, StateDescriptor] + | undefined + if (!storeAndDescriptor) { + storeAndDescriptor = initStore(id, state, getInitialState(id)) + + stores.set(id, storeAndDescriptor) + + const store = buildStoreToUse( + storeAndDescriptor[0], + storeAndDescriptor[1], id, - (store = withScope( - () => - buildStore( - id, - state, - getters as Record | undefined, - actions as Record | undefined, - getInitialState(id) - ) as Store - )) + getters as Record | undefined, + actions as Record | undefined ) if ( @@ -234,7 +274,7 @@ export function defineStore< ) { const app = getClientApp() if (app) { - addDevtools(app, store, req) + addDevtools(app, store) } else if (!isDevWarned && !__TEST__) { isDevWarned = true console.warn( @@ -246,8 +286,16 @@ export function defineStore< ) } } + + return store } - return store + return buildStoreToUse( + storeAndDescriptor[0], + storeAndDescriptor[1], + id, + getters as Record | undefined, + actions as Record | undefined + ) } } diff --git a/src/types.ts b/src/types.ts index cb5e70fc..a8a418ed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,16 @@ import { Ref } from 'vue' +import { Pinia } from './rootStore' export type StateTree = Record +/** + * Object descriptor for Object.defineProperty + */ +export interface StateDescriptor { + get(): S + set(newValue: S): void +} + export function isPlainObject( // eslint-disable-next-line @typescript-eslint/no-explicit-any o: any @@ -14,8 +23,6 @@ export function isPlainObject( ) } -export type NonNullObject = Record - export interface StoreGetter { (state: S, getters: Record>): T } @@ -40,11 +47,11 @@ export interface StoreWithState { $state: S /** - * Private property defining the request key for this store + * Private property defining the pinia the store is attached to. * * @internal */ - _r: NonNullObject + _p: Pinia /** * Applies a state patch to current state. Allows passing nested values diff --git a/src/withScope.ts b/src/withScope.ts deleted file mode 100644 index 86315138..00000000 --- a/src/withScope.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createApp } from 'vue' -import { IS_CLIENT } from './env' - -export function withScope(factory: () => T): T { - if (__BROWSER__ && IS_CLIENT) { - let store: T - createApp({ - setup() { - store = factory() - return () => null - }, - }).mount(document.createElement('div')) - // TODO: collect apps to be unmounted when the main app is unmounted - return store! - } else { - // no need to wrap with an app on SSR - return factory() - } -}