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 under 1kb gzip
-- Only `state` and `getters` 👐 `patch` is the new _mutation_
-- Actions are just functions ⚗️ Group your business there
+- Light layer on top of Vue 💨 keep it very lightweight
+- Only `state`, `getters` 👐 `patch` is the new _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 ⚙️
- DevTools support 💻 Which is crucial to make this enjoyable
## Roadmap / Ideas
- [ ] List Getters on DevTools
-- [ ] Nuxt Module
+- [x] Nuxt Module
+- [ ] Should the state be merged at the same level as actions and getters?
- [ ] Flag to remove devtools support (for very light production apps)
- [ ] Allow grouping stores together into a similar structure and allow defining new getters (`pinia`)
- [ ] Getter with params that act like computed properties (@ktsn)
+- [ ] Passing all getters to a getter (need Typing support)
## Installation
### Creating a Store
-You can create as many stores as you want, and they should each exist in isolated files:
+You can create as many stores as you want, and they should each exist in different files:
```ts
import { createStore } from 'pinia'
-export const useMainStore = createStore(
+export const useMainStore = createStore({
// name of the store
// it is used in devtools and allows restoring state
- 'main',
+ id: 'main',
// a function that returns a fresh state
- () => ({
+ state: () => ({
counter: 0,
name: 'Eduardo',
}),
// optional getters
- {
+ getters: {
doubleCount: state => state.counter * 2,
- }
-)
+ },
+ // optional actions
+ actions: {
+ reset() {
+ // `this` is the store instance
+ this.state.counter = 0
+ },
+ },
+})
```
`createStore` returns a function that has to be called to get access to the store:
```ts
import { useMainStore } from '@/stores/main'
-export default createComponent({
+export default defineComponent({
setup() {
const main = useMainStore()
`state` is the result of a `ref` while every getter is the result of a `computed`. Both from `@vue/composition-api`.
+Actions are called invoked like methods:
+
+```ts
+export default createComponent({
+ setup() {
+ const main = useMainStore()
+ // call the action as a method of the store
+ main.reset()
+
+ return {}
+ },
+})
+```
+
### Mutating the `state`
To mutate the state you can either directly change something:
### SSR
-The main part about SSR is **not sharing `state`** between requests. So we can pass `true` to `useStore` **once** when getting a new request on the server. If we follow [the SSR guide](https://ssr.vuejs.org/guide/data.html), our `createApp` should look like this:
+When writing a Single Page Application, there always only one instance of the store, but on the server, each request will create new store instances. For Pinia to track which one should be used, we rely on the _Request_ object (usually named `req`). Pinia makes this automatic in a few places:
-```ts
-export function createApp() {
- // Here there could also be a router
- const store = useStore(true)
+- actions
+- getters
+- `setup`
+- `serverPrefetch`
- // we can change the state now!
- store.state.counter++
+Meaning that you can call `useMainStore` at the top of these functions and it will retrieve the correct store.
- // create the app instance
- const app = new Vue({
- render: h => h(App),
- })
+#### Nuxt Plugin
- // expose the app and the store.
- return { app, store }
+SSR is much easier with Nuxt, and so is for Pinia: include the Pinia module in your `buildModules` in your `nuxt.config.js`:
+
+```js
+export default {
+ // ...
+ // rest of the nuxt config
+ // ...
+ buildModules: ['pinia/nuxt'],
+}
+```
+
+If you are dealing with SSR, in order to make sure the correct store is retrieved by `useStore` functions, pass the current `req` to `useStore`. **This is necessary anywhere not in the list above**:
+
+```js
+export default {
+ async fetch({ req }) {
+ const store = useStore(req)
+ },
}
```
-### Actions
+**This is necessary in middlewares and other asyncronous 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**.
+
+#### Raw Vue SSR
+
+TODO: this part isn't built yet. You need to call `setActiveReq` with the _Request_ object before `useStore` is called
+
+### Accessing other Stores
+
+You can `useOtherStore` inside a store `actions` and `getters`:
Actions are simply function that contain business logic. As with components, they **must call `useStore`** to retrieve the store:
```ts
-export async function login(user, password) {
- const store = useUserStore()
- const userData = await apiLogin(user, password)
-
- store.patch({
- name: user,
- ...userData,
- })
-}
+createStore({
+ id: 'cart',
+ state: () => ({ items: [] }),
+ getters: {
+ message: state => {
+ const user = useUserStore()
+ return `Hi ${user.state.name}, you have ${items.length} items in the cart`
+ },
+ },
+ actions: {
+ async purchase() {
+ const user = useUserStore()
+
+ await apiBuy(user.state.token, this.state.items)
+
+ this.state.items = []
+ },
+ },
+})
```
### Composing Stores
#### Shared Getters
-If you need to compute a value based on the `state` and/or `getters` of multiple stores, you may be able to import all the stores but one into the remaining store, but depending on how your stores are used across your application, **this would hurt your code splitting** as you importing the store that imports all others stores, would result in **one single big chunk** with all of your stores.
-To prevent this, **we follow the rule above** and we create a new file:
+If you need to compute a value based on the `state` and/or `getters` of multiple stores, you may be able to import all the stores but one into the remaining store, but depending on how your stores are used across your application, **this would hurt your code splitting** because importing the store that imports all others stores, would result in **one single big chunk** with all of your stores.
+To prevent this, **we follow the rule above** and we create a new file with a new store:
```ts
-import { computed } from '@vue/composition-api'
+import { createStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'
-export const summary = computed(() => {
- const user = useUserStore()
- const cart = useCartStore()
+export const useSharedStore = createStore({
+ id: 'shared',
+ state: () => ({}),
+ getters: {
+ summary() {
+ 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.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`
+ },
+ },
})
```
#### Shared Actions
-When an actions needs to use multiple stores, we do the same, we create a new file:
+When an actions needs to use multiple stores, we do the same, we create a new file with a new store:
```ts
+import { createStore } from 'pinia'
import { useUserStore } from './user'
-import { useCartStore, emptyCart } from './cart'
-
-export async function orderCart() {
- const user = useUserStore()
- const cart = useCartStore()
+import { useCartStore } from './cart'
- try {
- await apiOrderCart(user.state.token, cart.state.items)
- emptyCart()
- } catch (err) {
- displayError(err)
- }
-}
+export const useSharedStore = createStore({
+ id: 'shared',
+ state: () => ({}),
+ actions: {
+ async orderCart() {
+ const user = useUserStore()
+ const cart = useCartStore()
+
+ try {
+ await apiOrderCart(user.state.token, cart.state.items)
+ cart.emptyCart()
+ } catch (err) {
+ displayError(err)
+ }
+ },
+ },
+})
```
#### Creating _Pinias_
-_Not implemented_. Replaces the examples above about combining state and getters and about composing stores.
+_Not implemented_. Still under discussion, needs more feedback as this doesn't seem necessary.
Combine multiple _stores_ (gajos) into a new one:
cart: useCartStore,
},
{
- combinedGetter: state =>
- `Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`,
+ getters: {
+ combinedGetter: ({ user, cart }) =>
+ `Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`,
+ },
+ actions: {
+ async orderCart() {
+ try {
+ await apiOrderCart(this.user.state.token, this.cart.state.items)
+ this.cart.emptyCart()
+ } catch (err) {
+ displayError(err)
+ }
+ },
+ },
}
)
-
-export async function orderCart() {
- const store = useCartUserStore()
-
- try {
- await apiOrderCart(store.state.user.token, store.state.cart.items)
- emptyCart()
- } catch (err) {
- displayError(err)
- }
-}
```
## Related
import Vue from 'vue'
-import { setActiveReq, setStateProvider } from 'pinia'
+import { setActiveReq, setStateProvider, getRootState } from 'pinia'
// import { Plugin } from '@nuxt/types'
// declare module '@nuxt/types' {
if (context.ssrContext && context.ssrContext.req) {
setActiveReq(context.ssrContext.req)
}
- setStateProvider(createStateProvider(context.ssrContext))
+ // setStateProvider(createStateProvider(context.ssrContext))
return setup(props, context)
}
}
const original = patchedServerPrefetch[i]
patchedServerPrefetch[i] = function() {
setActiveReq(this.$ssrContext.req)
- setStateProvider(createStateProvider(this.$ssrContext.req))
+ // setStateProvider(createStateProvider(this.$ssrContext.req))
return original.call(this)
}
}
/** @type {import('@nuxt/types').Plugin} */
const myPlugin = context => {
// console.log('🍍 Pinia Nuxt plugin installed')
- setActiveReq(context.req)
- setStateProvider(createStateProvider(context.ssrContext))
+ // setActiveReq(context.req)
+ // setStateProvider(createStateProvider(context.ssrContext))
+
+ if (process.server) {
+ setActiveReq(context.req)
+ context.beforeNuxtRender(({ nuxtState }) => {
+ nuxtState.pinia = getRootState(context.req)
+ })
+ } else {
+ setStateProvider({
+ get: () => context.nuxtState.pinia,
+ // TODO: remove the setter
+ set: () => {},
+ })
+ }
}
export default myPlugin
-import { ref, watch, computed } from '@vue/composition-api'
-import { Ref, UnwrapRef } from '@vue/composition-api/dist/reactivity'
+import { ref, watch, computed, reactive, Ref } from '@vue/composition-api'
import {
StateTree,
StoreWithState,
StoreWithGetters,
StoreGetter,
NonNullObject,
+ StoreReactiveGetters,
} from './types'
import { useStoreDevtools } from './devtools'
> = StoreWithState<Id, S> & StoreWithGetters<S, G> & StoreWithActions<A>
export type WrapStoreWithId<
- S extends Store<any, any, any, any>
+ S extends Store<string, StateTree, any, any>
> = S extends Store<infer Id, infer S, infer G, infer A>
? {
[k in Id]: Store<Id, S, G, A>
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 StoreWithGetters<S, G>[typeof getterName]
}
+ // const reactiveGetters = reactive(computedGetters)
+
const wrappedActions: StoreWithActions<A> = {} as StoreWithActions<A>
for (const actionName in actions) {
wrappedActions[actionName] = function() {
*/
interface StateProvider {
get(): Record<string, StateTree>
- set(store: Store<any, any, any, any>): any
+ set(store: Store<string, StateTree, any, any>): any
}
/**
return provider && provider.get()[id]
}
-function setInitialState(store: Store<any, any, any, any>): void {
+function setInitialState(store: Store<string, StateTree, any, any>): void {
const provider = stateProviders.get(getActiveReq())
if (provider) provider.set(store)
}
+type StoreGetterWithGetters<
+ G extends Record<string, StoreGetter<StateTree>>
+> = {
+ [k in keyof G]: G[k] extends StoreGetter<infer S, infer V>
+ ? (state: S, getters: G) => V
+ : never
+}
+
+export function getRootState(req: NonNullObject): Record<string, StateTree> {
+ const stores = storesMap.get(req)
+ if (!stores) return {}
+ const rootState = {} as Record<string, StateTree>
+
+ for (const store of Object.values(stores)) {
+ rootState[store.id] = store.state
+ }
+
+ console.log('global state', rootState)
+
+ return rootState
+}
+
/**
* Creates a `useStore` function that retrieves the store instance
* @param options