Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

Fix: store context in unctx and allow to not lose context on async calls with callWithContext #742

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
51 changes: 51 additions & 0 deletions docs/pages/en/6.API/3.callWithContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: callWithContext
description: 'Ensures Context will always be available inside called Composable'
---

This function equals one of Nuxt Bridge/Nuxt 3 internals: `callWithNuxt`.
It accepts `useContext()` response as first argument, function-to-call as second and function's arguments as third.
When you call this function, you can always be sure that function-to-call will have access to `useContext`, `useRoute`, e.t.c. methods.

Example of usage with useAsync:

```ts
import {
defineComponent,
useContext,
callWithContext,
} from '@nuxtjs/composition-api'
import { useMyComposable, useSecondComposable } from '../composables'

export default defineComponent({
setup() {
const context = useContext()

useAsync(async () => {
try {
//Context is lost after you call first await on SSR
const firstAwait = await useMyComposable({ option: true })
//This one depends on firstAwait and calls useContext inside of it
const secondAwait = await callWithContext(
context,
useSecondComposable,
[{ option: true }]
)

return {
firstAwait,
secondAwait,
}
} catch (e) {
//Wait for logging system response etc
await callWithContext(context, useProcessError, [{ error: e }])
throw e
}
})
},
})
```

:::alert{type="info"}
Note: after first call of useContext on Client Side context will be stored as global. You will not have to call function to access returned in setup composables on client side.
:::
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"fs-extra": "^9.1.0",
"magic-string": "^0.27.0",
"pathe": "^1.0.0",
"ufo": "^1.0.1"
"ufo": "^1.0.1",
"unctx": "^2.1.2"
},
"devDependencies": {
"@babel/traverse": "^7.20.5",
Expand Down Expand Up @@ -102,12 +103,14 @@
"tsd": "^0.25.0",
"typescript": "4.9.4",
"vitest": "^0.25.7",
"vue-router": "^3.6.5",
"yorkie": "^2.0.0"
},
"peerDependencies": {
"@nuxt/vue-app": "^2.15",
"nuxt": "^2.15",
"vue": "^2.7.14"
"vue": "^2.7.14",
"vue-router": "^3.6"
},
"engines": {
"node": ">=v14.13.0"
Expand Down
78 changes: 58 additions & 20 deletions src/runtime/composables/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { computed } from 'vue'
import type { Ref } from 'vue'
import type { Context } from '@nuxt/types'
import type { Route } from 'vue-router'
import { useRoute } from 'vue-router/composables'

import { getContext } from 'unctx'
import { globalNuxt } from '@nuxtjs/composition-api/dist/runtime/globals'
import { getCurrentInstance } from './utils'
import { Vue } from 'vue/types/vue'

interface ContextCallback {
(context: Context): void
Expand All @@ -30,6 +33,25 @@ interface UseContextReturn
params: Ref<Route['params']>
}

const nuxtCtx = getContext<UseContextReturn>('nuxt-context')

/**
* Ensures that the setup function passed in has access to the Nuxt instance via `useContext`.
*
* @param context useContext response
* @param setup The function to call
* @param args Function's arguments
*/
export function callWithContext<T extends (...args: any[]) => any>(
context: UseContextReturn,
setup: T,
args?: Parameters<T>
) {
const fn: () => ReturnType<T> = () =>
args ? setup(...(args as Parameters<T>)) : setup()
return nuxtCtx.callAsync(context, fn)
}

/**
* `useContext` will return the Nuxt context.
* @example
Expand All @@ -45,26 +67,42 @@ interface UseContextReturn
```
*/
export const useContext = (): UseContextReturn => {
const vm = getCurrentInstance()
if (!vm) throw new Error('This must be called within a setup function.')
const nuxtContext = nuxtCtx.tryUse()

if (!nuxtContext) {
const vm = getCurrentInstance()
if (!vm) {
throw new Error('This must be called within a setup function.')
}

const root = vm.$root as unknown as { _$route: typeof vm.$root['$route'] }

return {
...(vm[globalNuxt] || vm.$options).context,
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `route` from `useContext` but rather to use the `useRoute` helper function.
*/
route: computed(() => vm.$route),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `query` from `useContext` but rather to use the `useRoute` helper function.
*/
query: computed(() => vm.$route.query),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `from` from `useContext` but rather to use the `useRoute` helper function.
*/
from: computed(() => (vm[globalNuxt] || vm.$options).context.from),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `params` from `useContext` but rather to use the `useRoute` helper function.
*/
params: computed(() => vm.$route.params),
// Call of vue-router initialization of _$route
if (!root._$route) useRoute()

const context = {
...(vm[globalNuxt] || vm.$options).context,
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `route` from `useContext` but rather to use the `useRoute` helper function.
*/
route: computed(() => root._$route),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `query` from `useContext` but rather to use the `useRoute` helper function.
*/
query: computed(() => root._$route.query),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `from` from `useContext` but rather to use the `useRoute` helper function.
*/
from: computed(() => (vm[globalNuxt] || vm.$options).context.from),
/**
* @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `params` from `useContext` but rather to use the `useRoute` helper function.
*/
params: computed(() => root._$route.params),
}

if (process.client) nuxtCtx.set(context)
return context
}

return nuxtContext
}
11 changes: 9 additions & 2 deletions src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
export { useAsync } from './async'
export { defineComponent } from './component'
export { useContext, withContext } from './context'
export { callWithContext, useContext, withContext } from './context'
export * from './defineHelpers'
export { useFetch } from './fetch'
export { globalPlugin, onGlobalSetup, setMetaPlugin } from './hooks'
export { useMeta } from './meta'
export { reqRef, reqSsrRef } from './req-ref'
export { ssrRef, shallowSsrRef, setSSRContext, ssrPromise } from './ssr-ref'
export { useStatic } from './static'
export { useRoute, useRouter, useStore, wrapProperty } from './wrappers'
export {
useRoute,
useRouter,
useStore,
wrapProperty,
wrapContextProperty,
useRedirect,
} from './wrappers'

export * from './vue'
58 changes: 51 additions & 7 deletions src/runtime/composables/wrappers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { computed, ComputedRef, InjectionKey } from 'vue'
import { computed, ComputedRef, InjectionKey, isRef } from 'vue'
import type { Store } from 'vuex'
import { useContext } from './context'

import { getCurrentInstance } from './utils'
import { Context } from '@nuxt/types'
import { useRouter as useVueRouter } from 'vue-router/composables'

/**
* You might want to create a custom helper to 'convert' a non-Composition API property to a Composition-ready one. `wrapProperty` enables you to do that easily, returning either a computed or a bare property as required.
Expand All @@ -27,6 +30,27 @@ export const wrapProperty = <
}
}

/**
* You might want to create a custom helper to 'convert' a non-Composition Context property to a Composition-ready one. `wrapProperty` enables you to do that easily, returning either a computed or a bare property as required.
* @param property the name of the property you would like to access. For example, `store` to access `context.store`.
* @param makeComputed a boolean indicating whether the helper function should return a computed property or not. Defaults to `true`.
*/
export const wrapContextProperty = <
K extends keyof Context,
T extends boolean = true
>(
property: K,
makeComputed?: T
) => {
return (): T extends true ? ComputedRef<Context[K]> : Context[K] => {
const context = useContext()

return makeComputed !== false && !isRef(context[property])
? (computed(() => context[property]) as any)
: context[property]
}
}

/**
* Gain access to the router just like using this.$router in a non-Composition API manner.
* @example
Expand All @@ -41,7 +65,30 @@ export const wrapProperty = <
})
```
*/
export const useRouter = wrapProperty('$router', false)
export const useRouter = (): VueRouter => {
const vm = getCurrentInstance()
if (vm) return useVueRouter()

const contextRouter = useContext().app.router
if (contextRouter) return contextRouter

throw new Error('This must be called within a setup function.')
}

/**
* Gain safe access to the redirect method from Context
* @example
```ts
import { defineComponent, useRedirect } from '@nuxtjs/composition-api'

export default defineComponent({
setup() {
useRedirect('/')
}
})
```
*/
export const useRedirect = wrapContextProperty('redirect')

/**
* Returns `this.$route`, wrapped in a computed - so accessible from `.value`.
Expand All @@ -57,7 +104,7 @@ export const useRouter = wrapProperty('$router', false)
})
```
*/
export const useRoute = wrapProperty('$route')
export const useRoute = wrapContextProperty('route')

/**
* Gain access to the store just like using this.$store in a non-Composition API manner. You can also provide an injection key or custom type to get back a semi-typed store:
Expand All @@ -82,8 +129,5 @@ export const useRoute = wrapProperty('$route')
```
*/
export const useStore = <S>(key?: InjectionKey<S>): Store<S> => {
const vm = getCurrentInstance()
if (!vm) throw new Error('This must be called within a setup function.')

return vm.$store
return useContext().store
}
Loading