diff --git a/docs/content/3.docs/1.usage/4.data-fetching.md b/docs/content/3.docs/1.usage/1.data-fetching.md
similarity index 100%
rename from docs/content/3.docs/1.usage/4.data-fetching.md
rename to docs/content/3.docs/1.usage/1.data-fetching.md
diff --git a/docs/content/3.docs/1.usage/2.state.md b/docs/content/3.docs/1.usage/2.state.md
new file mode 100644
index 0000000000..0a9a4d05e8
--- /dev/null
+++ b/docs/content/3.docs/1.usage/2.state.md
@@ -0,0 +1,40 @@
+# State
+
+Nuxt provides `useState` to create a globally shared state.
+
+## `useState`
+
+Within your pages, components and plugins you can use `useState`. It can be used to create your own store implementation.
+You can think of it as a ssr-friendly ref that it's value will be hydrated (preserved) after Server-Side rendering and it is shared across all components.
+
+
+### Usage
+
+```js
+useState(key: string)
+```
+
+* **key**: a unique key to ensure that data fetching can be properly de-duplicated across requests
+
+### Example
+
+In this example, we use a server-only plugin to find about request locale.
+
+```ts [plugins/locale.server.ts]
+import { defineNuxtPlugin, useState } from '#app'
+
+export default defineNuxtPlugin((nuxt) => {
+ const locale = useState('locale')
+ locale.value = nuxt.ssrContext.req.headers['accept-language']?.split(',')[0]
+})
+```
+
+```vue
+
+
+
+ Current locale: {{ locale }}
+
+```
diff --git a/docs/content/3.docs/1.usage/4.meta-tags.md b/docs/content/3.docs/1.usage/3.meta-tags.md
similarity index 100%
rename from docs/content/3.docs/1.usage/4.meta-tags.md
rename to docs/content/3.docs/1.usage/3.meta-tags.md
diff --git a/examples/use-state/nuxt.config.ts b/examples/use-state/nuxt.config.ts
new file mode 100644
index 0000000000..a3e4d68096
--- /dev/null
+++ b/examples/use-state/nuxt.config.ts
@@ -0,0 +1,4 @@
+import { defineNuxtConfig } from 'nuxt3'
+
+export default defineNuxtConfig({
+})
diff --git a/examples/use-state/package.json b/examples/use-state/package.json
new file mode 100644
index 0000000000..120bb17011
--- /dev/null
+++ b/examples/use-state/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "example-use-state",
+ "private": true,
+ "devDependencies": {
+ "nuxt3": "latest"
+ },
+ "scripts": {
+ "dev": "nuxt dev",
+ "build": "nuxt build",
+ "start": "node .output/server/index.mjs"
+ }
+}
diff --git a/examples/use-state/pages/index.vue b/examples/use-state/pages/index.vue
new file mode 100644
index 0000000000..9e088ab06f
--- /dev/null
+++ b/examples/use-state/pages/index.vue
@@ -0,0 +1,13 @@
+
+
+ Locale: {{ locale }}
+
+
+
+
+
diff --git a/examples/use-state/plugins/locale.server.ts b/examples/use-state/plugins/locale.server.ts
new file mode 100644
index 0000000000..52ed70d8b6
--- /dev/null
+++ b/examples/use-state/plugins/locale.server.ts
@@ -0,0 +1,6 @@
+import { useState } from '#app'
+
+export default defineNuxtPlugin((nuxt) => {
+ const locale = useState('locale')
+ locale.value = nuxt.ssrContext.req.headers['accept-language']?.split(',')[0]
+})
diff --git a/packages/bridge/src/runtime/app.plugin.mjs b/packages/bridge/src/runtime/app.plugin.mjs
index 0762815b07..b3f3cdbc87 100644
--- a/packages/bridge/src/runtime/app.plugin.mjs
+++ b/packages/bridge/src/runtime/app.plugin.mjs
@@ -1,6 +1,6 @@
import Vue from 'vue'
import { createHooks } from 'hookable/dist/index.mjs'
-import { setNuxtInstance } from '#app'
+import { setNuxtAppInstance } from '#app'
export default (ctx, inject) => {
const nuxt = {
@@ -42,7 +42,7 @@ export default (ctx, inject) => {
nuxt.legacyApp = this
})
- setNuxtInstance(nuxt)
+ setNuxtAppInstance(nuxt)
inject('_nuxtApp', nuxt)
}
diff --git a/packages/bridge/src/runtime/app.ts b/packages/bridge/src/runtime/app.ts
index 9d183f3be7..a2875984be 100644
--- a/packages/bridge/src/runtime/app.ts
+++ b/packages/bridge/src/runtime/app.ts
@@ -33,26 +33,26 @@ export interface Context {
$_nuxtApp: NuxtAppCompat
}
-let currentNuxtInstance: NuxtAppCompat | null
+let currentNuxtAppInstance: NuxtAppCompat | null
-export const setNuxtInstance = (nuxt: NuxtAppCompat | null) => {
- currentNuxtInstance = nuxt
+export const setNuxtAppInstance = (nuxt: NuxtAppCompat | null) => {
+ currentNuxtAppInstance = nuxt
}
export const defineNuxtPlugin = plugin => (ctx: Context) => {
- setNuxtInstance(ctx.$_nuxtApp)
+ setNuxtAppInstance(ctx.$_nuxtApp)
plugin(ctx.$_nuxtApp)
- setNuxtInstance(null)
+ setNuxtAppInstance(null)
}
export const useNuxtApp = () => {
const vm = getCurrentInstance()
if (!vm) {
- if (!currentNuxtInstance) {
- throw new Error('nuxt instance unavailable')
+ if (!currentNuxtAppInstance) {
+ throw new Error('nuxt app instance unavailable')
}
- return currentNuxtInstance
+ return currentNuxtAppInstance
}
return vm?.proxy.$_nuxtApp
diff --git a/packages/bridge/src/runtime/composables.ts b/packages/bridge/src/runtime/composables.ts
index 577f3a61ad..512f46a089 100644
--- a/packages/bridge/src/runtime/composables.ts
+++ b/packages/bridge/src/runtime/composables.ts
@@ -1,4 +1,4 @@
-import { reactive } from '@vue/composition-api'
+import { reactive, toRef, isReactive } from '@vue/composition-api'
import type VueRouter from 'vue-router'
import type { Route } from 'vue-router'
import { useNuxtApp } from './app'
@@ -39,3 +39,15 @@ export const useRoute = () => {
return nuxt._route as Route
}
+
+// payload.state is used for vuex by nuxt 2
+export const useState = (key: string) => {
+ const nuxtApp = useNuxtApp()
+ if (!nuxtApp.payload.useState) {
+ nuxtApp.payload.useState = {}
+ }
+ if (!isReactive(nuxtApp.payload.useState)) {
+ nuxtApp.payload.useState = reactive(nuxtApp.payload.useState)
+ }
+ return toRef(nuxtApp.payload.useState, key)
+}
diff --git a/packages/nuxt3/src/app/composables/index.ts b/packages/nuxt3/src/app/composables/index.ts
index 2ba366ad15..5f499638fd 100644
--- a/packages/nuxt3/src/app/composables/index.ts
+++ b/packages/nuxt3/src/app/composables/index.ts
@@ -1,3 +1,4 @@
export { defineNuxtComponent } from './component'
export { useAsyncData } from './asyncData'
export { useHydration } from './hydrate'
+export { useState } from './state'
diff --git a/packages/nuxt3/src/app/composables/state.ts b/packages/nuxt3/src/app/composables/state.ts
new file mode 100644
index 0000000000..9ed3d05836
--- /dev/null
+++ b/packages/nuxt3/src/app/composables/state.ts
@@ -0,0 +1,12 @@
+import type { Ref } from 'vue'
+import { useNuxtApp } from '#app'
+
+/**
+ * Create a global reactive ref that will be hydrated but not shared across ssr requests
+ *
+ * @param key a unique key to identify the data in the Nuxt payload
+ */
+export const useState = (key: string): Ref => {
+ const nuxt = useNuxtApp()
+ return toRef(nuxt.payload.state, key)
+}
diff --git a/packages/nuxt3/src/app/nuxt.ts b/packages/nuxt3/src/app/nuxt.ts
index 319bfc7d3c..a4f3eeb251 100644
--- a/packages/nuxt3/src/app/nuxt.ts
+++ b/packages/nuxt3/src/app/nuxt.ts
@@ -44,6 +44,7 @@ export interface NuxtApp {
payload: {
serverRendered?: true
data?: Record
+ state?: Record
rendered?: Function
[key: string]: any
}
@@ -70,7 +71,11 @@ export function createNuxtApp (options: CreateOptions) {
const nuxt: NuxtApp = {
provide: undefined,
globalName: 'nuxt',
- payload: reactive(process.server ? { serverRendered: true, data: {} } : (window.__NUXT__ || { data: {} })),
+ payload: reactive({
+ data: {},
+ state: {},
+ ...(process.client ? window.__NUXT__ : { serverRendered: true })
+ }),
isHydrating: process.client,
_asyncDataPromises: {},
...options
@@ -150,10 +155,10 @@ export function isLegacyPlugin (plugin: unknown): plugin is LegacyPlugin {
return !plugin[NuxtPluginIndicator]
}
-let currentNuxtInstance: NuxtApp | null
+let currentNuxtAppInstance: NuxtApp | null
-export const setNuxtInstance = (nuxt: NuxtApp | null) => {
- currentNuxtInstance = nuxt
+export const setNuxtAppInstance = (nuxt: NuxtApp | null) => {
+ currentNuxtAppInstance = nuxt
}
/**
@@ -163,9 +168,9 @@ export const setNuxtInstance = (nuxt: NuxtApp | null) => {
* @param setup The function to call
*/
export async function callWithNuxt (nuxt: NuxtApp, setup: () => any) {
- setNuxtInstance(nuxt)
+ setNuxtAppInstance(nuxt)
const p = setup()
- setNuxtInstance(null)
+ setNuxtAppInstance(null)
await p
}
@@ -176,10 +181,10 @@ export function useNuxtApp (): NuxtApp {
const vm = getCurrentInstance()
if (!vm) {
- if (!currentNuxtInstance) {
+ if (!currentNuxtAppInstance) {
throw new Error('nuxt instance unavailable')
}
- return currentNuxtInstance
+ return currentNuxtAppInstance
}
return vm.appContext.app.$nuxt
diff --git a/packages/nuxt3/src/auto-imports/identifiers.ts b/packages/nuxt3/src/auto-imports/identifiers.ts
index 663d03903b..099b3f3199 100644
--- a/packages/nuxt3/src/auto-imports/identifiers.ts
+++ b/packages/nuxt3/src/auto-imports/identifiers.ts
@@ -4,7 +4,8 @@ const identifiers = {
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
- 'useRuntimeConfig'
+ 'useRuntimeConfig',
+ 'useState'
],
'#meta': [
'useMeta'
diff --git a/test/fixtures/bridge/layouts/default.vue b/test/fixtures/bridge/layouts/default.vue
index 408c4943b3..7e12ec0014 100644
--- a/test/fixtures/bridge/layouts/default.vue
+++ b/test/fixtures/bridge/layouts/default.vue
@@ -1,7 +1,8 @@