Skip to content

Commit

Permalink
feat(app): app.runWithContext()
Browse files Browse the repository at this point in the history
Allows accessing globals provided at the app level with `app.provide()`
via a regular `inject()` call as long as you have access to the
application. Useful for Pinia, vue-router, Nuxt, Quasar, and other
advanced use cases.

- vuejs/pinia#1784
  • Loading branch information
posva committed Jan 2, 2023
1 parent 1fa3d95 commit b137661
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 4 deletions.
16 changes: 16 additions & 0 deletions packages/runtime-core/__tests__/apiCreateApp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ describe('api: createApp', () => {
expect(`App already provides property with key "bar".`).toHaveBeenWarned()
})

test('runWithContext', () => {
const app = createApp({
setup() {
provide('foo', 'should not be seen')
return () => h('div')
}
})
app.provide('foo', 1)

expect(app.runWithContext(() => inject('foo'))).toBe(1)

// ensure the context is restored
inject('foo')
expect('inject() can only be used inside setup').toHaveBeenWarned()
})

test('component', () => {
const Root = {
// local override
Expand Down
22 changes: 22 additions & 0 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export interface App<HostElement = any> {
unmount(): void
provide<T>(key: InjectionKey<T> | string, value: T): this

/**
* Runs a function with the app as active instance. This allows using of `inject()` within the function to get access
* to variables provided via `app.provide()`.
*
* @param fn - function to run with the app as active instance
*/
runWithContext<T>(fn: () => T): T

// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
Expand Down Expand Up @@ -370,6 +378,15 @@ export function createAppAPI<HostElement>(
context.provides[key as string | symbol] = value

return app
},

runWithContext(fn) {
currentApp = this
try {
return fn()
} finally {
currentApp = null
}
}
})

Expand All @@ -380,3 +397,8 @@ export function createAppAPI<HostElement>(
return app
}
}

/**
* @internal Used to identify the current app when using `inject()` within `app.runWithContext()`.
*/
export let currentApp: App<unknown> | null = null
12 changes: 8 additions & 4 deletions packages/runtime-core/src/apiInject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isFunction } from '@vue/shared'
import { currentInstance } from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { App, currentApp } from './apiCreateApp'
import { warn } from './warning'

export interface InjectionKey<T> extends Symbol {}
Expand Down Expand Up @@ -44,14 +45,17 @@ export function inject(
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
// a functional component and to currentApp so it can be called within `app.runWithContext()`
const instance = currentInstance || currentRenderingInstance || currentApp

if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides =
instance.parent == null
'mount' in instance // checks if instance is an App
? instance._context.provides
: instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides

Expand All @@ -60,7 +64,7 @@ export function inject(
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
? defaultValue.call((instance as App & { proxy: undefined }).proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
Expand Down

0 comments on commit b137661

Please sign in to comment.