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 + + + +``` 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 @@ + + + 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 @@ @@ -9,7 +10,6 @@ export default { setup () { const route = useRoute() - console.log(route.path) return { route } } } diff --git a/test/fixtures/bridge/nuxt.config.ts b/test/fixtures/bridge/nuxt.config.ts index 2edb0a94af..422306af5e 100644 --- a/test/fixtures/bridge/nuxt.config.ts +++ b/test/fixtures/bridge/nuxt.config.ts @@ -17,5 +17,8 @@ export default defineNuxtConfig({ plugins: ['~/plugins/setup.js'], nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } + }, + bridge: { + meta: true } }) diff --git a/test/fixtures/bridge/pages/index.vue b/test/fixtures/bridge/pages/index.vue index d8afccc492..1dd1fcbdaf 100644 --- a/test/fixtures/bridge/pages/index.vue +++ b/test/fixtures/bridge/pages/index.vue @@ -1,8 +1,18 @@ diff --git a/yarn.lock b/yarn.lock index b5e5f7a58a..f5affa7a25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9230,6 +9230,14 @@ __metadata: languageName: unknown linkType: soft +"example-use-state@workspace:examples/use-state": + version: 0.0.0-use.local + resolution: "example-use-state@workspace:examples/use-state" + dependencies: + nuxt3: latest + languageName: unknown + linkType: soft + "example-wasm@workspace:examples/wasm": version: 0.0.0-use.local resolution: "example-wasm@workspace:examples/wasm"