Skip to content

Conversation

@pguilbert
Copy link

@pguilbert pguilbert commented Dec 11, 2024

Context

The Pinia documentation states the following:

Make sure all of your useStore() calls appear before any await. Otherwise, this could lead to using the wrong Pinia instance in SSR apps: pinia.vuejs.org/cookbook/composing-stores.html#Shared-Actions

We recently discovered that we had cross-request pollution caused by bad use of Pinia stores after an await in Nuxt. With a large codebase, it was tough to track down every instance of the issue. So, we ended up creating a custom version of defineStore, similar to the one in this PR, to help prevent developers from misusing Pinia stores.

I think it would be useful if this could be implemented directly in the Pinia Nuxt module.

Description of the changes

The Nuxt Pinia module does the following for each request:

// Create the instance for this request
const pinia = createPinia()
// Store the instance within the Nuxt context (mainly for serialization after)
nuxtApp.vueApp.use(pinia)
// Set the Pinia global instance (that will be used by Pinia and is subject to cross-request pollution)
setActivePinia(pinia)

The module also provides a helper usePinia() that retrieves the Pinia instance from the Nuxt context and not from the Pinia global variable. Conveniently, this helper will throw an error if used after an await (outside of a Nuxt-aware context)!

This PR ensures that on the server we always use usePinia() to get the current Pinia instance. By doing so useStore will systematically throw if used after an await on the server (similarly to the useNuxtApp composable)

Breaking change

Server-side, using a store outside a Nuxt-aware context will now result in an error. Previously, it would continue without error, but sometimes, the wrong Pinia instance was used.

Error in dev:

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function

Error in prod:

[nuxt] instance unavailable

Summary by CodeRabbit

  • New Features

    • Stores now work reliably in server-side rendering environments with improved context awareness and error handling
    • Enhanced error messages when stores are accessed outside the proper Nuxt context
  • Documentation

    • Added example demonstrating store usage patterns in server-side rendering scenarios
  • Tests

    • Added test coverage for store behavior across various server-side rendering contexts

@netlify
Copy link

netlify bot commented Dec 11, 2024

Deploy Preview for pinia-playground canceled.

Name Link
🔨 Latest commit 5cf66cf
🔍 Latest deploy log https://app.netlify.com/sites/pinia-playground/deploys/6814c644d2bf3400081cf1b7

@netlify
Copy link

netlify bot commented Dec 11, 2024

Deploy Preview for pinia-official canceled.

Name Link
🔨 Latest commit 05d6bf7
🔍 Latest deploy log https://app.netlify.com/projects/pinia-official/deploys/6908b14f74ab970008630be0

Copy link
Member

@posva posva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Do you have a reproduction of the problem where correctly using the useStore() composable returned by defineStore() doesn't work?

@pguilbert
Copy link
Author

Thanks for the PR! Do you have a reproduction of the problem where correctly using the useStore() composable returned by defineStore() doesn't work?

"correctly" no. The purpose of this PR is to ease the detection of incorrect usage. In the current version bad usage of a store after a await will work without warning in low-traffic environments. But, in cases of simultaneous requests, there is a possibility that pinia uses the pinia instance from another request.

I can provide a reproduction if needed, but this is already a documented case:

Make sure all of your useStore() calls appear before any await. Otherwise, this could lead to using the wrong Pinia instance in SSR apps: pinia.vuejs.org/cookbook/composing-stores.html#Shared-Actions

For context: We recently found out that we had cross-request pollution caused by bad use of Pinia in Nuxt. With our large codebase, it was tough to track down every instance of the issue. So, we ended up creating a custom version of defineStore, similar to this one, to help prevent developers from misusing Pinia stores.

(I'll update the PR description with those infos.)

@pguilbert pguilbert changed the title fix: throw when a store is used outside of a Nuxt-aware context. (POC) fix: throw when a store is used outside of a Nuxt-aware context. Dec 11, 2024
@posva
Copy link
Member

posva commented Dec 12, 2024

Thanks for the great description of the problem! I agree it's a nice addition

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 12, 2024

Open in StackBlitz

npm i https://pkg.pr.new/@pinia/nuxt@2857
npm i https://pkg.pr.new/pinia@2857
npm i https://pkg.pr.new/@pinia/testing@2857

commit: 05d6bf7

@codecov
Copy link

codecov bot commented Dec 12, 2024

Codecov Report

Attention: Patch coverage is 0% with 13 lines in your changes missing coverage. Please review.

Project coverage is 87.96%. Comparing base (2071db2) to head (2a0c9ed).

Files with missing lines Patch % Lines
packages/nuxt/src/runtime/composables.ts 0.00% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               v2    #2857      +/-   ##
==========================================
- Coverage   88.75%   87.96%   -0.79%     
==========================================
  Files          19       19              
  Lines        1449     1462      +13     
  Branches      226      226              
==========================================
  Hits         1286     1286              
- Misses        162      175      +13     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pguilbert
Copy link
Author

Thanks for the great description of the problem! I agree it's a nice addition

Nice! I'll complete the typing and refine the test to move this PR out of draft.

@pguilbert pguilbert force-pushed the error-usage-after-await branch from 2a0c9ed to d357d58 Compare December 13, 2024 17:02
@pguilbert pguilbert marked this pull request as ready for review December 16, 2024 13:47
@pguilbert pguilbert requested a review from posva December 17, 2024 08:48
@pguilbert
Copy link
Author

@posva what do you think of this version? Could we merge this? 🙏

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

export const defineStore: typeof _defineStore = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the error dev only, it shouldn't affect the final bundle. This should be possible by using process.env.NODE_ENV === 'production' ? _defineStore : ... or a similar guard

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it took so long; it's done now!

@github-project-automation github-project-automation bot moved this to 🆕 Triaging in Pinia Roadmap Apr 14, 2025
@posva posva moved this from 🆕 Triaging to 🧑‍💻 In progress in Pinia Roadmap Apr 14, 2025
@pguilbert pguilbert force-pushed the error-usage-after-await branch from d357d58 to 1f3d17a Compare April 30, 2025 13:22
@pguilbert pguilbert requested a review from posva May 2, 2025 13:22
@posva posva changed the base branch from v2 to v3 June 3, 2025 05:15
@iPrytz
Copy link

iPrytz commented Oct 14, 2025

Thanks for this @pguilbert ! I think this is a great addition to Pinia. Is this planned to merge any time soon? 😊 @posva

posva and others added 8 commits November 3, 2025 14:24
Prefer the Nuxt Pinia instance over the global active Pinia instance. Since the Nuxt Pinia instance is discarded after each request, it ensures that we can't accidentally use one from another request. Additionally, `usePinia` will throw an error when used outside of a Nuxt-aware context.

The error is as follows in dev :
> [nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.
@posva posva force-pushed the error-usage-after-await branch from 5cf66cf to 05d6bf7 Compare November 3, 2025 13:42
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

Walkthrough

Implements SSR-safe store handling by wrapping defineStore to defer store creation to server contexts, adds global __TEST__ TypeScript declaration, includes a playground page demonstrating async store usage patterns, and adds corresponding error handling test cases.

Changes

Cohort / File(s) Summary
TypeScript Configuration
packages/nuxt/tsconfig.json
Added ./global.d.ts to the include array to register global TypeScript declarations.
Global Type Declarations
packages/nuxt/global.d.ts
Declared global __TEST__ boolean constant for test environment identification.
Runtime Store Composables
packages/nuxt/src/runtime/composables.ts
Enhanced defineStore with SSR-aware wrapper that proxies store instantiation via usePinia() in non-production environments. Updated usePinia return type to Pinia | undefined.
Test Suite
packages/nuxt/test/nuxt.spec.ts
Added server-side test case validating error handling when accessing /usage-after-await without Nuxt context.
Playground Demo
packages/nuxt/playground/pages/usage-after-await.vue
Created new Vue SFC demonstrating async useFancyCounter composition with error hook registration and server status handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant defineStore as SSR defineStore
    participant Pinia
    
    alt Non-Production/Test Environment
        Client->>Server: Request store instance
        Server->>defineStore: useStore() called
        defineStore->>defineStore: Check if server context
        defineStore->>Pinia: Defer to usePinia()
        Pinia-->>defineStore: Return Pinia instance
        defineStore->>defineStore: Proxy store creation
        defineStore-->>Server: Return store via wrapper
    else Production Environment
        Client->>defineStore: useStore() called
        defineStore->>Pinia: Direct _defineStore call
        Pinia-->>defineStore: Return store
        defineStore-->>Client: Return store instance
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • packages/nuxt/src/runtime/composables.ts requires careful attention to the SSR-aware wrapper logic, conditional compilation based on environment, and metadata preservation ($id, _pinia properties).
  • packages/nuxt/playground/pages/usage-after-await.vue demonstrates the intended usage pattern but interaction with error hooks and async store initialization should be verified.
  • Cross-file consistency between the new global declaration, test assertions, and playground usage should be validated.

Poem

🐰 A store that knows when to defer,
Through server realms it gently sways,
With __TEST__ flags to help confer,
And error hooks that light the ways!
SSR safety springs to life,
No more context strife! ✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'fix: throw when a store is used outside of a Nuxt-aware context.' directly and accurately describes the main change of the PR. The implementation adds an SSR-safe wrapper around defineStore that ensures usePinia() is always used on the server, causing stores accessed after an await (outside Nuxt-aware context) to throw an error instead of silently failing. The title captures the core fix and its purpose clearly.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 126e031 and 05d6bf7.

📒 Files selected for processing (5)
  • packages/nuxt/global.d.ts (1 hunks)
  • packages/nuxt/playground/pages/usage-after-await.vue (1 hunks)
  • packages/nuxt/src/runtime/composables.ts (1 hunks)
  • packages/nuxt/test/nuxt.spec.ts (1 hunks)
  • packages/nuxt/tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/nuxt/global.d.ts (1)
rollup.config.mjs (1)
  • __TEST__ (176-179)
packages/nuxt/src/runtime/composables.ts (1)
rollup.config.mjs (1)
  • __TEST__ (176-179)
🔇 Additional comments (9)
packages/nuxt/src/runtime/composables.ts (3)

2-6: LGTM: Import structure supports the wrapper pattern.

The imports correctly bring in the original defineStore (renamed to _defineStore) and necessary types for implementing the SSR-aware wrapper.


9-9: LGTM: Correctly typed to handle context unavailability.

The | undefined return type is essential for detecting when usePinia() is called outside a Nuxt-aware context (e.g., after an await). When undefined is passed to the store, it will trigger the intended error.


11-33: Store definition property copying is complete and correct.

The $id and _pinia properties copied on lines 29-30 comprise the complete set of properties defined on the StoreDefinition interface in Pinia's types (packages/pinia/src/types.ts:494-525). The interface defines exactly these two properties:

  • $id: The store identifier, used by Pinia's map helpers and HMR
  • _pinia: The dev-only Pinia instance for hot module replacement
packages/nuxt/tsconfig.json (1)

5-5: LGTM: Correctly includes global type declarations.

Adding global.d.ts to the include array ensures the global __TEST__ constant is visible throughout the TypeScript project.

packages/nuxt/test/nuxt.spec.ts (1)

37-41: LGTM: Test correctly validates SSR context enforcement.

The test case properly verifies that using a store after an await (outside the Nuxt context) throws the expected error. The test includes the error message as requested in past review feedback, providing stricter validation.

Note: The error message '[nuxt] instance unavailable' comes from Nuxt's useNuxtApp() when called outside a valid context, which is the intended behavior when usePinia() is invoked after an await.

packages/nuxt/global.d.ts (1)

1-2: LGTM: Global compile-time constant correctly declared.

The __TEST__ global boolean is properly declared for use as a compile-time feature flag. This enables the SSR-safe wrapper in test environments while excluding it from production builds (as seen in composables.ts line 12).

packages/nuxt/playground/pages/usage-after-await.vue (3)

2-7: LGTM: Correctly demonstrates the problematic pattern for testing.

The useFancyCounter function intentionally demonstrates the "bad usage" pattern (accessing a store after an await) that this PR aims to detect. The comment on line 5 clearly marks this as incorrect usage, and this page serves as a test case to verify that the error is properly thrown and handled.


9-14: LGTM: Error handling correctly propagates for test validation.

The error hook properly captures the error thrown when the store is accessed outside the Nuxt context and sets the response status to 500. This allows the test in nuxt.spec.ts to verify the error behavior via the rejected $fetch promise.

The defensive check if (event) on line 11 is good practice, though the event should always be available on the server side.


16-16: LGTM: Triggers the SSR context error as intended.

Awaiting useFancyCounter() on line 16 will cause the store to be accessed after losing the Nuxt context, triggering the error that this PR is designed to catch. This is the critical line that validates the SSR-safety enforcement.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@@ -0,0 +1,2 @@
// Global compile-time constants
declare var __TEST__: boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way to get rid of this? I don't think we should need this

: (((
...args: Parameters<typeof _defineStore>
): ReturnType<typeof _defineStore> => {
if (!import.meta.server) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition can be probably be moved up so we simply return _defineStore too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🧑‍💻 In progress

Development

Successfully merging this pull request may close these issues.

3 participants