From: Eduardo San Martin Morote Date: Tue, 9 Feb 2021 17:49:15 +0000 (+0100) Subject: refactor: prefix store public properties with `$` X-Git-Tag: v0.2.0~43 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=63c41a446a16d7c694ff6158cfbd6b3545c4dfe0;p=thirdparty%2Fvuejs%2Fpinia.git refactor: prefix store public properties with `$` Close #253. This also aligns with the current v2 API and makes migration easier from Vue 2 to Vue 3. BREAKING CHANGES: 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 + Rename) on each of the store's properties --- diff --git a/README.md b/README.md index 9fdf7c73..9fef74ee 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ const router = new Router({ const main = useMainStore() router.beforeEach((to, from, next) => { - if (main.state.isLoggedIn) next() + if (main.$state.isLoggedIn) next() else next('/login') }) ``` @@ -191,7 +191,7 @@ router.beforeEach((to, from, next) => { // ✅ This will work (requires an extra param for SSR, see below) const main = useMainStore() - if (main.state.isLoggedIn) next() + if (main.$state.isLoggedIn) next() else next('/login') }) ``` @@ -246,23 +246,23 @@ To mutate the state you can either directly change something: 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` Simply set it to a new object; ```ts -main.state = { counter: 666, name: 'Paimon' } +main.$state = { counter: 666, name: 'Paimon' } ``` ### SSR diff --git a/__tests__/actions.spec.ts b/__tests__/actions.spec.ts index a97a7227..f1fdd324 100644 --- a/__tests__/actions.spec.ts +++ b/__tests__/actions.spec.ts @@ -30,7 +30,7 @@ describe('Actions', () => { }, setFoo(foo: string) { - this.patch({ nested: { foo } }) + this.$patch({ nested: { foo } }) }, combined() { @@ -52,34 +52,34 @@ describe('Actions', () => { 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', () => { @@ -91,12 +91,12 @@ describe('Actions', () => { // 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', () => { @@ -105,13 +105,13 @@ describe('Actions', () => { 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') }) }) diff --git a/__tests__/pinia/stores/cart.ts b/__tests__/pinia/stores/cart.ts index f2b3480b..63eb26ca 100644 --- a/__tests__/pinia/stores/cart.ts +++ b/__tests__/pinia/stores/cart.ts @@ -38,23 +38,23 @@ export type CartStore = ReturnType export function addItem(name: string) { const store = useCartStore() - store.state.rawItems.push(name) + store.$state.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.$state.rawItems.indexOf(name) + if (i > -1) store.$state.rawItems.splice(i, 1) } export async function purchaseItems() { const cart = useCartStore() const user = useUserStore() - if (!user.state.name) return + if (!user.$state.name) return console.log('Purchasing', cart.items.value) const n = cart.items.value.length - cart.state.rawItems = [] + cart.$state.rawItems = [] return n } diff --git a/__tests__/pinia/stores/user.ts b/__tests__/pinia/stores/user.ts index 33f15fdf..28b250d5 100644 --- a/__tests__/pinia/stores/user.ts +++ b/__tests__/pinia/stores/user.ts @@ -15,7 +15,7 @@ export const useUserStore = defineStore({ async login(user: string, password: string) { const userData = await apiLogin(user, password) - this.patch({ + this.$patch({ name: user, ...userData, }) @@ -24,7 +24,7 @@ export const useUserStore = defineStore({ logout() { this.login('a', 'b').then(() => {}) - this.patch({ + this.$patch({ name: '', isAdmin: false, }) @@ -44,7 +44,7 @@ export function logout() { store.login('e', 'e').then(() => {}) - store.patch({ + store.$patch({ name: '', isAdmin: false, }) diff --git a/__tests__/ssr/app/App.ts b/__tests__/ssr/app/App.ts index 5699a4d2..d4fbfc04 100644 --- a/__tests__/ssr/app/App.ts +++ b/__tests__/ssr/app/App.ts @@ -4,21 +4,21 @@ import { useStore } from './store' export default defineComponent({ async serverPrefetch() { const store = useStore() - store.state.counter++ + store.$state.counter++ }, setup() { const store = useStore() - const doubleCount = computed(() => store.state.counter * 2) + const doubleCount = computed(() => store.$state.counter * 2) function increment() { - store.state.counter++ + store.$state.counter++ } return { doubleCount, increment, - state: store.state, + state: store.$state, } }, diff --git a/__tests__/store.patch.spec.ts b/__tests__/store.patch.spec.ts index a8927f38..25b68b9f 100644 --- a/__tests__/store.patch.spec.ts +++ b/__tests__/store.patch.spec.ts @@ -18,8 +18,8 @@ describe('store.patch', () => { 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', @@ -30,16 +30,16 @@ describe('store.patch', () => { 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', @@ -50,8 +50,8 @@ describe('store.patch', () => { 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', diff --git a/__tests__/store.spec.ts b/__tests__/store.spec.ts index 9b1b0367..a3dbc301 100644 --- a/__tests__/store.spec.ts +++ b/__tests__/store.spec.ts @@ -20,7 +20,7 @@ describe('Store', () => { it('sets the initial state', () => { const store = useStore() - expect(store.state).toEqual({ + expect(store.$state).toEqual({ a: true, nested: { foo: 'foo', @@ -31,13 +31,13 @@ describe('Store', () => { 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', @@ -49,7 +49,7 @@ describe('Store', () => { 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', () => { @@ -77,7 +77,7 @@ describe('Store', () => { const store = useStore() - expect(store.state).toEqual({ + expect(store.$state).toEqual({ a: false, nested: { foo: 'bar', @@ -88,7 +88,7 @@ describe('Store', () => { it('can replace its state', () => { const store = useStore() - store.state = { + store.$state = { a: false, nested: { foo: 'bar', @@ -97,7 +97,7 @@ describe('Store', () => { }, }, } - expect(store.state).toEqual({ + expect(store.$state).toEqual({ a: false, nested: { foo: 'bar', @@ -109,17 +109,17 @@ describe('Store', () => { 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( { @@ -127,17 +127,17 @@ describe('Store', () => { 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( { @@ -145,7 +145,7 @@ describe('Store', () => { storeName: 'main', type: expect.stringContaining('patch'), }, - store.state + store.$state ) }) }) diff --git a/__tests__/subscriptions.spec.ts b/__tests__/subscriptions.spec.ts index 3b426c69..859f3cc2 100644 --- a/__tests__/subscriptions.spec.ts +++ b/__tests__/subscriptions.spec.ts @@ -19,27 +19,27 @@ describe('Subscriptions', () => { 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) }) diff --git a/__tests__/tds/store.test-d.ts b/__tests__/tds/store.test-d.ts index ebee55ba..67db2a6b 100644 --- a/__tests__/tds/store.test-d.ts +++ b/__tests__/tds/store.test-d.ts @@ -13,7 +13,7 @@ const useStore = defineStore({ const store = useStore() -expectType<{ a: 'on' | 'off' }>(store.state) +expectType<{ a: 'on' | 'off' }>(store.$state) expectType<{ upper: string }>(store) diff --git a/src/devtools.ts b/src/devtools.ts index afddc245..4be13c2a 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -57,26 +57,26 @@ export function useStoreDevtools(store: StoreWithState) { devtoolHook.emit('vuex:init', rootStore) } - rootStore.state[store.id] = store.state + rootStore.state[store.$id] = store.$state // tell the devtools we added a module - rootStore.registerModule(store.id, store) + rootStore.registerModule(store.$id, store) - Object.defineProperty(rootStore.state, store.id, { - get: () => store.state, - set: (state) => (store.state = state), + Object.defineProperty(rootStore.state, store.$id, { + get: () => store.$state, + set: (state) => (store.$state = state), }) // Vue.set(rootStore.state, store.name, store.state) // the trailing slash is removed by the devtools - rootStore._modulesNamespaceMap[store.id + '/'] = true + rootStore._modulesNamespaceMap[store.$id + '/'] = true devtoolHook.on('vuex:travel-to-state', (targetState) => { - store.state = targetState[store.id] + store.$state = targetState[store.$id] }) - store.subscribe((mutation, state) => { - rootStore.state[store.id] = state + store.$subscribe((mutation, state) => { + rootStore.state[store.$id] = state devtoolHook.emit( 'vuex:mutation', { diff --git a/src/rootStore.ts b/src/rootStore.ts index 85a23c69..09ed006c 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -50,7 +50,7 @@ export function getRootState(req: NonNullObject): Record { // 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 diff --git a/src/store.ts b/src/store.ts index 78708e45..7ecc7fff 100644 --- a/src/store.ts +++ b/src/store.ts @@ -66,24 +66,24 @@ export function buildStore< G extends Record, A extends Record >( - id: Id, + $id: Id, buildState = () => ({} as S), getters: G = {} as G, actions: A = {} as A, initialState?: S | undefined ): Store { - const state: Ref = ref(initialState || buildState()) + const $state: Ref = ref(initialState || buildState()) const _r = getActiveReq() let isListening = true let subscriptions: SubscriptionCallback[] = [] watch( - () => state.value, + () => $state.value, (state) => { if (isListening) { subscriptions.forEach((callback) => { - callback({ storeName: id, type: '🧩 in place', payload: {} }, state) + callback({ storeName: $id, type: '🧩 in place', payload: {} }, state) }) } }, @@ -93,20 +93,20 @@ export function buildStore< } ) - function patch(partialState: DeepPartial): void { + function $patch(partialState: DeepPartial): void { isListening = false - innerPatch(state.value, partialState) + 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 }, - state.value + { storeName: $id, type: '⤵️ patch', payload: partialState }, + $state.value ) }) } - function subscribe(callback: SubscriptionCallback) { + function $subscribe(callback: SubscriptionCallback) { subscriptions.push(callback) return () => { const idx = subscriptions.indexOf(callback) @@ -116,27 +116,27 @@ export function buildStore< } } - function reset() { + function $reset() { subscriptions = [] - state.value = buildState() + $state.value = buildState() } const storeWithState: StoreWithState = { - id, + $id, _r, // @ts-ignore, `reactive` unwraps this making it of type S - state: computed({ - get: () => state.value, + $state: computed({ + get: () => $state.value, set: (newState) => { isListening = false - state.value = newState + $state.value = newState isListening = true }, }), - patch, - subscribe, - reset, + $patch, + $subscribe, + $reset, } const computedGetters: StoreWithGetters = {} as StoreWithGetters @@ -165,7 +165,7 @@ export function buildStore< const store: Store = reactive({ ...storeWithState, // using this means no new properties can be added as state - ...toComputed(state), + ...toComputed($state), ...computedGetters, ...wrappedActions, }) as Store diff --git a/src/types.ts b/src/types.ts index ede3d79a..bebea62c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -import { Ref } from '@vue/composition-api' - export type StateTree = Record export function isPlainObject( @@ -16,8 +14,6 @@ export function isPlainObject( export type NonNullObject = Record -type TODO = any -// type StoreMethod = TODO export type DeepPartial = { [K in keyof T]?: DeepPartial } // type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } @@ -30,12 +26,12 @@ export interface StoreWithState { /** * 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 @@ -44,22 +40,22 @@ export interface StoreWithState { /** * Applies a state patch to current state. Allows passing nested values - * @param partialState patch to apply to the state + * @param partialState - patch to apply to the state */ - patch(partialState: DeepPartial): void + $patch(partialState: DeepPartial): 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 + * @param callback - callback that is called whenever the state * @returns function that removes callback from subscriptions */ - subscribe(callback: SubscriptionCallback): () => void + $subscribe(callback: SubscriptionCallback): () => void } export type Method = (...args: any[]) => any