From 86edf83c4ab0fb89a579313adc90403c4e323952 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 17 Jan 2020 15:02:50 +0100 Subject: [PATCH] docs: document new api --- README.md | 207 ++++++++++++++++++++++++++++++++----------------- nuxt/plugin.js | 23 ++++-- src/index.ts | 8 +- src/store.ts | 35 +++++++-- src/types.ts | 7 ++ 5 files changed, 200 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 15680831..906ce7d7 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ What I want is to inspire others to think about ways to improve Vuex and come up 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 @@ -57,10 +57,12 @@ A few notes about the project and possible questions: ## 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 @@ -74,25 +76,32 @@ npm install pinia ### 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: @@ -100,7 +109,7 @@ export const useMainStore = createStore( ```ts import { useMainStore } from '@/stores/main' -export default createComponent({ +export default defineComponent({ setup() { const main = useMainStore() @@ -145,6 +154,20 @@ export default createComponent({ `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: @@ -174,40 +197,72 @@ main.state = { counter: 666, name: 'Paimon' } ### 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 @@ -220,46 +275,59 @@ If one store uses an other store, there is no need to create a new file, you can #### 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: @@ -274,21 +342,22 @@ export const useCartUserStore = pinia( 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 diff --git a/nuxt/plugin.js b/nuxt/plugin.js index d6fa2f43..c27d5189 100644 --- a/nuxt/plugin.js +++ b/nuxt/plugin.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import { setActiveReq, setStateProvider } from 'pinia' +import { setActiveReq, setStateProvider, getRootState } from 'pinia' // import { Plugin } from '@nuxt/types' // declare module '@nuxt/types' { @@ -58,7 +58,7 @@ Vue.mixin({ if (context.ssrContext && context.ssrContext.req) { setActiveReq(context.ssrContext.req) } - setStateProvider(createStateProvider(context.ssrContext)) + // setStateProvider(createStateProvider(context.ssrContext)) return setup(props, context) } } @@ -72,7 +72,7 @@ Vue.mixin({ 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) } } @@ -86,8 +86,21 @@ Vue.mixin({ /** @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 diff --git a/src/index.ts b/src/index.ts index e451ba6a..7743f447 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,8 @@ -export { createStore, Store, setActiveReq, setStateProvider } from './store' +export { + createStore, + Store, + setActiveReq, + setStateProvider, + getRootState, +} from './store' export { StateTree, StoreGetter } from './types' diff --git a/src/store.ts b/src/store.ts index 0b96fb6e..711aef0d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,4 @@ -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, @@ -9,6 +8,7 @@ import { StoreWithGetters, StoreGetter, NonNullObject, + StoreReactiveGetters, } from './types' import { useStoreDevtools } from './devtools' @@ -62,7 +62,7 @@ export type Store< > = StoreWithState & StoreWithGetters & StoreWithActions export type WrapStoreWithId< - S extends Store + S extends Store > = S extends Store ? { [k in Id]: Store @@ -175,10 +175,13 @@ export function buildStore< 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[typeof getterName] } + // const reactiveGetters = reactive(computedGetters) + const wrappedActions: StoreWithActions = {} as StoreWithActions for (const actionName in actions) { wrappedActions[actionName] = function() { @@ -223,7 +226,7 @@ export const storesMap = new WeakMap< */ interface StateProvider { get(): Record - set(store: Store): any + set(store: Store): any } /** @@ -240,11 +243,33 @@ function getInitialState(id: string): StateTree | undefined { return provider && provider.get()[id] } -function setInitialState(store: Store): void { +function setInitialState(store: Store): void { const provider = stateProviders.get(getActiveReq()) if (provider) provider.set(store) } +type StoreGetterWithGetters< + G extends Record> +> = { + [k in keyof G]: G[k] extends StoreGetter + ? (state: S, getters: G) => V + : never +} + +export function getRootState(req: NonNullObject): Record { + const stores = storesMap.get(req) + if (!stores) return {} + const rootState = {} as Record + + 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 diff --git a/src/types.ts b/src/types.ts index fbd8d159..55348e05 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,13 @@ export type SubscriptionCallback = ( state: S ) => void +export type StoreReactiveGetters< + S extends StateTree, + G extends Record any> +> = { + [k in keyof G]: G[k] extends (state: S, getters: any) => infer V ? V : never +} + export type StoreWithGetters< S extends StateTree, G extends Record> -- 2.47.2