Skip to content

Commit

Permalink
feat: access the state and getters through this (#190)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
posva committed Sep 22, 2020
1 parent cae8fca commit 7bb7733
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 98 deletions.
63 changes: 40 additions & 23 deletions README.md
Expand Up @@ -18,7 +18,8 @@ There are the core principles that I try to achieve with this experiment:

- Flat modular structure 🍍 No nesting, only stores, compose them as needed
- Light layer on top of Vue 💨 keep it very lightweight
- Only `state`, `getters` 👐 `patch` is the new _mutation_
- Only `state`, `getters`
- No more verbose mutations, 👐 `patch` is _the mutation_
- Actions are like _methods_ ⚗️ Group your business there
- Import what you need, let webpack code split 📦 No need for dynamically registered modules
- SSR support ⚙️
Expand Down Expand Up @@ -101,15 +102,19 @@ export const useMainStore = createStore({
}),
// optional getters
getters: {
doubleCount: (state, getters) => state.counter * 2,
doubleCount() {
return this.counter * 2,
},
// use getters in other getters
doubleCountPlusOne: (state, { doubleCount }) => doubleCount.value * 2,
doubleCountPlusOne() {
return this.doubleCount * 2
}
},
// optional actions
actions: {
reset() {
// `this` is the store instance
this.state.counter = 0
this.counter = 0
},
},
})
Expand All @@ -127,10 +132,10 @@ export default defineComponent({
return {
// gives access to the whole store
main,
// gives access to the state
state: main.state,
// gives access to specific getter; like `computed` properties, do not include `.value`
doubleCount: main.doubleCount,
// gives access only to specific state
state: computed(() => main.counter),
// gives access to specific getter; like `computed` properties
doubleCount: computed(() => main.doubleCount),
}
},
})
Expand Down Expand Up @@ -193,20 +198,31 @@ router.beforeEach((to, from, next) => {

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

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):
You can access any property defined in `state` and `getters` directly on the store, similar to `data` and `computed` properties in a Vue component.

```ts
export default defineComponent({
setup() {
const main = useMainStore()
const text = main.state.name
const doubleCount = main.doubleCount.value // notice the `.value` at the end
const text = main.name
const doubleCount = main.doubleCount
return {}
},
})
```

`state` is the result of a `ref` while every getter is the result of a `computed`. Both from `@vue/composition-api`.
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:

```ts
export default defineComponent({
setup() {
// ❌ This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = useMainStore()
return { name, doubleCount }
},
})
```

Actions are invoked like methods:

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

```ts
main.state.counter++
main.counter++
```

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

Note: **This is necessary in middlewares and other asyncronous methods**
Note: **This is necessary in middlewares and other asynchronous methods**.

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**.

Expand Down Expand Up @@ -344,18 +360,18 @@ createStore({
id: 'cart',
state: () => ({ items: [] }),
getters: {
message: state => {
message() {
const user = useUserStore()
return `Hi ${user.state.name}, you have ${items.length} items in the cart`
return `Hi ${user.name}, you have ${this.items.length} items in the cart`
},
},
actions: {
async purchase() {
const user = useUserStore()

await apiBuy(user.state.token, this.state.items)
await apiBuy(user.token, this.items)

this.state.items = []
this.items = []
},
},
})
Expand Down Expand Up @@ -386,7 +402,7 @@ export const useSharedStore = createStore({
const user = useUserStore()
const cart = useCartStore()

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

try {
await apiOrderCart(user.state.token, cart.state.items)
await apiOrderCart(user.token, cart.items)
cart.emptyCart()
} catch (err) {
displayError(err)
Expand Down Expand Up @@ -438,13 +454,14 @@ export const useCartUserStore = pinia(
},
{
getters: {
combinedGetter: ({ user, cart }) =>
`Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`,
combinedGetter () {
return `Hi ${this.user.name}, you have ${this.cart.list.length} items in your cart. It costs ${this.cart.price}.`,
}
},
actions: {
async orderCart() {
try {
await apiOrderCart(this.user.state.token, this.cart.state.items)
await apiOrderCart(this.user.token, this.cart.items)
this.cart.emptyCart()
} catch (err) {
displayError(err)
Expand Down
15 changes: 13 additions & 2 deletions __tests__/actions.spec.ts
@@ -1,6 +1,6 @@
import { createStore, setActiveReq } from '../src'

describe('Store', () => {
describe('Actions', () => {
const useStore = () => {
// create a new store
setActiveReq({})
Expand All @@ -13,9 +13,20 @@ describe('Store', () => {
a: { b: 'string' },
},
}),
getters: {
nonA(): boolean {
return !this.a
},
otherComputed() {
return this.nonA
},
},
actions: {
async getNonA() {
return this.nonA
},
toggle() {
this.state.a = !this.state.a
return (this.a = !this.a)
},

setFoo(foo: string) {
Expand Down
43 changes: 26 additions & 17 deletions __tests__/getters.spec.ts
@@ -1,6 +1,6 @@
import { createStore, setActiveReq } from '../src'

describe('Store', () => {
describe('Getters', () => {
const useStore = () => {
// create a new store
setActiveReq({})
Expand All @@ -10,9 +10,18 @@ describe('Store', () => {
name: 'Eduardo',
}),
getters: {
upperCaseName: ({ name }) => name.toUpperCase(),
composed: (state, { upperCaseName }) =>
(upperCaseName.value as string) + ': ok',
upperCaseName() {
return this.name.toUpperCase()
},
doubleName() {
return this.upperCaseName
},
composed() {
return this.upperCaseName + ': ok'
},
// TODO: I can't figure out how to pass `this` as an argument. Not sure
// it is possible in this specific scenario
// upperCaseNameArrow: store => store.name,
},
})()
}
Expand All @@ -26,24 +35,24 @@ describe('Store', () => {
id: 'A',
state: () => ({ a: 'a' }),
getters: {
fromB(state) {
fromB() {
const bStore = useB()
return state.a + ' ' + bStore.state.b
return this.a + ' ' + bStore.b
},
},
})

it('adds getters to the store', () => {
const store = useStore()
expect(store.upperCaseName.value).toBe('EDUARDO')
store.state.name = 'Ed'
expect(store.upperCaseName.value).toBe('ED')
expect(store.upperCaseName).toBe('EDUARDO')
store.name = 'Ed'
expect(store.upperCaseName).toBe('ED')
})

it('updates the value', () => {
const store = useStore()
store.state.name = 'Ed'
expect(store.upperCaseName.value).toBe('ED')
store.name = 'Ed'
expect(store.upperCaseName).toBe('ED')
})

it('supports changing between requests', () => {
Expand All @@ -55,16 +64,16 @@ describe('Store', () => {
// simulate a different request
setActiveReq(req2)
const bStore = useB()
bStore.state.b = 'c'
bStore.b = 'c'

aStore.state.a = 'b'
expect(aStore.fromB.value).toBe('b b')
aStore.a = 'b'
expect(aStore.fromB).toBe('b b')
})

it('can use other getters', () => {
const store = useStore()
expect(store.composed.value).toBe('EDUARDO: ok')
store.state.name = 'Ed'
expect(store.composed.value).toBe('ED: ok')
expect(store.composed).toBe('EDUARDO: ok')
store.name = 'Ed'
expect(store.composed).toBe('ED: ok')
})
})
2 changes: 1 addition & 1 deletion __tests__/rootState.spec.ts
@@ -1,6 +1,6 @@
import { createStore, getRootState } from '../src'

describe('Store', () => {
describe('Root State', () => {
const useA = createStore({
id: 'a',
state: () => ({ a: 'a' }),
Expand Down
31 changes: 31 additions & 0 deletions __tests__/state.spec.ts
@@ -0,0 +1,31 @@
import { createStore, setActiveReq } from '../src'
import { computed } from '@vue/composition-api'

describe('State', () => {
const useStore = () => {
// create a new store
setActiveReq({})
return createStore({
id: 'main',
state: () => ({
name: 'Eduardo',
counter: 0,
}),
})()
}

it('can directly access state at the store level', () => {
const store = useStore()
expect(store.name).toBe('Eduardo')
store.name = 'Ed'
expect(store.name).toBe('Ed')
})

it('state is reactive', () => {
const store = useStore()
const upperCased = computed(() => store.name.toUpperCase())
expect(upperCased.value).toBe('EDUARDO')
store.name = 'Ed'
expect(upperCased.value).toBe('ED')
})
})
6 changes: 5 additions & 1 deletion __tests__/tds/store.test-d.ts
Expand Up @@ -5,12 +5,16 @@ const useStore = createStore({
id: 'name',
state: () => ({ a: 'on' as 'on' | 'off' }),
getters: {
upper: state => state.a.toUpperCase(),
upper() {
return this.a.toUpperCase()
},
},
})

const store = useStore()

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

expectType<{ upper: string }>(store)

expectError(() => store.nonExistant)
2 changes: 1 addition & 1 deletion src/index.ts
@@ -1,4 +1,4 @@
export { createStore } from './store'
export { setActiveReq, setStateProvider, getRootState } from './rootStore'
export { StateTree, StoreGetter, Store } from './types'
export { StateTree, Store } from './types'
export { PiniaSsr } from './ssrPlugin'
6 changes: 3 additions & 3 deletions src/ssrPlugin.ts
Expand Up @@ -2,7 +2,7 @@ import { VueConstructor } from 'vue/types'
import { setActiveReq } from './rootStore'
import { SetupContext } from '@vue/composition-api'

export const PiniaSsr = (vue: VueConstructor) => {
export const PiniaSsr = (_Vue: VueConstructor) => {
const isServer = typeof window === 'undefined'

if (!isServer) {
Expand All @@ -12,14 +12,14 @@ export const PiniaSsr = (vue: VueConstructor) => {
return
}

vue.mixin({
_Vue.mixin({
beforeCreate() {
// @ts-ignore
const { setup, serverPrefetch } = this.$options
if (setup) {
// @ts-ignore
this.$options.setup = (props: any, context: SetupContext) => {
// @ts-ignore
// @ts-ignore TODO: fix usage with nuxt-composition-api https://github.com/posva/pinia/issues/179
if (context.ssrContext) setActiveReq(context.ssrContext.req)
return setup(props, context)
}
Expand Down

0 comments on commit 7bb7733

Please sign in to comment.