Skip to content

Commit 7bb7733

Browse files
authored
feat: access the state and getters through this (#190)
BREAKING CHANGE: there is no longer a `state` property on the store, you need to directly access it. `getters` no longer receive parameters, directly call `this.myState` to read state and other getters
1 parent cae8fca commit 7bb7733

File tree

10 files changed

+196
-98
lines changed

10 files changed

+196
-98
lines changed

README.md

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ There are the core principles that I try to achieve with this experiment:
1818

1919
- Flat modular structure 🍍 No nesting, only stores, compose them as needed
2020
- Light layer on top of Vue 💨 keep it very lightweight
21-
- Only `state`, `getters` 👐 `patch` is the new _mutation_
21+
- Only `state`, `getters`
22+
- No more verbose mutations, 👐 `patch` is _the mutation_
2223
- Actions are like _methods_ ⚗️ Group your business there
2324
- Import what you need, let webpack code split 📦 No need for dynamically registered modules
2425
- SSR support ⚙️
@@ -101,15 +102,19 @@ export const useMainStore = createStore({
101102
}),
102103
// optional getters
103104
getters: {
104-
doubleCount: (state, getters) => state.counter * 2,
105+
doubleCount() {
106+
return this.counter * 2,
107+
},
105108
// use getters in other getters
106-
doubleCountPlusOne: (state, { doubleCount }) => doubleCount.value * 2,
109+
doubleCountPlusOne() {
110+
return this.doubleCount * 2
111+
}
107112
},
108113
// optional actions
109114
actions: {
110115
reset() {
111116
// `this` is the store instance
112-
this.state.counter = 0
117+
this.counter = 0
113118
},
114119
},
115120
})
@@ -127,10 +132,10 @@ export default defineComponent({
127132
return {
128133
// gives access to the whole store
129134
main,
130-
// gives access to the state
131-
state: main.state,
132-
// gives access to specific getter; like `computed` properties, do not include `.value`
133-
doubleCount: main.doubleCount,
135+
// gives access only to specific state
136+
state: computed(() => main.counter),
137+
// gives access to specific getter; like `computed` properties
138+
doubleCount: computed(() => main.doubleCount),
134139
}
135140
},
136141
})
@@ -193,20 +198,31 @@ router.beforeEach((to, from, next) => {
193198

194199
⚠️: Note that if you are developing an SSR application, [you will need to do a bit more](#ssr).
195200

196-
Once you have access to the store, you can access the `state` through `store.state` and any getter directly on the `store` itself as a _computed_ property (from `@vue/composition-api`) (meaning you need to use `.value` to read the actual value on the JavaScript but not in the template):
201+
You can access any property defined in `state` and `getters` directly on the store, similar to `data` and `computed` properties in a Vue component.
197202

198203
```ts
199204
export default defineComponent({
200205
setup() {
201206
const main = useMainStore()
202-
const text = main.state.name
203-
const doubleCount = main.doubleCount.value // notice the `.value` at the end
207+
const text = main.name
208+
const doubleCount = main.doubleCount
204209
return {}
205210
},
206211
})
207212
```
208213

209-
`state` is the result of a `ref` while every getter is the result of a `computed`. Both from `@vue/composition-api`.
214+
The `main` store in an object wrapped with `reactive`, meaning there is no need to write `.value` after getters but, like `props` in `setup`, we cannot destructure it:
215+
216+
```ts
217+
export default defineComponent({
218+
setup() {
219+
// ❌ This won't work because it breaks reactivity
220+
// it's the same as destructuring from `props`
221+
const { name, doubleCount } = useMainStore()
222+
return { name, doubleCount }
223+
},
224+
})
225+
```
210226

211227
Actions are invoked like methods:
212228

@@ -227,7 +243,7 @@ export default defineComponent({
227243
To mutate the state you can either directly change something:
228244

229245
```ts
230-
main.state.counter++
246+
main.counter++
231247
```
232248

233249
or call the method `patch` that allows you apply multiple changes at the same time with a partial `state` object:
@@ -291,7 +307,7 @@ export default {
291307
}
292308
```
293309

294-
Note: **This is necessary in middlewares and other asyncronous methods**
310+
Note: **This is necessary in middlewares and other asynchronous methods**.
295311

296312
It may look like things are working even if you don't pass `req` to `useStore` **but multiple concurrent requests to the server could end up sharing state between different users**.
297313

@@ -344,18 +360,18 @@ createStore({
344360
id: 'cart',
345361
state: () => ({ items: [] }),
346362
getters: {
347-
message: state => {
363+
message() {
348364
const user = useUserStore()
349-
return `Hi ${user.state.name}, you have ${items.length} items in the cart`
365+
return `Hi ${user.name}, you have ${this.items.length} items in the cart`
350366
},
351367
},
352368
actions: {
353369
async purchase() {
354370
const user = useUserStore()
355371

356-
await apiBuy(user.state.token, this.state.items)
372+
await apiBuy(user.token, this.items)
357373

358-
this.state.items = []
374+
this.items = []
359375
},
360376
},
361377
})
@@ -386,7 +402,7 @@ export const useSharedStore = createStore({
386402
const user = useUserStore()
387403
const cart = useCartStore()
388404

389-
return `Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`
405+
return `Hi ${user.name}, you have ${cart.list.length} items in your cart. It costs ${cart.price}.`
390406
},
391407
},
392408
})
@@ -410,7 +426,7 @@ export const useSharedStore = createStore({
410426
const cart = useCartStore()
411427

412428
try {
413-
await apiOrderCart(user.state.token, cart.state.items)
429+
await apiOrderCart(user.token, cart.items)
414430
cart.emptyCart()
415431
} catch (err) {
416432
displayError(err)
@@ -438,13 +454,14 @@ export const useCartUserStore = pinia(
438454
},
439455
{
440456
getters: {
441-
combinedGetter: ({ user, cart }) =>
442-
`Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`,
457+
combinedGetter () {
458+
return `Hi ${this.user.name}, you have ${this.cart.list.length} items in your cart. It costs ${this.cart.price}.`,
459+
}
443460
},
444461
actions: {
445462
async orderCart() {
446463
try {
447-
await apiOrderCart(this.user.state.token, this.cart.state.items)
464+
await apiOrderCart(this.user.token, this.cart.items)
448465
this.cart.emptyCart()
449466
} catch (err) {
450467
displayError(err)

__tests__/actions.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStore, setActiveReq } from '../src'
22

3-
describe('Store', () => {
3+
describe('Actions', () => {
44
const useStore = () => {
55
// create a new store
66
setActiveReq({})
@@ -13,9 +13,20 @@ describe('Store', () => {
1313
a: { b: 'string' },
1414
},
1515
}),
16+
getters: {
17+
nonA(): boolean {
18+
return !this.a
19+
},
20+
otherComputed() {
21+
return this.nonA
22+
},
23+
},
1624
actions: {
25+
async getNonA() {
26+
return this.nonA
27+
},
1728
toggle() {
18-
this.state.a = !this.state.a
29+
return (this.a = !this.a)
1930
},
2031

2132
setFoo(foo: string) {

__tests__/getters.spec.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStore, setActiveReq } from '../src'
22

3-
describe('Store', () => {
3+
describe('Getters', () => {
44
const useStore = () => {
55
// create a new store
66
setActiveReq({})
@@ -10,9 +10,18 @@ describe('Store', () => {
1010
name: 'Eduardo',
1111
}),
1212
getters: {
13-
upperCaseName: ({ name }) => name.toUpperCase(),
14-
composed: (state, { upperCaseName }) =>
15-
(upperCaseName.value as string) + ': ok',
13+
upperCaseName() {
14+
return this.name.toUpperCase()
15+
},
16+
doubleName() {
17+
return this.upperCaseName
18+
},
19+
composed() {
20+
return this.upperCaseName + ': ok'
21+
},
22+
// TODO: I can't figure out how to pass `this` as an argument. Not sure
23+
// it is possible in this specific scenario
24+
// upperCaseNameArrow: store => store.name,
1625
},
1726
})()
1827
}
@@ -26,24 +35,24 @@ describe('Store', () => {
2635
id: 'A',
2736
state: () => ({ a: 'a' }),
2837
getters: {
29-
fromB(state) {
38+
fromB() {
3039
const bStore = useB()
31-
return state.a + ' ' + bStore.state.b
40+
return this.a + ' ' + bStore.b
3241
},
3342
},
3443
})
3544

3645
it('adds getters to the store', () => {
3746
const store = useStore()
38-
expect(store.upperCaseName.value).toBe('EDUARDO')
39-
store.state.name = 'Ed'
40-
expect(store.upperCaseName.value).toBe('ED')
47+
expect(store.upperCaseName).toBe('EDUARDO')
48+
store.name = 'Ed'
49+
expect(store.upperCaseName).toBe('ED')
4150
})
4251

4352
it('updates the value', () => {
4453
const store = useStore()
45-
store.state.name = 'Ed'
46-
expect(store.upperCaseName.value).toBe('ED')
54+
store.name = 'Ed'
55+
expect(store.upperCaseName).toBe('ED')
4756
})
4857

4958
it('supports changing between requests', () => {
@@ -55,16 +64,16 @@ describe('Store', () => {
5564
// simulate a different request
5665
setActiveReq(req2)
5766
const bStore = useB()
58-
bStore.state.b = 'c'
67+
bStore.b = 'c'
5968

60-
aStore.state.a = 'b'
61-
expect(aStore.fromB.value).toBe('b b')
69+
aStore.a = 'b'
70+
expect(aStore.fromB).toBe('b b')
6271
})
6372

6473
it('can use other getters', () => {
6574
const store = useStore()
66-
expect(store.composed.value).toBe('EDUARDO: ok')
67-
store.state.name = 'Ed'
68-
expect(store.composed.value).toBe('ED: ok')
75+
expect(store.composed).toBe('EDUARDO: ok')
76+
store.name = 'Ed'
77+
expect(store.composed).toBe('ED: ok')
6978
})
7079
})

__tests__/rootState.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStore, getRootState } from '../src'
22

3-
describe('Store', () => {
3+
describe('Root State', () => {
44
const useA = createStore({
55
id: 'a',
66
state: () => ({ a: 'a' }),

__tests__/state.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createStore, setActiveReq } from '../src'
2+
import { computed } from '@vue/composition-api'
3+
4+
describe('State', () => {
5+
const useStore = () => {
6+
// create a new store
7+
setActiveReq({})
8+
return createStore({
9+
id: 'main',
10+
state: () => ({
11+
name: 'Eduardo',
12+
counter: 0,
13+
}),
14+
})()
15+
}
16+
17+
it('can directly access state at the store level', () => {
18+
const store = useStore()
19+
expect(store.name).toBe('Eduardo')
20+
store.name = 'Ed'
21+
expect(store.name).toBe('Ed')
22+
})
23+
24+
it('state is reactive', () => {
25+
const store = useStore()
26+
const upperCased = computed(() => store.name.toUpperCase())
27+
expect(upperCased.value).toBe('EDUARDO')
28+
store.name = 'Ed'
29+
expect(upperCased.value).toBe('ED')
30+
})
31+
})

__tests__/tds/store.test-d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ const useStore = createStore({
55
id: 'name',
66
state: () => ({ a: 'on' as 'on' | 'off' }),
77
getters: {
8-
upper: state => state.a.toUpperCase(),
8+
upper() {
9+
return this.a.toUpperCase()
10+
},
911
},
1012
})
1113

1214
const store = useStore()
1315

1416
expectType<{ a: 'on' | 'off' }>(store.state)
1517

18+
expectType<{ upper: string }>(store)
19+
1620
expectError(() => store.nonExistant)

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { createStore } from './store'
22
export { setActiveReq, setStateProvider, getRootState } from './rootStore'
3-
export { StateTree, StoreGetter, Store } from './types'
3+
export { StateTree, Store } from './types'
44
export { PiniaSsr } from './ssrPlugin'

src/ssrPlugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { VueConstructor } from 'vue/types'
22
import { setActiveReq } from './rootStore'
33
import { SetupContext } from '@vue/composition-api'
44

5-
export const PiniaSsr = (vue: VueConstructor) => {
5+
export const PiniaSsr = (_Vue: VueConstructor) => {
66
const isServer = typeof window === 'undefined'
77

88
if (!isServer) {
@@ -12,14 +12,14 @@ export const PiniaSsr = (vue: VueConstructor) => {
1212
return
1313
}
1414

15-
vue.mixin({
15+
_Vue.mixin({
1616
beforeCreate() {
1717
// @ts-ignore
1818
const { setup, serverPrefetch } = this.$options
1919
if (setup) {
2020
// @ts-ignore
2121
this.$options.setup = (props: any, context: SetupContext) => {
22-
// @ts-ignore
22+
// @ts-ignore TODO: fix usage with nuxt-composition-api https://github.com/posva/pinia/issues/179
2323
if (context.ssrContext) setActiveReq(context.ssrContext.req)
2424
return setup(props, context)
2525
}

0 commit comments

Comments
 (0)