feat: useState composable (#719)

This commit is contained in:
pooya parsa 2021-10-11 19:48:03 +02:00 committed by GitHub
parent d8f40155c1
commit 666b7f1ba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 150 additions and 23 deletions

View File

@ -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
<script setup>
const locale = useState('locale')
</script>
<template>
Current locale: {{ locale }}
</template>
```

View File

@ -0,0 +1,4 @@
import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({
})

View File

@ -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"
}
}

View File

@ -0,0 +1,13 @@
<template>
<div>
Locale: {{ locale }}
<button @click="updateLocale">
Set to Klington
</button>
</div>
</template>
<script setup>
const locale = useState('locale')
const updateLocale = () => { locale.value = 'tlh-klingon' }
</script>

View File

@ -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]
})

View File

@ -1,6 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import { createHooks } from 'hookable/dist/index.mjs' import { createHooks } from 'hookable/dist/index.mjs'
import { setNuxtInstance } from '#app' import { setNuxtAppInstance } from '#app'
export default (ctx, inject) => { export default (ctx, inject) => {
const nuxt = { const nuxt = {
@ -42,7 +42,7 @@ export default (ctx, inject) => {
nuxt.legacyApp = this nuxt.legacyApp = this
}) })
setNuxtInstance(nuxt) setNuxtAppInstance(nuxt)
inject('_nuxtApp', nuxt) inject('_nuxtApp', nuxt)
} }

View File

@ -33,26 +33,26 @@ export interface Context {
$_nuxtApp: NuxtAppCompat $_nuxtApp: NuxtAppCompat
} }
let currentNuxtInstance: NuxtAppCompat | null let currentNuxtAppInstance: NuxtAppCompat | null
export const setNuxtInstance = (nuxt: NuxtAppCompat | null) => { export const setNuxtAppInstance = (nuxt: NuxtAppCompat | null) => {
currentNuxtInstance = nuxt currentNuxtAppInstance = nuxt
} }
export const defineNuxtPlugin = plugin => (ctx: Context) => { export const defineNuxtPlugin = plugin => (ctx: Context) => {
setNuxtInstance(ctx.$_nuxtApp) setNuxtAppInstance(ctx.$_nuxtApp)
plugin(ctx.$_nuxtApp) plugin(ctx.$_nuxtApp)
setNuxtInstance(null) setNuxtAppInstance(null)
} }
export const useNuxtApp = () => { export const useNuxtApp = () => {
const vm = getCurrentInstance() const vm = getCurrentInstance()
if (!vm) { if (!vm) {
if (!currentNuxtInstance) { if (!currentNuxtAppInstance) {
throw new Error('nuxt instance unavailable') throw new Error('nuxt app instance unavailable')
} }
return currentNuxtInstance return currentNuxtAppInstance
} }
return vm?.proxy.$_nuxtApp return vm?.proxy.$_nuxtApp

View File

@ -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 VueRouter from 'vue-router'
import type { Route } from 'vue-router' import type { Route } from 'vue-router'
import { useNuxtApp } from './app' import { useNuxtApp } from './app'
@ -39,3 +39,15 @@ export const useRoute = () => {
return nuxt._route as Route 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)
}

View File

@ -1,3 +1,4 @@
export { defineNuxtComponent } from './component' export { defineNuxtComponent } from './component'
export { useAsyncData } from './asyncData' export { useAsyncData } from './asyncData'
export { useHydration } from './hydrate' export { useHydration } from './hydrate'
export { useState } from './state'

View File

@ -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 = <T> (key: string): Ref<T> => {
const nuxt = useNuxtApp()
return toRef(nuxt.payload.state, key)
}

View File

@ -44,6 +44,7 @@ export interface NuxtApp {
payload: { payload: {
serverRendered?: true serverRendered?: true
data?: Record<string, any> data?: Record<string, any>
state?: Record<string, any>
rendered?: Function rendered?: Function
[key: string]: any [key: string]: any
} }
@ -70,7 +71,11 @@ export function createNuxtApp (options: CreateOptions) {
const nuxt: NuxtApp = { const nuxt: NuxtApp = {
provide: undefined, provide: undefined,
globalName: 'nuxt', 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, isHydrating: process.client,
_asyncDataPromises: {}, _asyncDataPromises: {},
...options ...options
@ -150,10 +155,10 @@ export function isLegacyPlugin (plugin: unknown): plugin is LegacyPlugin {
return !plugin[NuxtPluginIndicator] return !plugin[NuxtPluginIndicator]
} }
let currentNuxtInstance: NuxtApp | null let currentNuxtAppInstance: NuxtApp | null
export const setNuxtInstance = (nuxt: NuxtApp | null) => { export const setNuxtAppInstance = (nuxt: NuxtApp | null) => {
currentNuxtInstance = nuxt currentNuxtAppInstance = nuxt
} }
/** /**
@ -163,9 +168,9 @@ export const setNuxtInstance = (nuxt: NuxtApp | null) => {
* @param setup The function to call * @param setup The function to call
*/ */
export async function callWithNuxt (nuxt: NuxtApp, setup: () => any) { export async function callWithNuxt (nuxt: NuxtApp, setup: () => any) {
setNuxtInstance(nuxt) setNuxtAppInstance(nuxt)
const p = setup() const p = setup()
setNuxtInstance(null) setNuxtAppInstance(null)
await p await p
} }
@ -176,10 +181,10 @@ export function useNuxtApp (): NuxtApp {
const vm = getCurrentInstance() const vm = getCurrentInstance()
if (!vm) { if (!vm) {
if (!currentNuxtInstance) { if (!currentNuxtAppInstance) {
throw new Error('nuxt instance unavailable') throw new Error('nuxt instance unavailable')
} }
return currentNuxtInstance return currentNuxtAppInstance
} }
return vm.appContext.app.$nuxt return vm.appContext.app.$nuxt

View File

@ -4,7 +4,8 @@ const identifiers = {
'defineNuxtComponent', 'defineNuxtComponent',
'useNuxtApp', 'useNuxtApp',
'defineNuxtPlugin', 'defineNuxtPlugin',
'useRuntimeConfig' 'useRuntimeConfig',
'useState'
], ],
'#meta': [ '#meta': [
'useMeta' 'useMeta'

View File

@ -1,7 +1,8 @@
<template> <template>
<div> <div>
<Nuxt /> <Nuxt />
{{ route.path }} <hr>
Route: {{ route.path }}
</div> </div>
</template> </template>
@ -9,7 +10,6 @@
export default { export default {
setup () { setup () {
const route = useRoute() const route = useRoute()
console.log(route.path)
return { route } return { route }
} }
} }

View File

@ -17,5 +17,8 @@ export default defineNuxtConfig({
plugins: ['~/plugins/setup.js'], plugins: ['~/plugins/setup.js'],
nitro: { nitro: {
output: { dir: process.env.NITRO_OUTPUT_DIR } output: { dir: process.env.NITRO_OUTPUT_DIR }
},
bridge: {
meta: true
} }
}) })

View File

@ -1,8 +1,18 @@
<template> <template>
<div>
<div>Hello Vue {{ version }}!</div> <div>Hello Vue {{ version }}!</div>
<div>
State: {{ state }} <button @click="updateState">
Update
</button>
</div>
</div>
</template> </template>
<script setup> <script setup>
useMeta({ meta: [{ name: 'description', content: 'This is a page to demo Nuxt Bridge.' }] }) useMeta({ meta: [{ name: 'description', content: 'This is a page to demo Nuxt Bridge.' }] })
const version = ref('2') const version = ref('2')
const state = useState('test-state')
state.value = '123'
const updateState = () => { state.value = '456' }
</script> </script>

View File

@ -9230,6 +9230,14 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "example-wasm@workspace:examples/wasm":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "example-wasm@workspace:examples/wasm" resolution: "example-wasm@workspace:examples/wasm"