@blueprint-ts/core 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v2.0.0 - 2026-02-18
2
+
3
+ # [2.0.0](/compare/v1.2.0...v2.0.0) (2026-02-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * Cache resolved route resources for child routes db70b77
1
9
  ## v1.2.0 - 2026-01-25
2
10
 
3
11
  # [1.2.0](/compare/v1.1.2...v1.2.0) (2026-01-25)
@@ -1,4 +1,4 @@
1
- # Route Model Binding
1
+ # Route Resource Binding
2
2
 
3
3
  When using `vue-router`, you can automatically bind route parameters to resources, similar to how Laravel's route model binding works.
4
4
 
@@ -7,60 +7,228 @@ When using `vue-router`, you can automatically bind route parameters to resource
7
7
  To enable the router to load resources automatically, install the route injection plugin when initializing your router:
8
8
 
9
9
  ```ts
10
+ import { installRouteInjection } from '@blueprint-ts/core'
11
+
10
12
  installRouteInjection(router)
11
13
  ```
12
14
 
13
- ### Defining Routes
15
+ ## Defining Routes
14
16
 
15
17
  Use the `defineRoute` helper to define your routes and specify which parameters should be resolved into resources:
16
18
 
17
19
  ```ts
18
- defineRoute<{
20
+ import { defineRoute, RouteResourceRequestResolver } from '@blueprint-ts/core'
21
+ import ProductDetailPage from '@/pages/ProductDetailPage.vue'
22
+
23
+ export default defineRoute<{
19
24
  product: ProductResource
20
25
  }>()({
21
26
  path: ':productId',
22
27
  name: 'products.show',
23
28
  component: ProductDetailPage,
24
- meta: {
25
- inject: {
26
- product: {
27
- from: 'productId',
28
- resolve: (productId: string) => {
29
- return new RouteModelRequestResolver(
30
- new ProductShowRequest(productId)
31
- )
32
- }
29
+ inject: {
30
+ product: {
31
+ from: 'productId',
32
+ resolve: (productId: string) => {
33
+ return new RouteResourceRequestResolver(
34
+ new ProductShowRequest(productId)
35
+ )
33
36
  }
34
37
  }
35
38
  }
36
39
  })
37
40
  ```
38
41
 
39
- The `beforeResolve` navigation guard will automatically fetch the `ProductResource` using the `ProductShowRequest` and the `productId` from the route.
42
+ Navigation is **non-blocking** the route navigates immediately while resources resolve in the background. Cached values are reused when navigating between child routes with unchanged parameters.
40
43
 
41
44
  ## Usage in Components
42
45
 
43
- Your component can then directly access the resolved resource via props:
46
+ Your component can directly access the resolved resource via props:
47
+
48
+ ```vue
49
+ <script setup lang="ts">
50
+ const props = defineProps<{
51
+ product: ProductResource
52
+ }>()
53
+ </script>
54
+ ```
55
+
56
+ ## Handling Loading & Error States
57
+
58
+ ### Using `RouteResourceBoundView`
59
+
60
+ `RouteResourceBoundView` is a drop-in replacement for `<RouterView>` that automatically handles loading and error states. Define error and loading components directly in the route:
61
+
62
+ ```ts
63
+ import { defineRoute, RouteResourceRequestResolver } from '@blueprint-ts/core'
64
+ import ProductDetailPage from '@/pages/ProductDetailPage.vue'
65
+ import GenericErrorPage from '@/pages/GenericErrorPage.vue'
66
+ import LoadingSpinner from '@/components/LoadingSpinner.vue'
67
+
68
+ export default defineRoute<{
69
+ product: ProductResource
70
+ }>()({
71
+ path: ':productId',
72
+ name: 'products.show',
73
+ component: ProductDetailPage,
74
+ errorComponent: GenericErrorPage,
75
+ loadingComponent: LoadingSpinner,
76
+ inject: {
77
+ product: {
78
+ from: 'productId',
79
+ resolve: (productId: string) => {
80
+ return new RouteResourceRequestResolver(
81
+ new ProductShowRequest(productId)
82
+ )
83
+ }
84
+ }
85
+ }
86
+ })
87
+ ```
88
+
89
+ Then replace `<RouterView>` with `<RouteResourceBoundView>` in your layout:
90
+
91
+ ```vue
92
+ <template>
93
+ <RouteResourceBoundView />
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import { RouteResourceBoundView } from '@blueprint-ts/core'
98
+ </script>
99
+ ```
100
+
101
+ The `errorComponent` receives `error` and `refresh` as props. The `refresh` function retries all failed resources:
102
+
103
+ ```vue
104
+ <!-- GenericErrorPage.vue -->
105
+ <template>
106
+ <div>
107
+ <p>{{ error.message }}</p>
108
+ <button @click="refresh">Try Again</button>
109
+ </div>
110
+ </template>
111
+
112
+ <script setup lang="ts">
113
+ defineProps<{
114
+ error: Error
115
+ refresh: () => Promise<void>
116
+ }>()
117
+ </script>
118
+ ```
119
+
120
+ #### Using Scoped Slots
121
+
122
+ `RouteResourceBoundView` supports the same `v-slot` pattern as `<RouterView>`:
123
+
124
+ ```vue
125
+ <RouteResourceBoundView v-slot="{ Component, route }">
126
+ <component
127
+ :is="Component"
128
+ v-if="Component"
129
+ />
130
+ <EmptyState v-else />
131
+ </RouteResourceBoundView>
132
+ ```
133
+
134
+ When no `errorComponent` or `loadingComponent` is defined on the route, you can use named slots as fallbacks:
44
135
 
45
136
  ```vue
137
+ <RouteResourceBoundView>
138
+ <template #default="{ Component, route }">
139
+ <component :is="Component" v-if="Component" />
140
+ </template>
141
+
142
+ <template #loading>
143
+ <LoadingSpinner />
144
+ </template>
145
+
146
+ <template #error="{ error, refresh }">
147
+ <div>
148
+ <p>{{ error.message }}</p>
149
+ <button @click="refresh">Retry</button>
150
+ </div>
151
+ </template>
152
+ </RouteResourceBoundView>
153
+ ```
154
+
155
+ ### Using `useRouteResource` (Manual Handling)
156
+
157
+ If you prefer to handle loading and error states inside the component itself, set `lazy: false` on the route. This renders the component immediately while resources resolve in the background:
158
+
159
+ ```ts
160
+ export default defineRoute<{
161
+ product: ProductResource
162
+ }>()({
163
+ path: ':productId',
164
+ name: 'products.show',
165
+ component: ProductDetailPage,
166
+ lazy: false,
167
+ inject: {
168
+ product: {
169
+ from: 'productId',
170
+ resolve: (productId: string) => {
171
+ return new RouteResourceRequestResolver(
172
+ new ProductShowRequest(productId)
173
+ )
174
+ }
175
+ }
176
+ }
177
+ })
178
+ ```
179
+
180
+ Then use the `useRouteResource` composable inside your component:
181
+
182
+ ```vue
183
+ <template>
184
+ <div v-if="isLoading">Loading...</div>
185
+ <div v-else-if="error">
186
+ <p>{{ error.message }}</p>
187
+ <button @click="refresh">Retry</button>
188
+ </div>
189
+ <div v-else>
190
+ <h1>{{ product.name }}</h1>
191
+ </div>
192
+ </template>
193
+
46
194
  <script setup lang="ts">
195
+ import { useRouteResource } from '@blueprint-ts/core'
196
+
47
197
  const props = defineProps<{
48
198
  product: ProductResource
49
199
  }>()
200
+
201
+ const { refresh, isLoading, error } = useRouteResource('product')
50
202
  </script>
51
203
  ```
52
204
 
53
- ## Handling Loading States
205
+ `useRouteResource` returns:
206
+
207
+ | Property | Type | Description |
208
+ |-------------|---------------------------|------------------------------------------|
209
+ | `isLoading` | `ComputedRef<boolean>` | `true` while the resource is resolving |
210
+ | `error` | `ComputedRef<Error\|null>` | The error if resolution failed, else `null` |
211
+ | `refresh` | `(options?: { silent?: boolean }) => Promise<void>` | Re-fetches the resource. Pass `{ silent: true }` to suppress the loading state. |
212
+
213
+ #### Silent Refresh
54
214
 
55
- You can handle the loading state of the request by using the event system of the request class:
215
+ By default, calling `refresh()` sets `isLoading` to `true` while the resource is being re-fetched, which causes `RouteResourceBoundView` to show the loading component. If you want to refresh the resource in the background without triggering the loading state (e.g. polling or optimistic updates), pass `{ silent: true }`:
56
216
 
57
217
  ```ts
58
- resolve: (productId: string) => {
59
- return new RouteModelRequestResolver(
60
- new ProductShowRequest(productId).on<boolean>(RequestEvents.LOADING, (loading: boolean) => {
61
- const loadingStore = useLoadingStore()
62
- loadingStore.setLoading(loading)
63
- })
64
- )
65
- }
66
- ```
218
+ const { refresh } = useRouteResource('product')
219
+
220
+ // Normal refresh triggers loading state
221
+ await refresh()
222
+
223
+ // Silent refresh — does not trigger loading state
224
+ await refresh({ silent: true })
225
+ ```
226
+
227
+ A silent refresh still updates the `error` state if the request fails.
228
+
229
+ ### Lazy vs Non-Lazy
230
+
231
+ | Option | Behavior |
232
+ |-----------------|-----------------------------------------------------------------------------------------------|
233
+ | `lazy: true` (default) | `RouteResourceBoundView` intercepts loading/error states and shows the appropriate component |
234
+ | `lazy: false` | The target component renders immediately; use `useRouteResource()` for manual state handling |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprint-ts/core",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,145 @@
1
+ import { computed, defineComponent, h, type Component as VueComponent, type VNode } from 'vue'
2
+ import { RouterView, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
3
+
4
+ type InjectionState = Record<string, { loading: boolean; error: Error | null }>
5
+
6
+ /**
7
+ * Drop-in replacement for `<router-view>` that handles route resource
8
+ * injection loading and error states.
9
+ *
10
+ * When any injected resource is loading → renders the route's `loadingComponent`.
11
+ * When any injected resource has an error → renders the route's `errorComponent`
12
+ * (receives `error` and `refresh` as props).
13
+ * Otherwise → passes `{ Component, route }` to the default scoped slot,
14
+ * just like `<RouterView>`.
15
+ *
16
+ * Error and loading components are defined in the route via `defineRoute`:
17
+ *
18
+ * ```ts
19
+ * defineRoute<{ credential: CredentialResource }>()({
20
+ * path: '/credentials/:credentialId',
21
+ * component: CredentialShowPage,
22
+ * errorComponent: GenericErrorPage,
23
+ * loadingComponent: LoadingSpinner,
24
+ * inject: {
25
+ * credential: {
26
+ * from: 'credentialId',
27
+ * resolve: (id) => new RouteResourceRequestResolver(new CredentialShowRequest(id)),
28
+ * },
29
+ * },
30
+ * })
31
+ * ```
32
+ *
33
+ * Set `lazy: false` to render the target component immediately (without
34
+ * waiting for resources). The component can then handle loading/error
35
+ * states itself via `useRouteResource()`:
36
+ *
37
+ * ```ts
38
+ * defineRoute<{ credential: CredentialResource }>()({
39
+ * path: '/credentials/:credentialId',
40
+ * component: CredentialShowPage,
41
+ * lazy: false,
42
+ * inject: { ... },
43
+ * })
44
+ * ```
45
+ *
46
+ * Usage in layout (supports the same v-slot pattern as RouterView):
47
+ * ```vue
48
+ * <RouteResourceBoundView v-slot="{ Component, route }">
49
+ * <component :is="Component" v-if="Component" :key="route.meta.usePathKey ? route.path : undefined" />
50
+ * <EmptyState v-else />
51
+ * </RouteResourceBoundView>
52
+ * ```
53
+ *
54
+ * Or without a slot (renders the matched component automatically):
55
+ * ```vue
56
+ * <RouteResourceBoundView />
57
+ * ```
58
+ */
59
+ export const RouteResourceBoundView = defineComponent({
60
+ name: 'RouteResourceBoundView',
61
+
62
+ setup(_, { slots }) {
63
+ const route = useRoute()
64
+
65
+ const injectionState = computed(() => {
66
+ return route.meta._injectionState as InjectionState | undefined
67
+ })
68
+
69
+ const firstError = computed((): Error | null => {
70
+ const state = injectionState.value
71
+ if (!state) return null
72
+
73
+ for (const key of Object.keys(state)) {
74
+ const entry = state[key]
75
+ if (entry?.error) return entry.error
76
+ }
77
+
78
+ return null
79
+ })
80
+
81
+ const anyLoading = computed((): boolean => {
82
+ const state = injectionState.value
83
+ if (!state) return false
84
+
85
+ return Object.keys(state).some((key) => state[key]?.loading)
86
+ })
87
+
88
+ const refresh = async () => {
89
+ const state = injectionState.value
90
+ if (!state) return
91
+
92
+ const erroredProps = Object.keys(state).filter((key) => state[key]?.error)
93
+
94
+ await Promise.all(erroredProps.map((propName) => route.meta.refresh?.(propName)))
95
+ }
96
+
97
+ /**
98
+ * Core render logic that decides which slot/component to show
99
+ * given the resolved Component and route from RouterView.
100
+ */
101
+ function renderContent(Component: VueComponent | undefined, resolvedRoute: RouteLocationNormalizedLoaded): VNode | VNode[] | undefined {
102
+ const isLazy = resolvedRoute.meta._lazy !== false
103
+
104
+ if (isLazy) {
105
+ if (firstError.value) {
106
+ const errorComponent = resolvedRoute.meta._errorComponent
107
+
108
+ if (errorComponent) {
109
+ return h(errorComponent, { error: firstError.value, refresh })
110
+ }
111
+
112
+ return slots['error']?.({ error: firstError.value, refresh })
113
+ }
114
+
115
+ if (anyLoading.value) {
116
+ const loadingComponent = resolvedRoute.meta._loadingComponent
117
+
118
+ if (loadingComponent) {
119
+ return h(loadingComponent)
120
+ }
121
+
122
+ return slots['loading']?.()
123
+ }
124
+ }
125
+
126
+ if (slots['default']) {
127
+ return slots['default']({ Component, route: resolvedRoute })
128
+ }
129
+
130
+ if (Component) {
131
+ return h(Component)
132
+ }
133
+
134
+ return undefined
135
+ }
136
+
137
+ return () => {
138
+ return h(RouterView, null, {
139
+ default: ({ Component, route: resolvedRoute }: { Component: VueComponent | undefined; route: RouteLocationNormalizedLoaded }) => {
140
+ return renderContent(Component, resolvedRoute)
141
+ }
142
+ })
143
+ }
144
+ }
145
+ })
@@ -1,3 +1,4 @@
1
+ import { markRaw, type Component } from 'vue'
1
2
  import { type RouteRecordRaw } from 'vue-router'
2
3
  import { type InjectConfig } from './types'
3
4
 
@@ -7,16 +8,43 @@ import { type InjectConfig } from './types'
7
8
  * - inject config types (must resolve Props[K])
8
9
  *
9
10
  * Usage:
10
- * export default defineRoute<{ product: ProductResource }>()({ ... })
11
+ * export default defineRoute<{ product: ProductResource }>()({
12
+ * path: '/products/:productId',
13
+ * component: ProductShowPage,
14
+ * errorComponent: GenericErrorPage,
15
+ * loadingComponent: LoadingSpinner,
16
+ * inject: { product: { from: 'productId', resolve: ... } },
17
+ * })
11
18
  */
12
19
  export function defineRoute<Props extends Record<string, unknown>>() {
13
20
  return function <
14
21
  T extends RouteRecordRaw & {
15
22
  inject?: InjectConfig<Props>
23
+ errorComponent?: Component
24
+ loadingComponent?: Component
25
+ lazy?: boolean
16
26
  }
17
27
  >(route: T): RouteRecordRaw {
18
28
  const originalProps = route.props
19
29
 
30
+ if (route.errorComponent) {
31
+ route.meta = route.meta ?? {}
32
+ route.meta._errorComponent = markRaw(route.errorComponent)
33
+ delete route.errorComponent
34
+ }
35
+
36
+ if (route.loadingComponent) {
37
+ route.meta = route.meta ?? {}
38
+ route.meta._loadingComponent = markRaw(route.loadingComponent)
39
+ delete route.loadingComponent
40
+ }
41
+
42
+ if (route.lazy !== undefined) {
43
+ route.meta = route.meta ?? {}
44
+ route.meta._lazy = route.lazy
45
+ delete route.lazy
46
+ }
47
+
20
48
  route.props = (to) => {
21
49
  const baseProps = typeof originalProps === 'function' ? originalProps(to) : originalProps === true ? to.params : (originalProps ?? {})
22
50
 
@@ -1,10 +1,11 @@
1
1
  import { defineRoute } from './defineRoute'
2
2
  import { installRouteInjection } from './installRouteInjection'
3
+ import { RouteResourceBoundView } from './RouteResourceBoundView'
3
4
  import { RouteResourceRequestResolver } from './RouteResourceRequestResolver'
4
5
  import { useRouteResource } from './useRouteResource'
5
6
 
6
7
  import type { DataRequest, InjectConfig, Resolver } from './types'
7
8
 
8
- export { defineRoute, installRouteInjection, RouteResourceRequestResolver, useRouteResource }
9
+ export { defineRoute, installRouteInjection, RouteResourceBoundView, RouteResourceRequestResolver, useRouteResource }
9
10
 
10
11
  export type { DataRequest, InjectConfig, Resolver }
@@ -7,17 +7,26 @@ type InjectRuntimeConfig = {
7
7
  getter: (payload: unknown) => unknown
8
8
  }
9
9
 
10
+ type CachedEntry = {
11
+ paramValue: string
12
+ payload: unknown
13
+ }
14
+
10
15
  /**
11
16
  * Installs the runtime part:
12
17
  * - resolves all route.inject entries before navigation completes
13
18
  * - stores results on to.meta._injectedProps
14
19
  * - ensures route props include injected results (so components receive them as props)
20
+ * - caches resolved resources so child routes inherit parent-resolved data
21
+ * without triggering redundant requests (as long as the param value is unchanged)
15
22
  *
16
23
  * Notes:
17
24
  * - This keeps router files clean: only an `inject` block per route.
18
25
  * - This is intentionally runtime-generic; type safety happens at route definition time.
19
26
  */
20
27
  export function installRouteInjection(router: Router) {
28
+ const cache: Record<string, CachedEntry> = {}
29
+
21
30
  router.beforeResolve(async (to) => {
22
31
  console.log('[Route Injection] Resolving route injections...')
23
32
 
@@ -25,7 +34,13 @@ export function installRouteInjection(router: Router) {
25
34
  to.meta._injectedProps = reactive({})
26
35
  }
27
36
 
28
- const resolvers: Record<string, () => Promise<unknown>> = {}
37
+ if (!to.meta._injectionState) {
38
+ to.meta._injectionState = reactive({})
39
+ }
40
+
41
+ const resolvers: Record<string, (options?: { silent?: boolean }) => Promise<unknown>> = {}
42
+ const activePropNames = new Set<string>()
43
+ const pendingResolvers: Array<() => Promise<void>> = []
29
44
 
30
45
  // Iterate through all matched route records (from parent to child)
31
46
  for (const record of to.matched) {
@@ -57,37 +72,93 @@ export function installRouteInjection(router: Router) {
57
72
  continue
58
73
  }
59
74
 
60
- // Define the refresh logic for this specific prop
61
- const resolveProp = async () => {
62
- const resolver = cfg.resolve(paramValue)
63
- let payload = await resolver.resolve()
64
- if (typeof cfg.getter === 'function') {
65
- payload = cfg.getter(payload)
66
- }
75
+ activePropNames.add(propName)
76
+
77
+ // Initialize state for this prop
78
+ const state = to.meta._injectionState as Record<string, { loading: boolean; error: Error | null }>
79
+ if (!state[propName]) {
80
+ state[propName] = reactive({ loading: false, error: null })
81
+ }
67
82
 
68
- // Updating the reactive object triggers the component re-render
69
- ;(to.meta._injectedProps as any)[propName] = payload
70
- return payload
83
+ // Define the refresh logic for this specific prop (always fetches fresh data)
84
+ const resolveProp = async (options?: { silent?: boolean }) => {
85
+ const propState = (to.meta._injectionState as Record<string, { loading: boolean; error: Error | null }>)[propName]!
86
+ if (!options?.silent) {
87
+ propState.loading = true
88
+ }
89
+ propState.error = null
90
+
91
+ try {
92
+ const resolver = cfg.resolve(paramValue)
93
+ let payload = await resolver.resolve()
94
+ if (typeof cfg.getter === 'function') {
95
+ payload = cfg.getter(payload)
96
+ }
97
+
98
+ // Update cache
99
+ cache[propName] = { paramValue, payload }
100
+
101
+ // Updating the reactive object triggers the component re-render
102
+ const injectedProps = to.meta._injectedProps as Record<string, unknown>
103
+ injectedProps[propName] = payload
104
+ return payload
105
+ } catch (e) {
106
+ propState.error = e instanceof Error ? e : new Error(String(e))
107
+ throw e
108
+ } finally {
109
+ propState.loading = false
110
+ }
71
111
  }
72
112
 
73
113
  resolvers[propName] = resolveProp
74
114
 
75
- await resolveProp()
115
+ // Reuse cached value if the param hasn't changed (e.g. navigating between child routes)
116
+ const cached = cache[propName]
117
+ if (cached && cached.paramValue === paramValue) {
118
+ console.debug(`[Route Injection] Using cached value for prop "${propName}" (param "${cfg.from}" unchanged)`)
119
+ const injectedPropsCache = to.meta._injectedProps as Record<string, unknown>
120
+ injectedPropsCache[propName] = cached.payload
76
121
 
77
- console.debug(`[Route Injection] Successfully resolved prop "${propName}" for route "${record.path}"`)
122
+ continue
123
+ }
124
+
125
+ // Set loading immediately and queue resolver (non-blocking)
126
+ const injectionState = to.meta._injectionState as Record<string, { loading: boolean; error: Error | null }>
127
+ injectionState[propName]!.loading = true
128
+ pendingResolvers.push(async () => {
129
+ try {
130
+ await resolveProp()
131
+ } catch (e) {
132
+ console.error(`[Route Injection] Failed to resolve prop "${propName}"`, e)
133
+ }
134
+ })
135
+
136
+ console.debug(`[Route Injection] Queued resolution for prop "${propName}" for route "${record.path}"`)
137
+ }
138
+ }
139
+
140
+ // Evict cache entries that are no longer part of the active route hierarchy
141
+ for (const key of Object.keys(cache)) {
142
+ if (!activePropNames.has(key)) {
143
+ delete cache[key]
78
144
  }
79
145
  }
80
146
 
81
147
  // Attach the resolvers and a global refresh helper to the route meta
82
148
  to.meta['_injectedResolvers'] = resolvers
83
- to.meta['refresh'] = async (propName: string) => {
149
+ to.meta['refresh'] = async (propName: string, options?: { silent?: boolean }) => {
84
150
  if (resolvers[propName]) {
85
- return await resolvers[propName]()
151
+ return await resolvers[propName](options)
86
152
  }
87
153
 
88
154
  console.warn(`[Route Injection] No resolver found for "${propName}"`)
89
155
 
90
156
  return undefined
91
157
  }
158
+
159
+ // Fire pending resolvers without blocking navigation
160
+ if (pendingResolvers.length > 0) {
161
+ Promise.all(pendingResolvers.map((fn) => fn()))
162
+ }
92
163
  })
93
164
  }
@@ -1,3 +1,4 @@
1
+ import { type Component } from 'vue'
1
2
  import { type RouteLocationNormalized } from 'vue-router'
2
3
 
3
4
  /**
@@ -42,7 +43,11 @@ declare module 'vue-router' {
42
43
  interface RouteMeta {
43
44
  _injectedProps?: Record<string, any>
44
45
  _injectedResolvers?: Record<string, () => Promise<any>>
45
- refresh?: (propName: string) => Promise<any>
46
+ _injectionState?: Record<string, { loading: boolean; error: Error | null }>
47
+ _errorComponent?: Component
48
+ _loadingComponent?: Component
49
+ _lazy?: boolean
50
+ refresh?: (propName: string, options?: { silent?: boolean }) => Promise<any>
46
51
  inject?: Record<string, unknown>
47
52
  }
48
53
  }