diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1370825..828e2b0 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -171,6 +171,15 @@ export default defineConfig({ } ] }, + { + text: 'Pinia', + items: [ + { + text: `Why am I getting an error about 'no active Pinia'?`, + link: '/faq/no-active-pinia' + } + ] + }, // { // text: 'Debugging', // items: [ diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 17180bf..416e117 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -75,3 +75,18 @@ html:not(.dark) .VPSidebar { .VPSidebarItem.level-1.is-active .VPLink::before { border-left-color: var(--vp-c-brand-1); } + +/* Custom styling for quoting error messages and warnings. This might be better as a custom container, once they're supported */ +blockquote.quote-code-error { + background-color: var(--vp-custom-block-warning-bg); + border-color: var(--vp-custom-block-warning-border); + font-family: var(--vp-font-family-mono); + margin-left: 20px; + margin-right: 20px; + padding: 10px 10px 10px 20px; + white-space: pre-wrap; +} + +.vp-doc blockquote.quote-code-error > p { + font-size: 12.25px; +} diff --git a/docs/faq/index.md b/docs/faq/index.md index 1e14aeb..a15fa26 100644 --- a/docs/faq/index.md +++ b/docs/faq/index.md @@ -61,6 +61,12 @@ Only about half the questions have complete answers. Those questions are listed --- + + +- [Why am I getting an error about 'no active Pinia'?](no-active-pinia) + +--- + - [Why does my logging show an empty/missing value after I've loaded the data?](logging-after-loading) diff --git a/docs/faq/no-active-pinia.md b/docs/faq/no-active-pinia.md new file mode 100644 index 0000000..efda0de --- /dev/null +++ b/docs/faq/no-active-pinia.md @@ -0,0 +1,682 @@ +# Why am I getting an error about 'no active Pinia'? + +This is the error message from Pinia: + +> [🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"? +> +> See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help. +> +> This will fail in production. +> {.quote-code-error} + +While this probably goes without saying, you should start by reading the page linked in the error message. + +## Quick fixes + +Before we do a deep dive into what might be going wrong here, let's briefly cover some common problems that have quick fixes. + +### Quick fix 1 - missing `setup` attribute + +If you're trying to use the store in a component with ` + + +``` + +Here it is in a Playground: + +- [Broken Playground example](https://play.vuejs.org/#eNp9VMGO2jAQ/RUrrZQgLc7Coj0gqGirPbSqWtTtMYcNyQS8JLZlOywSyr93bCchsGIPIHve85t5Y09OQZUyTl91MA9YJYUy5EQyBamBr1KShhRKVCQ81BAm/IqwZpylPUXa3ZlkT3uAxqmU1CskPBNcG4IRsjznifA36jAn1KMuSYSgFak1RA7GvY9UouYmCj/hOhwFd62HcZVK9CQ4ujolnJCkBXQSzImL2NgKi4pzOBghSj1OJbNoEuyMkXoexzWX+y3NRBW/I64e6SOdxSXbxKCrmPEcjpgwCe46bVfmLT0HrqZ0iho508YHKEqNN0q8aVCXYph+nEN1s74OX93TyYxO7l1hvqjKClmdJuENNsho7HLBtlftQRXJSlB/pGF4CxdtSstSvP10MaNq6KvKdpDtfbxIS30GXvXRV7pWgF4OMLBiUrUF4+Gn599wxHUPViKvS2R/AP4FLcraFulp32qeY90Dniv3h7tuxrf/9NPRANedK+vAtcPxXWu/f+D9XO4DnQ3a2D5pbOBCZ4pJ82UwHgqKW5ODL3it0Ehm9LMRCnoejWUbH2sLDIalA3Amro/bwfAkliOMiaPJ9AGDi7gvCzcGKlniMOGOkEXODm6By01tjOBklZUs2y/x1bbitACT7SKWj5Kg5RLyS6R5ey72B70ePmEruIgHabBFl36uPjE5FIzDZQu6L0jC4eho3tm7li2Hp6OwyxPe+WtLM3eN/SX2VroAccKiBFqKbfRibeFDIZ9PLG9esHeWgdeMf/gCeDMKmv+R47yg) + +Change ` + + +``` + +- [Broken Playground example](https://play.vuejs.org/#eNp9VF1v2jAU/StWNokgFadf6gOiE9vUh03TVq17zEPd+AIujm3ZDkVC/Pdd20kIVPQBKb7n+vicY192Wc2Eoq8um2aiNtp6siOVBebhqzFkTxZW12S0aWBUqpOGR6EE61tMWB2awu4E0IIZQxNDqSqtnCdYIfeHc3L8jTssEvVoPCRHMJA0DvII4zpVat0on48+4fdonF20HiY1M+hJK3S1KxUhZQu4MpuSWAm1OYoqOGy81tJNmBEBLbOV98ZNi6JRZr2kla6Ld43zO3pHbwspXgpwdSEUhy0eWGYXHXeUeY4vgvNreo0cXDifChSpJi9Wvzmwx2R4/IRDfVZfh88v6dUtvbqMwpKoOhAFnn2p9hiQd5jyQixP4kEWIyTYP8YLvIWjmJiU+u1nrHnbQK+qWkG1TvUFk+4AvLptUvpoAb1sYGDFM7sEn+CHp9+wxe8erDVvJHZ/AP4Fp2UTRKa2b43iqHvQF+X+iNct1PKfe9h6UK5zFRzEOGJ/jPb7B94Pcm/o7SDG9kljgDNXWWH8l8F44Ot78hq9nxuOHcGX/GjRUOVdbO07aWHa+sQFIA4NbOM2DgvWSNwehHDmcS46pRZ8Y1W3IkTwKbm6vklL1NwZDvfceEC0baWU9nLzU1X5GActeS7VrOid4sJDbSTOJ64ImXGxiR/4+dJ4rxWZV1JU63schCEhXYCvVrng4zJrNxDySzPebi7S7kSKoxFYZ8XgLIz+OJ+Tvy6MSCg4jrQLv88x/c+8u4L74e581J0zukhZsSo+jz653soh9UCsJVCpl/lzsIUPkHzeCb5/jkF2V4EvS+3H2f4/xF7YQQ==) + +### Quick fix 3 - Put it in a function + +If the store call is outside a component, or in a component that isn't using ` +``` + +When Vue compiles this to `.js` it'll become something like this: + +```js +import { ref } from 'vue' + +export default { + setup() { + const msg = ref('') + // ... other stuff + } +} +``` + +Most of the code inside ` + + +``` + +- [Broken Playground example](https://play.vuejs.org/#eNp9VMtu2zAQ/BVCLWAZiKm8kEPgFG6LHFoUbdD0qEMYaeUwpkiCD8eA4X/vkrQeduAcbJA7y9nZIVfbrGVc0leb3Wa81co4siWVAebgq9ZkRxqjWjJZe5iU8ijhgUvO+hQddkNSOJ0AWjCtaWIoZaWkdQQj5G6ok+Nv2mGRqEdjkRzBQOIt5BHGfYq0ykuXTz7hejLNzvY9zFqmsSclsattKQkp94Ats1sSIyG2QFFFDWunlLAzpnlAy+zFOW1vi8JLvVrSSrXFu8TFDb2h14XgzwXYtuCyhg0WLLOzjjvKPMUXwcUlvUSOmluXAhSpZs9GvVkwh2RYflZDe1Jfhy/O6cU1vTiPwpKoNhAFnl0pd2iQs+hyw5dH9iCL5gLMH+043sKBTUwI9fYzxpzx0KuqXqBapXjDhB2AV7tJSh8MYC9rGLXimFmCS/D942/Y4LoHW1V7gdkfgH/BKuGDyJT2zcsadY/yotwf8bq5XP6z9xsH0nZdhQ6iHTE/Wvv9g94HuVf0emTj/kmjgXNbGa7dl9F4GGhOTQ6+4AeDjVTOPjploM+jhd7HZzYAo2HpAJyJ4+NhMEoJm8heQ8O8wCpBpwXndT7tOklMvEYOVJdfXF7FkwFqvKxC40QoVg8nSF+XNuCql5zXdM2EBzwYUDQiLQxWMnI4Fmj6lGRYKedFbxNuHLRa4HDjjpB5zddxgctn7xwqWVSCV6u7Motc2R4l5FdPPS9SamLAIQoU82JEjJd06OjRRw7d4hIOL6H7hvWWJtveXdrd+HQ+6epMzpILLPpp+2fU+TfyNhArAVSoZf4U2sKnSj5veb17GvzFP3yDcjfNdv8BeRfljQ==) + +We can easily fix this example by moving the `const products = useProductsStore()` line inside `setup` instead. + +## `import()` and lazy loading + +So far we've only considered static `import` statements, but what about `import()`? + +The most common use of `import()` in Vue applications is for lazy-loaded route components. e.g.: + +```js +export default createRouter({ + routes: [ + { + path: '/', + component: () => import('@/views/HomeView.vue') + }, + // ... + ] +}) +``` + +`import()` returns a Promise that resolves once the file is loaded. By using `import()`, we delay the importing of `HomeView.vue`, as well as any other files imported by `HomeView.vue`. The top-level code in those files won't run until they're loaded. + +While those files are loading, other code will continue to run, including the code in `main.js` that creates the Pinia instance. If one of the loaded files tries to use a store in top-level code it won't trigger the 'no active Pinia' error, as the Pinia instance will have already been created by that point. + +This can cause a lot of confusion, as top-level code can appear to work in some cases and then break for no apparent reason in other cases. + +Let's revisit an example we saw earlier: + +```js +// product-utils.js +import { useProductsStore } from '@/stores/products' + +const products = useProductsStore() + +export function fetchProduct(id) { + return products.fetch(id) +} +``` + +Inside our `HomeView.vue` we then have: + +```js +import { fetchProduct } from './product-utils' +``` + +This will seem to work fine, as `HomeView.vue` is a lazy-loaded route component and won't load until after the Pinia instance is created. + +Then we try to add that same import into another file, e.g. `App.vue`. Suddenly it blows up, 'no active Pinia'. + +As `App.vue` is imported directly into `main.js` using `import`, we now have a static import chain from `main.js` to `App.vue` to `product-utils.js`. The result is that the top-level code in `product-utils.js` is now running before the code in `main.js`. + +## Why does my code only work with HMR? + +You may find that everything seems to be working fine while you're working on your code, but it all suddenly stops working when you do a full page refresh. + +If you're working with a dev server like Vite that supports HMR (hot module replacement), it'll try to make small, incremental updates to the page, only updating the files you've edited. + +The Pinia instance will be created when the page first loads. If you edit one of your files to try to access a store it'll only reload that one file. As the Pinia instance already exists, no error is thrown. It's only when you reload the whole page that the code runs in the normal order, leading to the error. + +## The 'import app' hack + +Let's imagine we have code like this in our `main.js`: + +```js +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) + +app.mount('#app') +``` + +It's all pretty normal stuff, much like what you'd get with `npm create vue@latest`. + +Now let's assume that we're getting the 'no active Pinia' error. The router isn't using lazy loading, so something is getting pulled in that uses a store in top-level code. + +But if imported code runs first, can't we move code into an import to solve the problem? + +Yes, we can. This almost certainly isn't a good idea, but it can work. + +For example, let's move some of the code above into a file called `app-create.js`: + +```js +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' + +const app = createApp(App) +app.use(createPinia()) + +export { app } +``` + +Then, we could change our `main.js` to this: + +```js +import { app } from './app-create' +import router from './router' + +app.use(router) + +app.mount('#app') +``` + +The imports in `main.js` are processed in order, so the code in `app-create.js` runs first, before the router is imported. As a result, the line `app.use(createPinia())` runs before any code pulled in by the router, allowing stores to be used in top-level code. + +To reiterate, working around the problem like this is not recommended. It's a useful example from the perspective of understanding the theory, but if you find yourself using this as a real solution then you're probably in trouble. Relying on the side effects of importing a module is generally frowned upon, but it's doubly frowned upon when imports have to be used in a particular order. Hopefully it's clear that solving the problem this way is incredibly brittle. + +## Why does Pinia work this way? + +Pinia is used for global state management. But what do we mean by *global*? + +In the case of Pinia, it aims to be global in the same way that [`app.config.globalProperties`](https://vuejs.org/api/application.html#app-config-globalproperties) is global. They're only global within the context of a specific Vue application instance. + +Let's imagine you choose not to use Pinia and instead do something like this: + +```js +// store.js +import { reactive } from 'vue' + +export const state = reactive({}) +``` + +You could implement global state this way, but the state here would be shared between all instances of the application within a particular JavaScript environment. + +Why does this matter? Wouldn't each user be running the application in their own browser? Different browser, different JavaScript environment. + +But that's not necessarily the case. + +If you're using SSR then multiple requests will share the same Node process. That'll lead to the state being shared between those requests. If one request modifies the state then those changes will impact all subsequent requests. + +So when you make a call to `useProductsStore()`, you might be thinking that means *'give me my products store'*. But it's maybe a better mental model to think of it as *'give me the products store for the current application'*. + +That's where the Pinia instance comes in. When you write `app.use(createPinia())`, you're tying a Pinia instance to a specific application. That Pinia instance is then passed down to the components within the application using `app.provide()` and `app.config.globalProperties`. The Pinia instance is where it actually stores the state. + +Calling `useProductsStore()` will return a store that is bound to a specific Pinia instance. There are 3 ways it attempts to identify which Pinia instance to use: + +1. It can be passed in. e.g. `useProductsStore(pinia)`. +2. If it isn't passed in, the store will try to access the Pinia instance using `inject()`. +3. If neither of those are available, it tries to use the last active Pinia instance, hoping it'll be the right one. + +If you're using the Options API with helpers like `mapStores()` and `mapState()`, those make internal calls like `useProductsStore(this.$pinia)`. The `this.$pinia` part is the Pinia instance, coming from `globalProperties`. That explicitly tells the store which Pinia instance to use, rather than using `inject()`. + +If you're using the Composition API for your components, the store will typically use `inject()` to find the Pinia instance. That'll only work in places that support `inject()` calls, e.g. at the top level inside ` + + +``` + +So far, no store. Now let's take a look at `products.js`: + +```js +import { useProductsStore } from '@/stores/products' + +export async function loadProducts() { + const store = useProductsStore() + + // The specifics here don't really matter + if (!store.products) { + await store.loadAll() + } + + return store.products +} +``` + +The exact details of how `loadProducts()` uses the store don't really matter, what's important for this example is just that it needs access to the store. + +The store's inside a function, so we won't get the 'no active Pinia' error. But we still have the problem that the store isn't tied to the relevant application. `loadProducts()` is called from a click handler, so it can't use `inject()` to grab the Pinia instance. Instead, it'll rely on just grabbing the last active instance. + +As the code runs in a click handler it shouldn't cause a problem for SSR. The code will probably work just fine. But maybe we can do better. + +The relevant Vue pattern here involves introducing a composable. Specifically, a composable that splits the code into two phases that run at different times: + +```js +import { useProductsStore } from '@/stores/products' + +export function useProducts() { + const store = useProductsStore() + + async function loadProducts() { + if (!store.products) { + await store.loadAll() + } + + return store.products + } + + return { + loadProducts + } +} +``` + +Inside our component we'd then use it something like this: + +```vue + + + +``` + +Your first reaction might be that this just makes the code more complicated. But the pattern we've used here can be applied more generally. + +The key thing to appreciate is that the old `loadProducts()` is now split into two steps. The call to `useProductsStore()` happens upfront, during the component's setup phase. That allows it to use `inject()` to grab the Pinia instance. Anything returned by the `useProducts()` composable can then use that store, without needing to worry about ensuring it's bound to the appropriate Pinia instance. + +This is a common pattern for working with composables, not just stores. Composables often need to be called during the synchronous execution of `setup()`, so they can call `inject()`, lifecycle hooks, or other composables. That allows them to grab whatever context they need, based on the current component. They can then return functions for anything that needs to be triggered later. + +This composable pattern, with code split across two phases, is much more flexible than trying to do everything in a single function call. It often leads to more maintainable code, as the implementation can change significantly without needing to change the components that consume the composable. + +Whether it's right for your application is something you'll need to decide for yourself. diff --git a/docs/faq/running-old-projects.md b/docs/faq/running-old-projects.md index 60a13af..1475633 100644 --- a/docs/faq/running-old-projects.md +++ b/docs/faq/running-old-projects.md @@ -101,10 +101,12 @@ Thankfully, this is usually easy to fix. Use your package manager to remove `nod If you're using an older version of webpack with a new version of Node then you may see an error like this: > digital envelope routines::unsupported +> {.quote-code-error} Or: > digital envelope routines::initialization error +> {.quote-code-error} This includes Vue CLI projects, especially those on version 4.