- 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 โ๏ธ
}),
// 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
},
},
})
return {
// gives access to the whole store
main,
- // gives access to the state
- state: main.state,
- // gives access to specific getter,
+ // gives access only to specific state
+ state: computed(() => main.counter),
+ // gives access to specific getter; like `computed` properties
+ doubleCount: computed(() => main.doubleCount),
}
},
})
Note: the SSR implementation is yet to be decided on Pinia, but if you intend having SSR on your application, you should avoid using `useStore` functions at the root level of a file to make sure the correct store is retrieved for your request.
-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 (meaning you need to use `.value` to read the actual value on the JavaScript but not in the template):
+Or:
+
+```ts
+import { createRouter } from 'vue-router'
+const router = createRouter({
+ // ...
+})
+
+// โ Depending on where you do this it will fail
+const main = useMainStore()
+
+router.beforeEach((to, from, next) => {
+ if (main.state.isLoggedIn) next()
+ else next('/login')
+})
+```
+
+It must be called **after the Composition API plugin is installed**. That's why calling `useStore` inside functions is usually safe, because they are called after the plugin being installed:
+
+```ts
+export default defineComponent({
+ setup() {
+ // โ
This will work
+ const main = useMainStore()
+
+ return {}
+ },
+})
+
+// In a different file...
+
+router.beforeEach((to, from, next) => {
+ // โ
This will work (requires an extra param for SSR, see below)
+ const main = useMainStore()
+
+ if (main.state.isLoggedIn) next()
+ else next('/login')
+})
+```
+
+โ ๏ธ: Note that if you are developing an SSR application, [you will need to do a bit more](#ssr).
+
+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`.
+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:
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:
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}.`
},
},
})
const cart = useCartStore()
try {
- await apiOrderCart(user.state.token, cart.state.items)
+ await apiOrderCart(user.token, cart.items)
cart.emptyCart()
} catch (err) {
displayError(err)
},
{
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)
import { createStore, setActiveReq } from '../src'
-describe('Store', () => {
+describe('Actions', () => {
const useStore = () => {
// create a new store
setActiveReq({})
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) {
import { createStore, setActiveReq } from '../src'
-describe('Store', () => {
+describe('Getters', () => {
const useStore = () => {
// create a new store
setActiveReq({})
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,
},
})()
}
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', () => {
// 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')
})
})
import { createStore, getRootState } from '../src'
-describe('Store', () => {
+describe('Root State', () => {
const useA = createStore({
id: 'a',
state: () => ({ a: 'a' }),
--- /dev/null
+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')
+ })
+})
-import { ref, watch, computed, Ref } from 'vue'
+import { ref, watch, computed, Ref, reactive } from 'vue'
import {
StateTree,
StoreWithState,
DeepPartial,
isPlainObject,
StoreWithGetters,
- StoreGetter,
- StoreAction,
Store,
StoreWithActions,
+ Method,
} from './types'
import {
getActiveReq,
return target
}
+function toComputed<T>(refObject: Ref<T>) {
+ // let asComputed = computed<T>()
+ const reactiveObject = {} as {
+ [k in keyof T]: Ref<T[k]>
+ }
+ for (const key in refObject.value) {
+ // @ts-ignore: the key matches
+ reactiveObject[key] = computed({
+ get: () => refObject.value[key as keyof T],
+ set: value => (refObject.value[key as keyof T] = value),
+ })
+ }
+
+ return reactiveObject
+}
+
/**
* Creates a store instance
* @param id - unique identifier of the store, like a name. eg: main, cart, user
export function buildStore<
Id extends string,
S extends StateTree,
- G extends Record<string, StoreGetter<S>>,
- A extends Record<string, StoreAction>
+ G extends Record<string, Method>,
+ A extends Record<string, Method>
>(
id: Id,
buildState: () => S = () => ({} as S),
// @ts-ignore FIXME: why is this even failing on TS
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 },
const storeWithState: StoreWithState<Id, S> = {
id,
_r,
- // it is replaced below by a getter
- // @ts-ignore FIXME: why is this even failing on TS
- state: state.value,
+ // @ts-ignore, `reactive` unwraps this making it of type S
+ state: computed<S>({
+ get: () => state.value,
+ set: newState => {
+ isListening = false
+ state.value = newState
+ isListening = true
+ },
+ }),
patch,
subscribe,
reset,
}
- const computedGetters: StoreWithGetters<S, G> = {} as StoreWithGetters<S, G>
+ const computedGetters: StoreWithGetters<G> = {} as StoreWithGetters<G>
for (const getterName in getters) {
computedGetters[getterName] = computed(() => {
setActiveReq(_r)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
- return getters[getterName](state.value as S, computedGetters)
- }) as any
+ return getters[getterName].call(store, store)
+ }) as StoreWithGetters<G>[typeof getterName]
}
// const reactiveGetters = reactive(computedGetters)
} as StoreWithActions<A>[typeof actionName]
}
- const store: Store<Id, S, G, A> = {
+ const store: Store<Id, S, G, A> = reactive({
...storeWithState,
+ // using this means no new properties can be added as state
+ ...toComputed(state),
...computedGetters,
...wrappedActions,
- }
-
- // make state access invisible
- Object.defineProperty(store, 'state', {
- get: () => state.value,
- set: (newState: S) => {
- isListening = false
- // @ts-ignore FIXME: why is this even failing on TS
- state.value = newState
- isListening = true
- },
- })
+ }) as Store<Id, S, G, A>
return store
}
export function createStore<
Id extends string,
S extends StateTree,
- G extends Record<string, StoreGetter<S>>,
- A extends Record<string, StoreAction>
+ G /* extends Record<string, StoreGetterThis> */,
+ A /* extends Record<string, StoreAction> */
>(options: {
id: Id
state?: () => S
- getters?: G
+ getters?: G & ThisType<S & StoreWithGetters<G>>
// allow actions use other actions
- actions?: A & ThisType<A & StoreWithState<Id, S> & StoreWithGetters<S, G>>
+ actions?: A & ThisType<A & S & StoreWithState<Id, S> & StoreWithGetters<G>>
}) {
const { id, state, getters, actions } = options
if (!store) {
stores.set(
id,
+ // @ts-ignore
(store = buildStore(id, state, getters, actions, getInitialState(id)))
)
state: S
) => void
-export type StoreWithGetters<
- S extends StateTree,
- G extends Record<string, StoreGetter<S>>
-> = {
- [k in keyof G]: G[k] extends StoreGetter<S, infer V> ? Ref<V> : never
-}
-
export interface StoreWithState<Id extends string, S extends StateTree> {
/**
* Unique identifier of the store
id: Id
/**
- * State of the Store
+ * State of the Store. Setting it will replace the whole state.
*/
state: S
subscribe(callback: SubscriptionCallback<S>): () => void
}
-export interface StoreAction {
- (...args: any[]): any
-}
+export type Method = (...args: any[]) => any
+
+// export type StoreAction<P extends any[], R> = (...args: P) => R
+// export interface StoreAction<P, R> {
+// (...args: P[]): R
+// }
// in this type we forget about this because otherwise the type is recursive
-export type StoreWithActions<A extends Record<string, StoreAction>> = {
- [k in keyof A]: A[k] extends (this: infer This, ...args: infer P) => infer R
- ? (this: This, ...args: P) => R
+export type StoreWithActions<A> = {
+ [k in keyof A]: A[k] extends (...args: infer P) => infer R
+ ? (...args: P) => R
: never
}
+// export interface StoreGetter<S extends StateTree, T = any> {
+// // TODO: would be nice to be able to define the getters here
+// (state: S, getters: Record<string, Ref<any>>): T
+// }
+
+export type StoreWithGetters<G> = {
+ [k in keyof G]: G[k] extends (this: infer This, store?: any) => infer R
+ ? R
+ : never
+}
+
+// // in this type we forget about this because otherwise the type is recursive
+// export type StoreWithThisGetters<G> = {
+// // TODO: does the infer this as the second argument work?
+// [k in keyof G]: G[k] extends (this: infer This, store?: any) => infer R
+// ? (this: This, store?: This) => R
+// : never
+// }
+
// has the actions without the context (this) for typings
export type Store<
Id extends string,
S extends StateTree,
- G extends Record<string, StoreGetter<S>>,
- A extends Record<string, StoreAction>
-> = StoreWithState<Id, S> & StoreWithGetters<S, G> & StoreWithActions<A>
+ G,
+ A
+> = StoreWithState<Id, S> & S & StoreWithGetters<G> & StoreWithActions<A>
export type GenericStore = Store<
string,
StateTree,
- Record<string, StoreGetter<StateTree>>,
- Record<string, StoreAction>
+ Record<string, Method>,
+ Record<string, Method>
>
export interface DevtoolHook {