#### Raw Vue SSR
-TODO: this part isn't built yet. You need to call `setActiveReq` with the _Request_ object before `useStore` is called
+In a Raw Vue SSR application you have to modify a few files to enable hydration and to tell requests apart.
+
+```js
+// entry-server.js
+import { getRootState, PiniaSsr } from "pinia";
+
+// install plugin to automatically use correct context in setup and onServerPrefetch
+Vue.use(PiniaSsr);
+
+export default context => {
+ /* ... */
+ context.rendered = () => {
+ // pass state to context
+ context.piniaState = getRootState(context.req);
+ };
+ /* ... */
+};
+```
+
+```html
+<!-- index.html -->
+<body>
+<!-- pass state from context to client -->
+{{{ renderState({ contextKey: 'piniaState', windowKey: '__PINIA_STATE__' }) }}}
+</body>
+```
+
+```js
+// entry-client.js
+import { setStateProvider } from "pinia";
+
+// inject ssr-state
+setStateProvider(() => window.__PINIA_STATE__);
+```
### Accessing other Stores
+/**
+ * @jest-environment node
+ */
+
import renderApp from './app/entry-server'
import { createRenderer } from 'vue-server-renderer'
const renderer = createRenderer()
+function createContext() {
+ return {
+ rendered: () => {},
+ req: {},
+ }
+}
+
describe('classic vue app', () => {
it('renders using the store', async () => {
- const context = {
- rendered: () => {},
- }
+ const context = createContext()
const app = await renderApp(context)
// @ts-ignore
- const html = await renderer.renderToString(app)
+ const html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)
})
it('resets the store', async () => {
- const context = {
- rendered: () => {},
- }
+ let context = createContext()
let app = await renderApp(context)
// @ts-ignore
- let html = await renderer.renderToString(app)
+ let html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)
- // render again
+ // render again with new request context
+ context = createContext()
app = await renderApp(context)
// @ts-ignore
- html = await renderer.renderToString(app)
+ html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)
import { useStore } from './store'
export default defineComponent({
+ async serverPrefetch() {
+ const store = useStore()
+ store.state.counter++
+ },
+
setup() {
const store = useStore()
+import Vue from 'vue'
import { createApp } from './main'
+import { PiniaSsr, getRootState } from '../../../src'
+
+Vue.use(PiniaSsr)
export default function(context: any) {
return new Promise(resolve => {
- const { app, store } = createApp()
+ const { app } = createApp()
// This `rendered` hook is called when the app has finished rendering
context.rendered = () => {
// When we attach the state to the context, and the `template` option
// is used for the renderer, the state will automatically be
// serialized and injected into the HTML as `window.__INITIAL_STATE__`.
- context.state = store.state
+ context.state = getRootState(context.req)
}
resolve(app)
import Vue from 'vue'
// import VueCompositionApi from '@vue/composition-api'
import App from './App'
-import { useStore } from './store'
-import { setActiveReq } from '../../../src'
// Done in setup.ts
// Vue.use(VueCompositionApi)
export function createApp() {
- // create router and store instances
- setActiveReq({})
- const store = useStore()
-
- store.state.counter++
-
// create the app instance, injecting both the router and the store
const app = new Vue({
render: h => h(App),
})
// expose the app, the router and the store.
- return { app, store }
+ return { app }
}
--- /dev/null
+import Vue from 'vue'
+import { PiniaSsr } from '../../src'
+
+it('should warn when installed in the browser', () => {
+ const mixinSpy = jest.spyOn(Vue, 'mixin')
+ const warnSpy = jest.spyOn(console, 'warn')
+ Vue.use(PiniaSsr)
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringMatching(/seems to be used in the browser bundle/i)
+ )
+ expect(mixinSpy).not.toHaveBeenCalled()
+})
// @ts-check
import Vue from 'vue'
// @ts-ignore: this must be pinia to load the local module
-import { setActiveReq, setStateProvider, getRootState } from 'pinia'
+import { setActiveReq, PiniaSsr, setStateProvider, getRootState } from 'pinia'
-Vue.mixin({
- beforeCreate() {
- // @ts-ignore
- const { setup, serverPrefetch } = this.$options
- if (setup) {
- // @ts-ignore
- this.$options.setup = (props, context) => {
- if (context.ssrContext && context.ssrContext.req) {
- setActiveReq(context.ssrContext.req)
- }
-
- return setup(props, context)
- }
- }
-
- if (process.server && serverPrefetch) {
- const patchedServerPrefetch = Array.isArray(serverPrefetch)
- ? serverPrefetch.slice()
- : [serverPrefetch]
-
- for (let i = 0; i < patchedServerPrefetch.length; i++) {
- const original = patchedServerPrefetch[i]
- /**
- * @type {(this: import('vue').default) => any}
- */
- patchedServerPrefetch[i] = function() {
- setActiveReq(this.$ssrContext.req)
-
- return original.call(this)
- }
- }
-
- // @ts-ignore
- this.$options.serverPrefetch = patchedServerPrefetch
- }
- },
-})
+if (process.server) {
+ Vue.use(PiniaSsr)
+}
/** @type {import('@nuxt/types').Plugin} */
const myPlugin = context => {
export { createStore } from './store'
export { setActiveReq, setStateProvider, getRootState } from './rootStore'
export { StateTree, StoreGetter, Store } from './types'
+export { PiniaSsr } from './ssrPlugin'
--- /dev/null
+import { VueConstructor } from 'vue/types'
+import { setActiveReq } from './rootStore'
+
+export const PiniaSsr = (vue: VueConstructor) => {
+ const isServer = typeof window === 'undefined'
+
+ if (!isServer) {
+ console.warn(
+ '`PiniaSsrPlugin` seems to be used in the browser bundle. You should only call it on the server entry: https://github.com/posva/pinia#raw-vue-ssr'
+ )
+ return
+ }
+
+ vue.mixin({
+ beforeCreate() {
+ const { setup, serverPrefetch } = this.$options
+ if (setup) {
+ this.$options.setup = (props, context) => {
+ // @ts-ignore
+ setActiveReq(context.ssrContext.req)
+ return setup(props, context)
+ }
+ }
+
+ if (serverPrefetch) {
+ const patchedServerPrefetch = Array.isArray(serverPrefetch)
+ ? serverPrefetch.slice()
+ : // serverPrefetch not being an array cannot be triggered due tue options merge
+ // https://github.com/vuejs/vue/blob/7912f75c5eb09e0aef3e4bfd8a3bb78cad7540d7/src/core/util/options.js#L149
+ /* istanbul ignore next */
+ [serverPrefetch]
+
+ for (let i = 0; i < patchedServerPrefetch.length; i++) {
+ const original = patchedServerPrefetch[i]
+ patchedServerPrefetch[i] = function() {
+ // @ts-ignore
+ setActiveReq(this.$ssrContext.req)
+
+ return original.call(this)
+ }
+ }
+
+ // @ts-ignore
+ this.$options.serverPrefetch = patchedServerPrefetch
+ }
+ },
+ })
+}