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 { 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)
}

View File

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

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 { 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)
}

View File

@ -1,3 +1,4 @@
export { defineNuxtComponent } from './component'
export { useAsyncData } from './asyncData'
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: {
serverRendered?: true
data?: Record<string, any>
state?: Record<string, any>
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

View File

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

View File

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

View File

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

View File

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

View File

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