Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: access the state and getters through this #190

Merged
merged 7 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading