From 50010fa48992a57c9117aeb63b23af59d12bac76 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 22 Jul 2021 17:23:04 +0200 Subject: [PATCH] docs: updates after effectScope --- docs/cookbook/index.md | 1 + docs/core-concepts/actions.md | 19 ++++++-- docs/core-concepts/index.md | 4 +- docs/core-concepts/plugins.md | 85 ++++++++++++++++++----------------- docs/core-concepts/state.md | 72 +++++++++++++---------------- docs/getting-started.md | 4 +- docs/introduction.md | 24 +++++++++- 7 files changed, 121 insertions(+), 88 deletions(-) diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 2b2cc30f..4337a2f0 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,5 +1,6 @@ # Cookbook +- [Testing Stores (WIP)](./testing.md): How to unit test Stores and mock them in component unit tests. - [Composing Stores](./composing-stores.md): How to cross use multiple stores. e.g. using the user store in the cart store. - [Options API](./options-api.md): How to use Pinia without the composition API, outside of `setup()`. - [Migrating from 0.0.7](./migration-0-0-7.md): A migration guide with more examples than the changelog. diff --git a/docs/core-concepts/actions.md b/docs/core-concepts/actions.md index ce59170f..aaa4a37b 100644 --- a/docs/core-concepts/actions.md +++ b/docs/core-concepts/actions.md @@ -118,11 +118,11 @@ export default { } ``` -## Watching actions +## Subscribing to actions > [Give feedback about `$onAction()`](https://github.com/posva/pinia/issues/240) -It is possible to observe actions and their outcome with `store.$onAction()`. This +It is possible to observe actions and their outcome with `store.$onAction()`. The callback passed to it is executed before the action itself. `after` handle promises and allows you to change the returned value of the action. `onError` allows you to stop the error from propagating. These are useful for tracking errors at runtime, similar to [what is explaining in Vue docs](https://v3.vuejs.org/guide/tooling/deployment.html#tracking-runtime-errors). Here is an example that logs before running actions and after they resolve/reject. @@ -163,4 +163,17 @@ const unsubscribe = someStore.$onAction( unsubscribe() ``` -By default, action listeners are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. +By default, _action subscriptions_ are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you want to keep them after the component is unmounted, pass `true` as the second argument to _detach_ the _action subscription_ from the current component: + +```js +export default { + setup() { + const someStore = useSomeStore() + + // this subscription will be kept after the component is unmounted + someStore.$onAction(callback, true) + + // ... + }, +} +``` diff --git a/docs/core-concepts/index.md b/docs/core-concepts/index.md index 44b08805..88d219f2 100644 --- a/docs/core-concepts/index.md +++ b/docs/core-concepts/index.md @@ -1,6 +1,6 @@ # Defining a Store -Before diving into core concepts, we need to know that a store is defined using `defineStore()` and that it requires an `id` property, **unique** across all of your stores: +Before diving into core concepts, we need to know that a store is defined using `defineStore()` and that it requires an **unique** name, passed as the first argument: ```js import { defineStore } from 'pinia' @@ -12,7 +12,7 @@ export const useStore = defineStore('main', { }) ``` -The `id` is necessary and is used by `pinia` to connect the store to the devtools. Naming the returned function _use..._ is a convention across composables to make its usage idiomatic. +This _name_, also referred as _id_, is necessary and is used by Pinia to connect the store to the devtools. Naming the returned function _use..._ is a convention across composables to make its usage idiomatic. ## Using the store diff --git a/docs/core-concepts/plugins.md b/docs/core-concepts/plugins.md index 58ad6121..703976f2 100644 --- a/docs/core-concepts/plugins.md +++ b/docs/core-concepts/plugins.md @@ -30,7 +30,7 @@ const store = useStore() store.secret // 'the cake is a lie' ``` -This is useful to add global objects like the router, modals, or toasts. +This is useful to add global objects like the router, modal, or toast managers. ## Introduction @@ -52,7 +52,7 @@ This function is then passed to `pinia` with `pinia.use()`: pinia.use(myPiniaPlugin) ``` -It will get executed **every time `useStore()`** is called to be able to extend them. This is a limitation of the current implementation until [the effectScope RFC](https://github.com/vuejs/rfcs/pull/212) is merged. +Plugins are only applied to stores **created after `pinia` is passed to the app**, otherwise they won't be applied. ## Augmenting a Store @@ -70,6 +70,19 @@ pinia.use(({ store }) => { }) ``` +Any property _returned_ by a plugin will be automatically tracked by devtools so in order to make `hello` visible in devtools, make sure to add it to `store._customProperties` **in dev mode only** if you want to debug it in devtools: + +```js +// from the example above +pinia.use(({ store }) => { + store.hello = 'world' + // make sure your bundler handle this. webpack and vite should do it by default + if (process.env.NODE_ENV === 'development') { + store._customProperties.add('secret') + } +}) +``` + Note that every store is wrapped with [`reactive`](https://v3.vuejs.org/api/basic-reactivity.html#reactive), automatically unwrapping any Ref (`ref()`, `computed()`, ...) it contains: ```js @@ -106,35 +119,19 @@ pinia.use(({ store }) => { // it gets automatically unwrapped store.secret // 'secret' - // we need to check if the state has been added yet because of - // the limitation mentioned during the introduction - if (!store.$state.hasOwnProperty('hasError')) { - // Each store has its own `hasError` - store.$state.hasError = ref(false) - } + const hasError = ref(false) + store.$state.hasError = hasError // this one must always be set store.hasError = toRef(store.$state, 'hasError') -}) -``` - -**Note**: If you are using Vue 2, make sure to use `set` (from `@vue/composition-api`) or `Vue.set` as mentioned in the [State page](./state.md#state) when creating new properties like `secret` and `hasError` in the example above. -Any property _returned_ by a plugin will be automatically tracked by devtools so in order to make `hasError` visible in devtools, make sure to add it to `store._customProperties` **in dev mode only** if you want to debug it in devtools: - -```js -// from the example above -pinia.use(({ store }) => { - store.$state.secret = globalSecret - store.secret = globalSecret - // make sure your bundler handle this. webpack and vite should do it by default - if (process.env.NODE_ENV === 'development') { - store._customProperties.add('secret') - } + // in this case it's better not to return `hasError` since it + // will be displayed in the `state` section in the devtools + // anyway and if we return it, devtools will display it twice. }) ``` :::warning -If you are using **Vue 2**, Pinia is subject to the [same reactivity caveats](https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats) as Vue. You will need to use `set` from `@vue/composition-api`: +If you are using **Vue 2**, Pinia is subject to the [same reactivity caveats](https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats) as Vue. You will need to use `set` from `@vue/composition-api` when creating new state properties like `secret` and `hasError`: ```js import { set } from '@vue/composition-api' @@ -144,11 +141,11 @@ pinia.use(({ store }) => { // If the data is meant to be used during SSR, you should // set it on the `$state` property so it is serialized and // picked up during hydration - set(store.$state, 'hello', secretRef) + set(store.$state, 'secret', secretRef) // set it directly on the store too so you can access it - // both ways: `store.$state.hello` / `store.hello` - set(store, 'hello', secretRef) - store.hello // 'secret' + // both ways: `store.$state.secret` / `store.secret` + set(store, 'secret', secretRef) + store.secret // 'secret' } }) ``` @@ -171,22 +168,19 @@ pinia.use(({ store }) => { ## Calling `$subscribe` inside plugins -Because of the limitation mentioned above about plugins being invoked **every time `useStore()` is called**, it's important to avoid _subscribing_ multiple times by keeping track of the registered subscriptions: +You can use [store.$subscribe](./state.md#subscribing-to-the-state) and [store.$onAction](./actions.md#subscribing-to-actions) inside plugins too: ```ts -let isRegistered pinia.use(({ store }) => { - if (!isRegistered) { - store.$subscribe(() => { - // react to store changes - }) - isRegistered = true - } + store.$subscribe(() => { + // react to store changes + }) + store.$onAction(() => { + // react to store actions + }) }) ``` -The same is true for `store.$onAction()`. - ## Adding new options It is possible to create new options when defining stores to later on consume them from plugins. For example, you could create a `debounce` option that allows you to debounce any action: @@ -215,6 +209,7 @@ import debounce from 'lodash/debunce' pinia.use(({ options, store }) => { if (options.debounce) { + // we are overriding the actions with new ones return Object.keys(options.debounce).reduce((debouncedActions, action) => { debouncedActions[action] = debounce( store[action], @@ -228,6 +223,8 @@ pinia.use(({ options, store }) => { ## TypeScript +Everything shown above can be done with typing support, so you don't ever need to use `any` or `@ts-ignore`. + ### Typing plugins A Pinia plugin can be typed as follows: @@ -249,7 +246,12 @@ import 'pinia' declare module 'pinia' { export interface PiniaCustomProperties { - hello: string + // by using a setter we can allow both strings and refs + set hello(value: string | Ref) + get hello(): string + + // you can define simpler values too + simpleNumber: number } } ``` @@ -259,8 +261,11 @@ It can then be written and read safely: ```ts pinia.use(({ store }) => { store.hello = 'Hola' - // @ts-expect-error: this will still add a string because refs get unwrapped store.hello = ref('Hola') + + store.number = Math.random() + // @ts-expect-error: we haven't typed this correctly + store.number = ref(Math.random()) }) ``` diff --git a/docs/core-concepts/state.md b/docs/core-concepts/state.md index 67bd7a0d..b16d5945 100644 --- a/docs/core-concepts/state.md +++ b/docs/core-concepts/state.md @@ -133,59 +133,51 @@ You can also replace the whole state of your application by changing the `state` pinia.state.value = {} ``` -## Watching the state +## Subscribing to the state -You can watch the state, similar to Vuex's [subscribe method](https://vuex.vuejs.org/api/#subscribe) by simply watching it (since it's a reactive source). Keep in mind a watcher is cleared up when the wrapping component is unmounted so you should add the watcher in your App component or outside of it if you want it to run forever. +You can watch the state and its changes through the `$subscribe()` method of a store, similar to Vuex's [subscribe method](https://vuex.vuejs.org/api/#subscribe). The advantage of using `$subscribe()` over a regular `watch()` is that _subscriptions_ will trigger only once after _patches_ (e.g. when using the function version from above). ```js -watch( - pinia.state, - (state) => { - // persist the whole state to the local storage whenever it changes - localStorage.setItem('piniaState', JSON.stringify(state)) - }, - { deep: true } -) +cartStore.$subscribe((mutation, state) => { + // import { MutationType } from 'pinia' + mutation.type // 'direct' | 'patch object' | 'patch function' + // same as cartStore.$id + mutation.storeId // 'cart' + // only available with mutation.type === 'patch object' + mutation.payload // patch object passed to cartStore.$patch() + + // persist the whole state to the local storage whenever it changes + localStorage.setItem('cart', JSON.stringify(state)) +}) ``` -You can also observe a specific store state instead of all of them by passing a function. Here is an example to watch a store with the id `cart`: +By default, _state subscriptions_ are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you want to keep them after the component is unmounted, pass `true` as the second argument to _detach_ the _state subscription_ from the current component: ```js -watch( - () => pinia.state.value.cart, - (cartState) => { - // persist the whole state to the local storage whenever it changes - localStorage.setItem('cart', JSON.stringify(cartState)) - }, - { deep: true } -) -``` +export default { + setup() { + const someStore = useSomeStore() -Note that depending on when you create the watcher, `pinia.state.value.cart` might be `undefined`. You can also watch a store's `$state` property (this will also make typing work): + // this subscription will be kept after the component is unmounted + someStore.$subscribe(callback, true) -```ts -import { defineStore } from 'pinia' - -const useCartStore = defineStore('cart', { - // ... -}) + // ... + }, +} +``` -const cartStore = useCartStore() +:::tip +You can watch the whole state on the `pinia` instance: -// watch the whole state of the cart +```js watch( - () => cartStore.$state, - () => { - // do something + pinia.state, + (state) => { + // persist the whole state to the local storage whenever it changes + localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } ) - -// you can also watch a getter -watch( - () => cartStore.totalAmount, - () => { - // do something - } -) ``` + +::: diff --git a/docs/getting-started.md b/docs/getting-started.md index c61eec74..bc08b579 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,7 +12,7 @@ npm install pinia@next `pinia@next` install Pinia v2 for Vue 3. If your app is using Vue 2, you need to install Pinia v1: `pinia@latest` **and** `@vue/composition-api`. If you are using Nuxt, you should follow [these instructions](/ssr/nuxt). ::: -Create a pinia (the root store) and pass it to app: +Create a pinia (the root store) and pass it to the app: ```js import { createPinia } from 'pinia' @@ -38,7 +38,7 @@ new Vue({ }) ``` -This will also add devtools support. In Vue 3, some features like time traveling and editing are still not supported because vue-devtools doesn't expose the necessary APIs yet. In Vue 2, Pinia uses the existing interface for Vuex (and can therefore not be used alongside it). +This will also add devtools support. In Vue 3, some features like time traveling and editing are still not supported because vue-devtools doesn't expose the necessary APIs yet but the devtools have way more features are the developer experience as a whole is far superior. In Vue 2, Pinia uses the existing interface for Vuex (and can therefore not be used alongside it). ## What is a Store? diff --git a/docs/introduction.md b/docs/introduction.md index fbf22f98..907ac524 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,6 +1,18 @@ # Introduction -Pinia [started](https://github.com/posva/pinia/commit/06aeef54e2cad66696063c62829dac74e15fd19e) as an experiment to redesign what a Store for Vue could look like with the [Composition API](https://github.com/vuejs/composition-api) around November 2019. Since then, the initial principles are still the same, but Pinia works for both Vue 2 and Vue 3 **and doesn't require you to use the composition API**. The API is the same for both except for _installation_ and _SSR_, and these docs are targeted to Vue 3 **with notes about Vue 2** whenever necessary so it can be used no matter if you are using Vue 2 or Vue 3! +Pinia [started](https://github.com/posva/pinia/commit/06aeef54e2cad66696063c62829dac74e15fd19e) as an experiment to redesign what a Store for Vue could look like with the [Composition API](https://github.com/vuejs/composition-api) around November 2019. Since then, the initial principles are still the same, but Pinia works for both Vue 2 and Vue 3 **and doesn't require you to use the composition API**. The API is the same for both except for _installation_ and _SSR_, and these docs are targeted to Vue 3 **with notes about Vue 2** whenever necessary so it can be read by Vue 2 and Vue 3 users! + +## Why should I use Pinia? + +Pinia is a store library for Vue, it allows you to share a state across components/pages. If you are familiar with the Composition API, you might be thinking you can already share a global state with a simple `export const state = reactive({})`. This is true for single page applications but **exposes your application to security vulnerabilities** if it is server side rendered. But even in small single page applications, you get a lot from using Pinia: + +- Devtools support + - A timeline to track actions, mutations + - Stores appear in components where they are used + - Time travel and easier debugging +- Plugins: extend Pinia features with plugins +- Proper TypeScript support or **autocompletion** for JS users +- Server Side Rendering Support ## Basic example @@ -35,6 +47,16 @@ export default { } ``` +You can even use a function (similar to a component `setup()`) to define a Store for more advanced use cases: + +```js +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + + return { count } +}) +``` + If you are still not into `setup()` and Composition API, don't worry, Pinia also support a similar set of [_map helpers_ like Vuex](https://vuex.vuejs.org/guide/state.html#the-mapstate-helper). You define stores the same way but then use `mapStores()`, `mapState()`, or `mapActions()`: ```js -- 2.47.2