BREAKING CHANGE: all store properties (`id`, `state`, `patch`, `subscribe`, and `reset`) are now prefixed with `$` to allow properties defined with the same type and avoid types breaking. Tip: you can refactor your whole codebase with F2 (or right-click + Refactor) on each of the store's properties
main.counter++
```
-or call the method `patch` that allows you apply multiple changes at the same time with a partial `state` object:
+or call the method `$patch` that allows you apply multiple changes at the same time with a partial `state` object:
```ts
-main.patch({
+main.$patch({
counter: -1,
name: 'Abalam',
})
```
-The main difference here is that `patch` allows you to group multiple changes into one single entry in the devtools.
+The main difference here is that `$patch` allows you to group multiple changes into one single entry in the devtools.
### Replacing the `state`
},
setFoo(foo: string) {
- this.patch({ nested: { foo } })
+ this.$patch({ nested: { foo } })
},
combined() {
actions: {
swap() {
const bStore = useB()
- const b = bStore.state.b
- bStore.state.b = this.state.a
- this.state.a = b
+ const b = bStore.$state.b
+ bStore.$state.b = this.$state.a
+ this.$state.a = b
},
},
})
it('can use the store as this', () => {
const store = useStore()
- expect(store.state.a).toBe(true)
+ expect(store.$state.a).toBe(true)
store.toggle()
- expect(store.state.a).toBe(false)
+ expect(store.$state.a).toBe(false)
})
it('store is forced as the context', () => {
const store = useStore()
- expect(store.state.a).toBe(true)
+ expect(store.$state.a).toBe(true)
store.toggle.call(null)
- expect(store.state.a).toBe(false)
+ expect(store.$state.a).toBe(false)
})
it('can call other actions', () => {
const store = useStore()
- expect(store.state.a).toBe(true)
- expect(store.state.nested.foo).toBe('foo')
+ expect(store.$state.a).toBe(true)
+ expect(store.$state.nested.foo).toBe('foo')
store.combined()
- expect(store.state.a).toBe(false)
- expect(store.state.nested.foo).toBe('bar')
+ expect(store.$state.a).toBe(false)
+ expect(store.$state.nested.foo).toBe('bar')
})
it('supports being called between requests', () => {
// simulate a different request
setActiveReq(req2)
const bStore = useB()
- bStore.state.b = 'c'
+ bStore.$state.b = 'c'
aStore.swap()
- expect(aStore.state.a).toBe('b')
+ expect(aStore.$state.a).toBe('b')
// a different instance of b store was used
- expect(bStore.state.b).toBe('c')
+ expect(bStore.$state.b).toBe('c')
})
it('can force the req', () => {
const aStore = useA(req1)
let bStore = useB(req2)
- bStore.state.b = 'c'
+ bStore.$state.b = 'c'
aStore.swap()
- expect(aStore.state.a).toBe('b')
+ expect(aStore.$state.a).toBe('b')
// a different instance of b store was used
- expect(bStore.state.b).toBe('c')
+ expect(bStore.$state.b).toBe('c')
bStore = useB(req1)
- expect(bStore.state.b).toBe('a')
+ expect(bStore.$state.b).toBe('a')
})
})
export const useCartStore = defineStore({
id: 'cart',
state: () => ({
+ id: 2,
rawItems: [] as string[],
}),
getters: {
removeItem(name: string) {
const i = this.rawItems.indexOf(name)
- if (i > -1) this.rawItems.splice(i, 1)
+ if (i > -1) this.$state.rawItems.splice(i, 1)
},
// TODO: use multiple stores
// console.log('Purchasing', this.items)
const n = this.items.length
- this.state.rawItems = []
+ this.rawItems = []
return { amount: n, user: user.name }
},
export function addItem(name: string) {
const store = useCartStore()
- store.state.rawItems.push(name)
+ store.rawItems.push(name)
}
export function removeItem(name: string) {
const store = useCartStore()
- const i = store.state.rawItems.indexOf(name)
- if (i > -1) store.state.rawItems.splice(i, 1)
+ const i = store.rawItems.indexOf(name)
+ if (i > -1) store.rawItems.splice(i, 1)
}
export async function purchaseItems() {
const cart = useCartStore()
const user = useUserStore()
- if (!user.state.name) return
+ if (!user.name) return
console.log('Purchasing', cart.items)
const n = cart.items.length
- cart.state.rawItems = []
+ cart.rawItems = []
return n
}
async login(user: string, password: string) {
const userData = await apiLogin(user, password)
- this.patch({
+ this.$patch({
name: user,
...userData,
})
logout() {
this.login('a', 'b').then(() => {})
- this.patch({
+ this.$patch({
name: '',
isAdmin: false,
})
store.login('e', 'e').then(() => {})
- store.patch({
+ store.$patch({
name: '',
isAdmin: false,
})
* @jest-environment node
*/
import { createPinia } from '../src'
-import { useCartStore } from './pinia/stores/cart'
import { createSSRApp, inject } from 'vue'
import { renderToString, ssrInterpolate } from '@vue/server-renderer'
import { useUserStore } from './pinia/stores/user'
+import { useCartStore } from './pinia/stores/cart'
describe('SSR', () => {
const App = {
import { defineStore, setActiveReq } from '../src'
-describe('store.patch', () => {
+describe('store.$patch', () => {
const useStore = () => {
// create a new store
setActiveReq({})
it('patches a property without touching the rest', () => {
const store = useStore()
- store.patch({ a: false })
- expect(store.state).toEqual({
+ store.$patch({ a: false })
+ expect(store.$state).toEqual({
a: false,
nested: {
foo: 'foo',
it('patches a nested property without touching the rest', () => {
const store = useStore()
- store.patch({ nested: { foo: 'bar' } })
- expect(store.state).toEqual({
+ store.$patch({ nested: { foo: 'bar' } })
+ expect(store.$state).toEqual({
a: true,
nested: {
foo: 'bar',
a: { b: 'string' },
},
})
- store.patch({ nested: { a: { b: 'hello' } } })
- expect(store.state).toEqual({
+ store.$patch({ nested: { a: { b: 'hello' } } })
+ expect(store.$state).toEqual({
a: true,
nested: {
foo: 'bar',
it('patches multiple properties at the same time', () => {
const store = useStore()
- store.patch({ a: false, nested: { foo: 'hello' } })
- expect(store.state).toEqual({
+ store.$patch({ a: false, nested: { foo: 'hello' } })
+ expect(store.$state).toEqual({
a: false,
nested: {
foo: 'hello',
setStateProvider,
} from '../src'
import { mount } from '@vue/test-utils'
+import { getCurrentInstance } from 'vue'
describe('Store', () => {
let req: object
it('sets the initial state', () => {
const store = useStore()
- expect(store.state).toEqual({
+ expect(store.$state).toEqual({
a: true,
nested: {
foo: 'foo',
it('can be reset', () => {
const store = useStore()
- store.state.a = false
+ store.$state.a = false
const spy = jest.fn()
- store.subscribe(spy)
- store.reset()
- store.state.nested.foo = 'bar'
+ store.$subscribe(spy)
+ store.$reset()
+ store.$state.nested.foo = 'bar'
expect(spy).not.toHaveBeenCalled()
- expect(store.state).toEqual({
+ expect(store.$state).toEqual({
a: true,
nested: {
foo: 'bar',
it('can create an empty state if no state option is provided', () => {
const store = defineStore({ id: 'some' })()
- expect(store.state).toEqual({})
+ expect(store.$state).toEqual({})
})
it('can hydrate the state', () => {
const store = useStore()
- expect(store.state).toEqual({
+ expect(store.$state).toEqual({
a: false,
nested: {
foo: 'bar',
it('can replace its state', () => {
const store = useStore()
- store.state = {
+ store.$state = {
a: false,
nested: {
foo: 'bar',
},
},
}
- expect(store.state).toEqual({
+ expect(store.$state).toEqual({
a: false,
nested: {
foo: 'bar',
it('do not share the state between same id store', () => {
const store = useStore()
const store2 = useStore()
- expect(store.state).not.toBe(store2.state)
- store.state.nested.a.b = 'hey'
- expect(store2.state.nested.a.b).toBe('string')
+ expect(store.$state).not.toBe(store2.$state)
+ store.$state.nested.a.b = 'hey'
+ expect(store2.$state.nested.a.b).toBe('string')
})
it('subscribe to changes', () => {
const store = useStore()
const spy = jest.fn()
- store.subscribe(spy)
+ store.$subscribe(spy)
- store.state.a = false
+ store.$state.a = false
expect(spy).toHaveBeenCalledWith(
{
storeName: 'main',
type: expect.stringContaining('in place'),
},
- store.state
+ store.$state
)
})
it('subscribe to changes done via patch', () => {
const store = useStore()
const spy = jest.fn()
- store.subscribe(spy)
+ store.$subscribe(spy)
const patch = { a: false }
- store.patch(patch)
+ store.$patch(patch)
expect(spy).toHaveBeenCalledWith(
{
storeName: 'main',
type: expect.stringContaining('patch'),
},
- store.state
+ store.$state
)
})
if (!store) throw new Error('no store')
const spy = jest.fn()
- store.subscribe(spy)
+ store.$subscribe(spy)
expect(spy).toHaveBeenCalledTimes(0)
store.a = !store.a
store.a = !store.a
expect(spy).toHaveBeenCalledTimes(2)
})
+
+ it.skip('should not break getCurrentInstance', () => {
+ let store: ReturnType<typeof useStore> | undefined
+
+ let i1: any = {}
+ let i2: any = {}
+ const wrapper = mount(
+ {
+ setup() {
+ i1 = getCurrentInstance()
+ store = useStore()
+ i2 = getCurrentInstance()
+
+ return { store }
+ },
+
+ template: `a: {{ store.a }}`,
+ },
+ {
+ global: {
+ plugins: [createPinia()],
+ },
+ }
+ )
+
+ expect(i1 === i2).toBe(true)
+
+ wrapper.unmount()
+ })
})
it('fires callback when patch is applied', () => {
const spy = jest.fn()
- store.subscribe(spy)
- store.state.name = 'Cleiton'
+ store.$subscribe(spy)
+ store.$state.name = 'Cleiton'
expect(spy).toHaveBeenCalledTimes(1)
})
it('unsubscribes callback when unsubscribe is called', () => {
const spy = jest.fn()
- const unsubscribe = store.subscribe(spy)
+ const unsubscribe = store.$subscribe(spy)
unsubscribe()
- store.state.name = 'Cleiton'
+ store.$state.name = 'Cleiton'
expect(spy).not.toHaveBeenCalled()
})
it('listeners are not affected when unsubscribe is called multiple times', () => {
const func1 = jest.fn()
const func2 = jest.fn()
- const unsubscribe1 = store.subscribe(func1)
- store.subscribe(func2)
+ const unsubscribe1 = store.$subscribe(func1)
+ store.$subscribe(func2)
unsubscribe1()
unsubscribe1()
- store.state.name = 'Cleiton'
+ store.$state.name = 'Cleiton'
expect(func1).not.toHaveBeenCalled()
expect(func2).toHaveBeenCalledTimes(1)
})
api.on.inspectComponent((payload, ctx) => {
if (payload.instanceData) {
payload.instanceData.state.push({
- type: '๐ ' + store.id,
+ type: '๐ ' + store.$id,
key: 'state',
editable: false,
- value: store.state,
+ value: store.$state,
})
}
})
api.sendInspectorState(piniaInspectorId)
}
- store.subscribe((mutation, state) => {
+ store.$subscribe((mutation, state) => {
// rootStore.state[store.id] = state
const data: Record<string, any> = {
store: formatDisplay(mutation.storeName),
payload.rootNodes = (payload.filter
? stores.filter((store) =>
- store.id.toLowerCase().includes(payload.filter.toLowerCase())
+ store.$id.toLowerCase().includes(payload.filter.toLowerCase())
)
: stores
).map(formatStoreForInspectorTree)
api.on.getInspectorState((payload) => {
if (payload.app === app && payload.inspectorId === piniaInspectorId) {
const stores = Array.from(getRegisteredStores())
- const store = stores.find((store) => store.id === payload.nodeId)
+ const store = stores.find((store) => store.$id === payload.nodeId)
if (store) {
payload.state = {
// trigger an update so it can display new registered stores
// @ts-ignore
api.notifyComponentUpdate()
- __VUE_DEVTOOLS_TOAST__(`๐ "${store.id}" store installed`)
+ __VUE_DEVTOOLS_TOAST__(`๐ "${store.$id}" store installed`)
}
)
}
function formatStoreForInspectorTree(store: GenericStore): CustomInspectorNode {
return {
- id: store.id,
- label: store.id,
+ id: store.$id,
+ label: store.$id,
tags: [],
}
}
store: GenericStore
): CustomInspectorState[string] {
const fields: CustomInspectorState[string] = [
- { editable: false, key: 'id', value: formatDisplay(store.id) },
- { editable: true, key: 'state', value: store.state },
+ { editable: false, key: 'id', value: formatDisplay(store.$id) },
+ { editable: true, key: 'state', value: store.$state },
]
return fields
import { App, InjectionKey, Plugin } from 'vue'
+import { IS_CLIENT } from './env'
import { NonNullObject, StateTree, GenericStore } from './types'
/**
// forEach is the only one that also works on IE11
stores.forEach((store) => {
- rootState[store.id] = store.state
+ rootState[store.$id] = store.$state
})
return rootState
export interface Pinia {
install: Exclude<Plugin['install'], undefined>
- store<F extends (req?: NonNullObject) => GenericStore>(
- useStore: F
- ): ReturnType<F>
+ store<F extends (...args: any[]) => any>(useStore: F): ReturnType<F>
+}
+
+declare module '@vue/runtime-core' {
+ export interface ComponentCustomProperties {
+ /**
+ * Instantiate a store anywhere
+ */
+ $pinia: Pinia['store']
+ }
}
export const piniaSymbol = (__DEV__
const pinia: Pinia = {
install(app: App) {
app.provide(piniaSymbol, pinia)
- // TODO: strip out if no need for
- setClientApp(app)
+ app.config.globalProperties.$pinia = pinia.store
+ // TODO: write test
+ // only set the app on client
+ if (__BROWSER__ && IS_CLIENT) {
+ setClientApp(app)
+ }
},
store<F extends (req?: NonNullObject) => GenericStore>(
useStore: F
): ReturnType<F> {
- const store = useStore(pinia) as ReturnType<F>
- return store
+ return useStore(pinia) as ReturnType<F>
},
}
G extends Record<string, Method>,
A extends Record<string, Method>
>(
- id: Id,
+ $id: Id,
buildState: () => S = () => ({} as S),
getters: G = {} as G,
actions: A = {} as A,
(state) => {
if (isListening) {
subscriptions.forEach((callback) => {
- callback({ storeName: id, type: '๐งฉ in place', payload: {} }, state)
+ callback({ storeName: $id, type: '๐งฉ in place', payload: {} }, state)
})
}
},
}
)
- function patch(partialState: DeepPartial<S>): void {
+ function $patch(partialState: DeepPartial<S>): void {
isListening = false
innerPatch(state.value, 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 },
+ { storeName: $id, type: 'โคต๏ธ patch', payload: partialState },
state.value
)
})
}
- function subscribe(callback: SubscriptionCallback<S>) {
+ function $subscribe(callback: SubscriptionCallback<S>) {
subscriptions.push(callback)
return () => {
const idx = subscriptions.indexOf(callback)
}
}
- function reset() {
+ function $reset() {
subscriptions = []
state.value = buildState()
}
const storeWithState: StoreWithState<Id, S> = {
- id,
+ $id,
_r,
// @ts-ignore, `reactive` unwraps this making it of type S
- state: computed<S>({
+ $state: computed<S>({
get: () => state.value,
set: (newState) => {
isListening = false
},
}),
- patch,
- subscribe,
- reset,
+ $patch,
+ $subscribe,
+ $reset,
}
const computedGetters: StoreWithGetters<G> = {} as StoreWithGetters<G>
// 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()))
/**
* Unique identifier of the store
*/
- id: Id
+ $id: Id
/**
* State of the Store. Setting it will replace the whole state.
*/
- state: S
+ $state: S
/**
* Private property defining the request key for this store
+ *
+ * @internal
*/
_r: NonNullObject
/**
* Applies a state patch to current state. Allows passing nested values
+ *
* @param partialState - patch to apply to the state
*/
- patch(partialState: DeepPartial<S>): void
+ $patch(partialState: DeepPartial<S>): void
/**
* Resets the store to its initial state by removing all subscriptions and
* building a new state object
*/
- reset(): void
+ $reset(): void
/**
* Setups a callback to be called whenever the state changes.
- * @param callback - callback that is called whenever the state
- * @returns function that removes callback from subscriptions
+ *
+ * @param callback - callback passed to the watcher
+ * @returns function that removes the watcher
*/
- subscribe(callback: SubscriptionCallback<S>): () => void
+ $subscribe(callback: SubscriptionCallback<S>): () => void
}
export type Method = (...args: any[]) => any
const deprecatedStore = useDeprecated()
-expectType<{ a: 'on' | 'off' }>(deprecatedStore.state)
+expectType<{ a: 'on' | 'off' }>(deprecatedStore.$state)
expectType<number>(deprecatedStore.nested.counter)
expectType<'on' | 'off'>(deprecatedStore.a)
let store = useStore()
-// FIXME: this should not be there anymore
-expectType<{ a: 'on' | 'off' }>(store.state)
+expectType<{ a: 'on' | 'off' }>(store.$state)
expectType<number>(store.nested.counter)
expectType<'on' | 'off'>(store.a)