Skip to content

fix: throw when a store is used outside of a Nuxt-aware context. #2857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
</p>
<br/>
<p align="center">
<a href="https://npmjs.com/package/pinia"><img src="https://badgen.net/npm/v/pinia" alt="npm package"></a>
<a href="https://github.com/vuejs/pinia/actions/workflows/ci.yml"><img src="https://github.com/vuejs/pinia/actions/workflows/ci.yml/badge.svg" alt="build status"></a>
<a href="https://codecov.io/gh/vuejs/pinia"><img src="https://codecov.io/gh/vuejs/pinia/graph/badge.svg?token=rU2xxQ6BGH"/></a>
<a href="https://npmjs.com/package/pinia"><img src="https://badgen.net/npm/v/pinia/v2" alt="npm package"></a>
<a href="https://github.com/vuejs/pinia/actions/workflows/ci.yml"><img src="https://github.com/vuejs/pinia/actions/workflows/ci.yml/badge.svg?branch=v2" alt="build status"></a>
<a href="https://codecov.io/gh/vuejs/pinia"><img src="https://codecov.io/gh/vuejs/pinia/branch/v2/graph/badge.svg?token=rU2xxQ6BGH"></a>
</p>
<br/>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinia/root",
"packageManager": "pnpm@9.15.3",
"packageManager": "pnpm@10.2.0",
"type": "module",
"private": true,
"workspaces": [
Expand Down
4 changes: 4 additions & 0 deletions packages/docs/.vitepress/config/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
},
],
},
{
text: 'v2.x',
items: [{ text: 'v3.x', link: 'https://pinia.vuejs.org' }],
},
],

sidebar: {
Expand Down
9 changes: 4 additions & 5 deletions packages/docs/.vitepress/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,11 @@ export const sharedConfig = defineConfig({
},

search: {
provider: 'algolia',
provider: 'local',
options: {
appId: '69Y3N7LHI2',
apiKey: '45441f4b65a2f80329fd45c7cb371fea',
indexName: 'pinia',
locales: { ...zhSearch },
locales: {
...zhSearch,
},
},
},

Expand Down
4 changes: 4 additions & 0 deletions packages/docs/.vitepress/config/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
},
],
},
{
text: 'v2.x',
items: [{ text: 'v3.x', link: 'https://pinia.vuejs.org' }],
},
],
sidebar: {
'/zh/api/': [
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/core-concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ You can define as many stores as you want and **you should define each store in

Once the store is instantiated, you can access any property defined in `state`, `getters`, and `actions` directly on the store. We will look at these in detail in the next pages but autocompletion will help you.

Note that `store` is an object wrapped with `reactive`, meaning there is no need to write `.value` after getters but, like `props` in `setup`, **we cannot destructure it**:
Note that `store` is an object wrapped with `reactive`, meaning there is no need to write `.value` after getters but, like any `reactive()` object in Vue, [**we lose reactivity when destructuring it**](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive):

```vue
<script setup>
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/core-concepts/outside-component-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const router = createRouter({
})

// ❌ Depending on the order of imports this will fail
const store = useStore()
const store = useUserStore()

router.beforeEach((to, from, next) => {
// we wanted to use the store here
Expand All @@ -53,7 +53,7 @@ router.beforeEach((to, from, next) => {
router.beforeEach((to) => {
// ✅ This will work because the router starts its navigation after
// the router is installed and pinia will be installed too
const store = useStore()
const store = useUserStore()

if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
Expand Down
9 changes: 5 additions & 4 deletions packages/docs/ssr/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,23 @@ And that's it, use your store as usual!

## Awaiting for actions in pages

As with `onServerPrefetch()`, you can call a store action within `asyncData()`. Given how `useAsyncData()` works, **make sure to return a value**. This will allow Nuxt to skip running the action on the client side and reuse the value from the server.
As with `onServerPrefetch()`, you can call a store action within the `callOnce()` composable.
This will allow Nuxt to run the action only once and avoids refetching data that is already present.

```vue{3-4}
<script setup>
const store = useStore()
// we could also extract the data, but it's already present in the store
await useAsyncData('user', () => store.fetchUser())
await callOnce('user', () => store.fetchUser())
</script>
```

If your action doesn't resolve a value, you can add any non nullish value:
Depending on your requirements, you can choose to run the action only once on the client, or on every navigation (which is closer to data fetching behavior of `useFetch()`/`useAsyncData()`)

```vue{3}
<script setup>
const store = useStore()
await useAsyncData('user', () => store.fetchUser().then(() => true))
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' })
</script>
```

Expand Down
2 changes: 2 additions & 0 deletions packages/nuxt/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Global compile-time constants
declare var __TEST__: boolean
24 changes: 24 additions & 0 deletions packages/nuxt/playground/pages/usage-after-await.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts" setup>
const useFancyCounter = async () => {
await new Promise((resolve) => setTimeout(resolve, 0))

// ❌ bad usage: the use of a store after an await could lead to using the wrong pinia instance.
return useCounter()
}

const event = useRequestEvent()
useNuxtApp().hook('vue:error', (error) => {
if (event) {
setResponseStatus(event, 500, String(error))
}
})

const counter = await useFancyCounter()
</script>

<template>
<div>
<p>Count: {{ counter.$state.count }}</p>
<button @click="counter.increment()">+</button>
</div>
</template>
29 changes: 28 additions & 1 deletion packages/nuxt/src/runtime/composables.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
import { useNuxtApp } from '#app'
import {
defineStore as _defineStore,
type Pinia,
type StoreGeneric,
} from 'pinia'
export * from 'pinia'

export const usePinia = () => useNuxtApp().$pinia
export const usePinia = () => useNuxtApp().$pinia as Pinia | undefined

export const defineStore: typeof _defineStore =
process.env.NODE_ENV === 'production' && !__TEST__
? _defineStore
: (...args: [idOrOptions: any, setup?: any, setupOptions?: any]) => {
if (!import.meta.server) {
return _defineStore(...args)
}

const originalUseStore = _defineStore(...args)
function useStore(
pinia?: Pinia | null,
hot?: StoreGeneric
): StoreGeneric {
return originalUseStore(pinia || usePinia(), hot)
}

useStore.$id = originalUseStore.$id
useStore._pinia = originalUseStore._pinia

return useStore
}
6 changes: 6 additions & 0 deletions packages/nuxt/test/nuxt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ describe('works with nuxt', async () => {
expect(html).not.toContain('I should not be serialized or hydrated')
expect(html).toContain('skipHydrate-wrapped state is correct')
})

it('throws an error server-side when the nuxt context is not available', async () => {
await expect($fetch('/usage-after-await')).rejects.toThrowError(
'[nuxt] instance unavailable'
)
})
})
1 change: 1 addition & 0 deletions packages/nuxt/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./playground/.nuxt/tsconfig.json",
"include": [
"./shims.d.ts",
"./global.d.ts",
// missing in the playground
"./src"
]
Expand Down
6 changes: 6 additions & 0 deletions packages/testing/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### [0.1.8](https://github.com/vuejs/pinia/compare/@pinia/testing@0.1.7...@pinia/testing@0.1.8) (2025-02-11)

### Features

- **testing:** warn about incorrect createSpy ([394f655](https://github.com/vuejs/pinia/commit/394f6553d13f2b46c6e52a68145c24699b98e7fa)), closes [#2896](https://github.com/vuejs/pinia/issues/2896)

## [0.1.7](https://github.com/vuejs/pinia/compare/@pinia/testing@0.1.6...@pinia/testing@0.1.7) (2024-11-03)

No code changes in this release.
Expand Down
2 changes: 1 addition & 1 deletion packages/testing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinia/testing",
"version": "0.1.7",
"version": "0.1.8",
"description": "Testing module for Pinia",
"keywords": [
"vue",
Expand Down
3 changes: 1 addition & 2 deletions scripts/release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ import semver from 'semver'
import prompts from '@posva/prompts'
import { execa } from 'execa'
import pSeries from 'p-series'
import { globby } from 'globby'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const args = minimist(process.argv.slice(2))
const {
skipBuild,
tag: optionTag,
tag: optionTag = 'legacy',
dry: isDryRun,
skipCleanCheck: skipCleanGitCheck,
noDepsUpdate,
Expand Down