Rendering ignores Pinia data in `initialState` when the store state is set in `beforeEnter` route guards
See original GitHub issueIn my application I’d like to initialise state on a per-route basis rather than all in one go up-front. This is nice because all pages in the application load their dependent data in the same way. So regardless of how you got there, initial page load (SSG or client-side) or a client-side navigation to the route, we have just one place that loads the data. To achieve this I’m using the beforeEnter
guard to initialise my Pinia store before the component mounts and the route is resolved.
The issue I’m having is that although the route guard is executed and the data is indeed fetched and set at build time, the page does not get rendered properly.
When I added some crude logging to see what was going on, I realised that there were multiple instances of the store. The instance which was initialised and loaded the data was somehow not the same instance present when the page was rendered out. This is not true if the page is the last route in the list - in that case the store is the same and it renders correctly.
Reproduction
First I create the basic store. Note how each instance of the store has a unique randomInit
value which I can use to keep track of what’s going on - it’ll make sense in a minute!
import { defineStore } from 'pinia'
interface Ship {
id: string
name: string
}
interface State {
randomInit: string
hasLoaded: boolean
ships: Ship[],
}
export const useShipsStore = defineStore('ships', {
state: (): State => ({
randomInit: `${Date.now()}_${(Math.round(Math.random() * 1000))}`,
hasLoaded: false,
ships: [],
}),
actions: {
async initialise() {
if (this.hasLoaded)
return
console.log(`loading fresh ships! state ID: ${this.randomInit}`)
const res = await fetch('https://swapi.dev/api/starships/')
const body = await res.json()
this.ships= body.results
this.hasLoaded= true
},
},
})
Here’s the main.ts
file. Note how I log the store’s randomInit
when the SSG data is being set.
import 'isomorphic-unfetch'
import devalue from '@nuxt/devalue'
import { ViteSSG } from 'vite-ssg'
import { createPinia } from 'pinia'
import { useStarshipStore } from '@/stores/ships'
import App from '@/App.vue'
import Account from '@/pages/Account.vue'
import Ships from '@/pages/Ships.vue'
import Home from '@/pages/Home.vue'
const routes = [
{
path: '/',
component: Home,
},
{
path: '/starships',
component: Starships,
beforeEnter: async (to, from) => {
const starshipStore = useStarshipStore()
await starshipStore.initialise()
},
},
{
path: '/account',
component: Account,
},
]
async function bootstrapPage(context) {
const { app, initialState, router, routePath, isClient } = context
const pinia = createPinia()
app.use(pinia)
const starshipStore = useStarshipStore()
if (isClient) {
// on the client side, we restore the state
pinia.state.value = initialState?.pinia || {}
} else {
// at build time set the hydration data to whatever is in the store
// this will be stringified and set to window.__INITIAL_STATE__
initialState.pinia = pinia.state.value
console.log(`setting SSG data: ${routePath} @ ${shipsStore.randomInit}`)
}
}
function transformState(state) {
return import.meta.env.SSR ? devalue(state) : state
}
export const createApp = ViteSSG(App, { routes }, bootstrapPage, { transformState })
Expected behaviour
starships.html
file in the dist directory has fully rendered the template and has the starship data in thewindow.__INITIAL_STATE__
object- output log:
[vite-ssg] Build for server...
vite v2.8.4 building SSR bundle for production...
✓ 22 modules transformed.
.vite-ssg-temp/main.mjs 26.08 KiB
setting SSG data: undefined @ 1646180624980_753
[vite-ssg] Rendering Pages... (5)
setting SSG data: /starships @ 1646180624988_22
loading fresh ships! state ID: 1646180624988_22
setting SSG data: / @ 1646180625005_628
setting SSG data: /account @ 1646180625007_552
dist/index.html 0.96 KiB
dist/account.html 0.93 KiB
dist/ships.html 1.43 KiB
Actual behaviour
starships.html
in the dist directory has an empty template and the initial store state in thewindow.__INITIAL_STATE__
object- output log:
[vite-ssg] Build for server...
vite v2.8.4 building SSR bundle for production...
✓ 22 modules transformed.
.vite-ssg-temp/main.mjs 26.03 KiB
setting SSG data: undefined @ 1646181850722_607
[vite-ssg] Rendering Pages... (5)
setting SSG data: / @ 1646181850726_738
setting SSG data: /ships @ 1646181850727_588
setting SSG data: /account @ 1646181850729_24
loading fresh ships! state ID: 1646181850729_24
dist/index.html 0.96 KiB
dist/account.html 0.93 KiB
dist/ships.html 8.07 KiB
Note how the randomInit
values are the same for the account page iteration but also somehow the initialisation of the store, which happens in a different route!
This strikes me as some kind of race condition or concurrency issue where the store is not being tracked properly on each iteration of the generation process.
So I took a look…
The fix
I’m not really sure why or how this works, but there is a way to reliably fix it.
Option one (first thing I tried): disable parallel rendering. In src/node/build.ts
:
await Promise.all(
routesPaths.map(async(route) => {
try {
const appCtx = await createApp(false, route) as ViteSSGContext<true>
Changing the loop code to a simple for (const route of routePaths) {
does solve it. But, this is not satisfactory for me - it would be far too slow! So I had another idea…
Option two (major hax): I found that doing any async operation before the call to createApp
would fix it.
await Promise.all(
routesPaths.map(async(route) => {
await new Promise(resolve => setImmediate(resolve));
try {
const appCtx = await createApp(false, route) as ViteSSGContext<true>
Yay! This is brilliant. Now each page in my app can use the route guard to get all its dependent data, whether you’re navigating on the client, or it’s the initial page load, or it’s the server building the page during SSG! Much nicer than doing everything in the vite-ssg callback function!
Reflection
Doing some aysnc work forces the next part (creating the app instance, registering the store) to the next tick of the event loop. Maybe this fixes something in Pinia so that the store instance is consistent during the render.
But I’m not quite sure why it’s an issue at all, or why this “fix” works. Surely there is a less hacky way to get this behaviour.
Issue Analytics
- State:
- Created 2 years ago
- Comments:11 (5 by maintainers)
Top GitHub Comments
To me this sounds like an issue with pinia, and possibly unsupported use of it (the docs only describe using stores inside setup functions, not route guards). It is a bit odd that your hack resolves it. But yeah I think doing data fetching inside setup yields a better result anyway so I’d just stick with doing it there.
I’ve set up Pinia as JAM Stack website cache (Vue3 + SSG). I could not get the state loaded with
initialState.pinia = pinia.state.value
, so I called it as.load()
and made the store initialize its own values.Hopefully it helps someone.