@navios/di-react 0.8.0 → 1.0.0-alpha.3

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +14 -67
  3. package/coverage/base.css +224 -0
  4. package/coverage/block-navigation.js +87 -0
  5. package/coverage/clover.xml +270 -0
  6. package/coverage/coverage-final.json +10 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/hooks/index.html +191 -0
  9. package/coverage/hooks/use-container.mts.html +268 -0
  10. package/coverage/hooks/use-invalidate.mts.html +208 -0
  11. package/coverage/hooks/use-optional-service.mts.html +910 -0
  12. package/coverage/hooks/use-scope.mts.html +346 -0
  13. package/coverage/hooks/use-service.mts.html +760 -0
  14. package/coverage/hooks/use-suspense-service.mts.html +784 -0
  15. package/coverage/index.html +131 -0
  16. package/coverage/prettify.css +1 -0
  17. package/coverage/prettify.js +2 -0
  18. package/coverage/providers/container-provider.mts.html +139 -0
  19. package/coverage/providers/context.mts.html +130 -0
  20. package/coverage/providers/index.html +146 -0
  21. package/coverage/providers/scope-provider.mts.html +355 -0
  22. package/coverage/sort-arrow-sprite.png +0 -0
  23. package/coverage/sorter.js +210 -0
  24. package/lib/index.d.mts +3 -57
  25. package/lib/index.d.mts.map +1 -1
  26. package/lib/index.d.ts +3 -57
  27. package/lib/index.d.ts.map +1 -1
  28. package/lib/index.js +59 -72
  29. package/lib/index.js.map +1 -1
  30. package/lib/index.mjs +60 -72
  31. package/lib/index.mjs.map +1 -1
  32. package/package.json +3 -3
  33. package/src/hooks/__tests__/use-container.spec.mts +95 -4
  34. package/src/hooks/__tests__/use-invalidate.spec.mts +10 -146
  35. package/src/hooks/__tests__/use-scope.spec.mts +293 -0
  36. package/src/hooks/__tests__/use-service.spec.mts +2 -2
  37. package/src/hooks/index.mts +1 -1
  38. package/src/hooks/use-container.mts +6 -3
  39. package/src/hooks/use-invalidate.mts +1 -82
  40. package/src/hooks/use-optional-service.mts +23 -32
  41. package/src/hooks/use-service.mts +31 -33
  42. package/src/hooks/use-suspense-service.mts +16 -24
  43. package/src/providers/__tests__/scope-provider.spec.mts +5 -2
  44. package/src/providers/scope-provider.mts +1 -8
@@ -0,0 +1,293 @@
1
+ import { Container, globalRegistry, Registry } from '@navios/di'
2
+
3
+ import { renderHook } from '@testing-library/react'
4
+ import { createElement } from 'react'
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6
+
7
+ import { ContainerProvider } from '../../providers/container-provider.mjs'
8
+ import { ScopeProvider } from '../../providers/scope-provider.mjs'
9
+ import {
10
+ useScope,
11
+ useScopedContainer,
12
+ useScopedContainerOrThrow,
13
+ useScopeMetadata,
14
+ useScopeOrThrow,
15
+ } from '../use-scope.mjs'
16
+
17
+ describe('useScope', () => {
18
+ let container: Container
19
+ let registry: Registry
20
+
21
+ beforeEach(() => {
22
+ registry = new Registry(globalRegistry)
23
+ container = new Container(registry)
24
+ })
25
+
26
+ afterEach(async () => {
27
+ await container.dispose()
28
+ })
29
+
30
+ it('should return null when not inside a ScopeProvider', () => {
31
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
32
+ // @ts-expect-error - container is required
33
+ createElement(ContainerProvider, { container }, children)
34
+
35
+ const { result } = renderHook(() => useScope(), { wrapper })
36
+
37
+ expect(result.current).toBeNull()
38
+ })
39
+
40
+ it('should return the scope ID when inside a ScopeProvider', () => {
41
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
42
+ createElement(
43
+ ContainerProvider,
44
+ // @ts-expect-error - props are not typed
45
+ { container },
46
+ // @ts-expect-error - props are not typed
47
+ createElement(ScopeProvider, { scopeId: 'test-scope-123' }, children),
48
+ )
49
+
50
+ const { result } = renderHook(() => useScope(), { wrapper })
51
+
52
+ expect(result.current).toBe('test-scope-123')
53
+ })
54
+
55
+ it('should return the innermost scope ID when nested', () => {
56
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
57
+ createElement(
58
+ ContainerProvider,
59
+ // @ts-expect-error - props are not typed
60
+ { container },
61
+ createElement(
62
+ ScopeProvider,
63
+ // @ts-expect-error - props are not typed
64
+ { scopeId: 'outer-scope' },
65
+ // @ts-expect-error - props are not typed
66
+ createElement(ScopeProvider, { scopeId: 'inner-scope' }, children),
67
+ ),
68
+ )
69
+
70
+ const { result } = renderHook(() => useScope(), { wrapper })
71
+
72
+ expect(result.current).toBe('inner-scope')
73
+ })
74
+ })
75
+
76
+ describe('useScopeOrThrow', () => {
77
+ let container: Container
78
+ let registry: Registry
79
+
80
+ beforeEach(() => {
81
+ registry = new Registry(globalRegistry)
82
+ container = new Container(registry)
83
+ })
84
+
85
+ afterEach(async () => {
86
+ await container.dispose()
87
+ })
88
+
89
+ it('should throw when not inside a ScopeProvider', () => {
90
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
91
+ // @ts-expect-error - container is required
92
+ createElement(ContainerProvider, { container }, children)
93
+
94
+ expect(() => {
95
+ renderHook(() => useScopeOrThrow(), { wrapper })
96
+ }).toThrow('useScopeOrThrow must be used within a ScopeProvider')
97
+ })
98
+
99
+ it('should return the scope ID when inside a ScopeProvider', () => {
100
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
101
+ createElement(
102
+ ContainerProvider,
103
+ // @ts-expect-error - props are not typed
104
+ { container },
105
+ // @ts-expect-error - props are not typed
106
+ createElement(ScopeProvider, { scopeId: 'test-scope' }, children),
107
+ )
108
+
109
+ const { result } = renderHook(() => useScopeOrThrow(), { wrapper })
110
+
111
+ expect(result.current).toBe('test-scope')
112
+ })
113
+ })
114
+
115
+ describe('useScopedContainer', () => {
116
+ let container: Container
117
+ let registry: Registry
118
+
119
+ beforeEach(() => {
120
+ registry = new Registry(globalRegistry)
121
+ container = new Container(registry)
122
+ })
123
+
124
+ afterEach(async () => {
125
+ await container.dispose()
126
+ })
127
+
128
+ it('should return null when not inside a ScopeProvider', () => {
129
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
130
+ // @ts-expect-error - container is required
131
+ createElement(ContainerProvider, { container }, children)
132
+
133
+ const { result } = renderHook(() => useScopedContainer(), { wrapper })
134
+
135
+ expect(result.current).toBeNull()
136
+ })
137
+
138
+ it('should return the ScopedContainer when inside a ScopeProvider', () => {
139
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
140
+ createElement(
141
+ ContainerProvider,
142
+ // @ts-expect-error - props are not typed
143
+ { container },
144
+ // @ts-expect-error - props are not typed
145
+ createElement(ScopeProvider, { scopeId: 'test-scope' }, children),
146
+ )
147
+
148
+ const { result } = renderHook(() => useScopedContainer(), { wrapper })
149
+
150
+ expect(result.current).not.toBeNull()
151
+ expect(result.current?.getRequestId()).toBe('test-scope')
152
+ })
153
+ })
154
+
155
+ describe('useScopedContainerOrThrow', () => {
156
+ let container: Container
157
+ let registry: Registry
158
+
159
+ beforeEach(() => {
160
+ registry = new Registry(globalRegistry)
161
+ container = new Container(registry)
162
+ })
163
+
164
+ afterEach(async () => {
165
+ await container.dispose()
166
+ })
167
+
168
+ it('should throw when not inside a ScopeProvider', () => {
169
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
170
+ // @ts-expect-error - container is required
171
+ createElement(ContainerProvider, { container }, children)
172
+
173
+ expect(() => {
174
+ renderHook(() => useScopedContainerOrThrow(), { wrapper })
175
+ }).toThrow('useScopedContainerOrThrow must be used within a ScopeProvider')
176
+ })
177
+
178
+ it('should return the ScopedContainer when inside a ScopeProvider', () => {
179
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
180
+ createElement(
181
+ ContainerProvider,
182
+ // @ts-expect-error - props are not typed
183
+ { container },
184
+ // @ts-expect-error - props are not typed
185
+ createElement(ScopeProvider, { scopeId: 'test-scope' }, children),
186
+ )
187
+
188
+ const { result } = renderHook(() => useScopedContainerOrThrow(), { wrapper })
189
+
190
+ expect(result.current.getRequestId()).toBe('test-scope')
191
+ })
192
+ })
193
+
194
+ describe('useScopeMetadata', () => {
195
+ let container: Container
196
+ let registry: Registry
197
+
198
+ beforeEach(() => {
199
+ registry = new Registry(globalRegistry)
200
+ container = new Container(registry)
201
+ })
202
+
203
+ afterEach(async () => {
204
+ await container.dispose()
205
+ })
206
+
207
+ it('should return undefined when not inside a ScopeProvider', () => {
208
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
209
+ // @ts-expect-error - container is required
210
+ createElement(ContainerProvider, { container }, children)
211
+
212
+ const { result } = renderHook(() => useScopeMetadata('someKey'), { wrapper })
213
+
214
+ expect(result.current).toBeUndefined()
215
+ })
216
+
217
+ it('should return undefined when key does not exist', () => {
218
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
219
+ createElement(
220
+ ContainerProvider,
221
+ // @ts-expect-error - props are not typed
222
+ { container },
223
+ createElement(
224
+ ScopeProvider,
225
+ // @ts-expect-error - props are not typed
226
+ { scopeId: 'test-scope', metadata: { existingKey: 'value' } },
227
+ children,
228
+ ),
229
+ )
230
+
231
+ const { result } = renderHook(() => useScopeMetadata('nonExistentKey'), {
232
+ wrapper,
233
+ })
234
+
235
+ expect(result.current).toBeUndefined()
236
+ })
237
+
238
+ it('should return the metadata value when key exists', () => {
239
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
240
+ createElement(
241
+ ContainerProvider,
242
+ // @ts-expect-error - props are not typed
243
+ { container },
244
+ createElement(
245
+ ScopeProvider,
246
+ // @ts-expect-error - props are not typed
247
+ {
248
+ scopeId: 'test-scope',
249
+ metadata: { userId: '123', theme: 'dark' },
250
+ },
251
+ children,
252
+ ),
253
+ )
254
+
255
+ // Test both values in a single hook to ensure they're from the same scope
256
+ const { result } = renderHook(
257
+ () => ({
258
+ userId: useScopeMetadata<string>('userId'),
259
+ theme: useScopeMetadata<string>('theme'),
260
+ }),
261
+ { wrapper },
262
+ )
263
+
264
+ expect(result.current.userId).toBe('123')
265
+ expect(result.current.theme).toBe('dark')
266
+ })
267
+
268
+ it('should return metadata from the innermost scope', () => {
269
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
270
+ createElement(
271
+ ContainerProvider,
272
+ // @ts-expect-error - props are not typed
273
+ { container },
274
+ createElement(
275
+ ScopeProvider,
276
+ // @ts-expect-error - props are not typed
277
+ { scopeId: 'outer-scope', metadata: { value: 'outer' } },
278
+ createElement(
279
+ ScopeProvider,
280
+ // @ts-expect-error - props are not typed
281
+ { scopeId: 'inner-scope', metadata: { value: 'inner' } },
282
+ children,
283
+ ),
284
+ ),
285
+ )
286
+
287
+ const { result } = renderHook(() => useScopeMetadata<string>('value'), {
288
+ wrapper,
289
+ })
290
+
291
+ expect(result.current).toBe('inner')
292
+ })
293
+ })
@@ -18,7 +18,7 @@ describe('useService', () => {
18
18
  })
19
19
 
20
20
  afterEach(async () => {
21
- await container.clear()
21
+ await container.dispose()
22
22
  })
23
23
 
24
24
  const createWrapper = () => {
@@ -79,7 +79,7 @@ describe('useService', () => {
79
79
  expect(result.current.isSuccess).toBe(false)
80
80
  })
81
81
 
82
- it.skip('should refetch service when refetch is called', async () => {
82
+ it('should refetch service when refetch is called', async () => {
83
83
  let callCount = 0
84
84
 
85
85
  @Injectable({ registry })
@@ -4,7 +4,7 @@ export type { UseServiceResult } from './use-service.mjs'
4
4
  export { useSuspenseService } from './use-suspense-service.mjs'
5
5
  export { useOptionalService } from './use-optional-service.mjs'
6
6
  export type { UseOptionalServiceResult } from './use-optional-service.mjs'
7
- export { useInvalidate, useInvalidateInstance } from './use-invalidate.mjs'
7
+ export { useInvalidateInstance } from './use-invalidate.mjs'
8
8
  export {
9
9
  useScope,
10
10
  useScopeOrThrow,
@@ -1,8 +1,11 @@
1
- import type { Container, IContainer, ScopedContainer } from '@navios/di'
1
+ import type { Container, ScopedContainer } from '@navios/di'
2
2
 
3
3
  import { useContext } from 'react'
4
4
 
5
- import { ContainerContext, ScopedContainerContext } from '../providers/context.mjs'
5
+ import {
6
+ ContainerContext,
7
+ ScopedContainerContext,
8
+ } from '../providers/context.mjs'
6
9
 
7
10
  /**
8
11
  * Hook to get the current container (ScopedContainer if inside ScopeProvider, otherwise Container).
@@ -14,7 +17,7 @@ import { ContainerContext, ScopedContainerContext } from '../providers/context.m
14
17
  *
15
18
  * @returns The current container (ScopedContainer or Container)
16
19
  */
17
- export function useContainer(): IContainer {
20
+ export function useContainer(): Container | ScopedContainer {
18
21
  const scopedContainer = useContext(ScopedContainerContext)
19
22
  const container = useContext(ContainerContext)
20
23
 
@@ -1,87 +1,6 @@
1
- import type {
2
- BoundInjectionToken,
3
- ClassType,
4
- FactoryInjectionToken,
5
- InjectionToken,
6
- InjectionTokenSchemaType,
7
- } from '@navios/di'
8
-
9
1
  import { useCallback } from 'react'
10
2
 
11
- import { useContainer, useRootContainer } from './use-container.mjs'
12
-
13
- type InvalidatableToken =
14
- | ClassType
15
- | InjectionToken<any, any>
16
- | BoundInjectionToken<any, any>
17
- | FactoryInjectionToken<any, any>
18
-
19
- /**
20
- * Hook that returns a function to invalidate a service by its token.
21
- *
22
- * When called, this will:
23
- * 1. Destroy the current service instance
24
- * 2. Trigger re-fetch in all components using useService/useSuspenseService for that token
25
- *
26
- * @example
27
- * ```tsx
28
- * function UserProfile() {
29
- * const { data: user } = useService(UserService)
30
- * const invalidateUser = useInvalidate(UserService)
31
- *
32
- * const handleRefresh = () => {
33
- * invalidateUser() // All components using UserService will re-fetch
34
- * }
35
- *
36
- * return (
37
- * <div>
38
- * <span>{user?.name}</span>
39
- * <button onClick={handleRefresh}>Refresh</button>
40
- * </div>
41
- * )
42
- * }
43
- * ```
44
- */
45
- export function useInvalidate<T extends InvalidatableToken>(
46
- token: T,
47
- ): () => Promise<void>
48
-
49
- /**
50
- * Hook that returns a function to invalidate a service by its token with args.
51
- *
52
- * @example
53
- * ```tsx
54
- * function UserProfile({ userId }: { userId: string }) {
55
- * const args = useMemo(() => ({ userId }), [userId])
56
- * const { data: user } = useService(UserToken, args)
57
- * const invalidateUser = useInvalidate(UserToken, args)
58
- *
59
- * return (
60
- * <div>
61
- * <span>{user?.name}</span>
62
- * <button onClick={() => invalidateUser()}>Refresh</button>
63
- * </div>
64
- * )
65
- * }
66
- * ```
67
- */
68
- export function useInvalidate<T, S extends InjectionTokenSchemaType>(
69
- token: InjectionToken<T, S>,
70
- args: S extends undefined ? never : unknown,
71
- ): () => Promise<void>
72
-
73
- export function useInvalidate(
74
- token: InvalidatableToken,
75
- args?: unknown,
76
- ): () => Promise<void> {
77
- const rootContainer = useRootContainer()
78
- const serviceLocator = rootContainer.getServiceLocator()
79
-
80
- return useCallback(async () => {
81
- const instanceName = serviceLocator.getInstanceIdentifier(token, args)
82
- await serviceLocator.invalidate(instanceName)
83
- }, [serviceLocator, token, args])
84
- }
3
+ import { useContainer } from './use-container.mjs'
85
4
 
86
5
  /**
87
6
  * Hook that returns a function to invalidate a service instance directly.
@@ -155,7 +155,6 @@ export function useOptionalService(
155
155
  // useContainer returns ScopedContainer if inside ScopeProvider, otherwise Container
156
156
  const container = useContainer()
157
157
  const rootContainer = useRootContainer()
158
- const serviceLocator = rootContainer.getServiceLocator()
159
158
 
160
159
  // Try to get the instance synchronously first for better performance
161
160
  // This avoids the async loading state when the instance is already cached
@@ -184,13 +183,12 @@ export function useOptionalService(
184
183
  useEffect(() => {
185
184
  if (argsRef.current !== args) {
186
185
  if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
187
- console.log(`WARNING: useOptionalService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
188
- This is likely because you are using not memoized value that is not stable.
189
- Please use a memoized value or use a different approach to pass the args.
190
- Example:
191
- const args = useMemo(() => ({ userId: '123' }), [])
192
- return useOptionalService(UserToken, args)
193
- `)
186
+ console.warn(`useOptionalService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
187
+ This is likely because you are using a value that is not memoized.
188
+ Please use a memoized value or use a different approach to pass the args.
189
+ Example:
190
+ const args = useMemo(() => ({ userId: '123' }), [])
191
+ return useOptionalService(UserToken, args)`)
194
192
  }
195
193
  argsRef.current = args
196
194
  }
@@ -206,12 +204,11 @@ export function useOptionalService(
206
204
  token as AnyInjectableType,
207
205
  args as any,
208
206
  )
209
-
210
207
  // Get instance name for event subscription
211
- instanceNameRef.current = serviceLocator.getInstanceIdentifier(
212
- token as AnyInjectableType,
213
- args,
214
- )
208
+ const instanceName = container.calculateInstanceName(token, args)
209
+ if (instanceName) {
210
+ instanceNameRef.current = instanceName
211
+ }
215
212
  dispatch({ type: 'success', data: instance })
216
213
  } catch (error) {
217
214
  // Caught exceptions are treated as errors
@@ -227,42 +224,36 @@ export function useOptionalService(
227
224
  dispatch({ type: 'error', error: err })
228
225
  }
229
226
  }
230
- }, [container, serviceLocator, token, args])
227
+ }, [container, token, args])
231
228
 
232
229
  // Subscribe to invalidation events
233
230
  useEffect(() => {
234
- const eventBus = serviceLocator.getEventBus()
231
+ const eventBus = rootContainer.getEventBus()
235
232
  let unsubscribe: (() => void) | undefined
236
233
 
237
234
  // If we already have a sync instance from initial render, just set up subscription
238
235
  // Otherwise, fetch async
239
236
  const syncInstance = initialSyncInstanceRef.current
240
237
  if (syncInstance) {
241
- instanceNameRef.current = serviceLocator.getInstanceIdentifier(
242
- token as AnyInjectableType,
243
- args,
244
- )
245
- unsubscribe = eventBus.on(instanceNameRef.current, 'destroy', () => {
246
- void fetchService()
247
- })
238
+ const instanceName = container.calculateInstanceName(token, args)
239
+ if (instanceName) {
240
+ instanceNameRef.current = instanceName
241
+ unsubscribe = eventBus.on(instanceName, 'destroy', () => {
242
+ // Re-fetch when the service is invalidated
243
+ void fetchService()
244
+ })
245
+ }
248
246
  } else {
249
- void fetchService()
250
-
251
- // Set up subscription after we have the instance name
252
- const setupSubscription = () => {
247
+ void fetchService().then(() => {
253
248
  if (instanceNameRef.current) {
254
249
  unsubscribe = eventBus.on(instanceNameRef.current, 'destroy', () => {
255
250
  // Re-fetch when the service is invalidated
256
251
  void fetchService()
257
252
  })
258
253
  }
259
- }
260
-
261
- // Wait a tick for the instance name to be set
262
- const timeoutId = setTimeout(setupSubscription, 10)
254
+ })
263
255
 
264
256
  return () => {
265
- clearTimeout(timeoutId)
266
257
  unsubscribe?.()
267
258
  }
268
259
  }
@@ -270,7 +261,7 @@ export function useOptionalService(
270
261
  return () => {
271
262
  unsubscribe?.()
272
263
  }
273
- }, [fetchService, serviceLocator, token, args])
264
+ }, [fetchService, rootContainer, token, args])
274
265
 
275
266
  return {
276
267
  data: state.status === 'success' ? state.data : undefined,
@@ -105,7 +105,6 @@ export function useService(
105
105
  // This automatically handles request-scoped services correctly
106
106
  const container = useContainer()
107
107
  const rootContainer = useRootContainer()
108
- const serviceLocator = rootContainer.getServiceLocator()
109
108
 
110
109
  // Try to get the instance synchronously first for better performance
111
110
  // This avoids the async loading state when the instance is already cached
@@ -131,13 +130,12 @@ export function useService(
131
130
  useEffect(() => {
132
131
  if (argsRef.current !== args) {
133
132
  if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
134
- console.log(`WARNING: useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
135
- This is likely because you are using not memoized value that is not stable.
136
- Please use a memoized value or use a different approach to pass the args.
137
- Example:
138
- const args = useMemo(() => ({ userId: '123' }), [])
139
- return useService(UserToken, args)
140
- `)
133
+ console.warn(`useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
134
+ This is likely because you are using a value that is not memoized.
135
+ Please use a memoized value or use a different approach to pass the args.
136
+ Example:
137
+ const args = useMemo(() => ({ userId: '123' }), [])
138
+ return useService(UserToken, args)`)
141
139
  }
142
140
  argsRef.current = args
143
141
  }
@@ -146,7 +144,7 @@ export function useService(
146
144
 
147
145
  // Subscribe to invalidation events
148
146
  useEffect(() => {
149
- const eventBus = serviceLocator.getEventBus()
147
+ const eventBus = rootContainer.getEventBus()
150
148
  let unsubscribe: (() => void) | undefined
151
149
  let isMounted = true
152
150
 
@@ -163,22 +161,23 @@ export function useService(
163
161
  if (!isMounted) return
164
162
 
165
163
  // Get instance name for event subscription
166
- const instanceName = serviceLocator.getInstanceIdentifier(
167
- token as AnyInjectableType,
168
- args,
169
- )
170
- instanceNameRef.current = instanceName
164
+ const instanceName = container.calculateInstanceName(token, args)
165
+ if (instanceName) {
166
+ instanceNameRef.current = instanceName
167
+ }
171
168
 
172
169
  dispatch({ type: 'success', data: instance })
173
170
 
174
171
  // Set up subscription after we have the instance
175
- unsubscribe = eventBus.on(instanceName, 'destroy', () => {
176
- // Re-fetch when the service is invalidated
177
- if (isMounted) {
178
- dispatch({ type: 'loading' })
179
- void fetchAndSubscribe()
180
- }
181
- })
172
+ if (instanceName) {
173
+ unsubscribe = eventBus.on(instanceName, 'destroy', () => {
174
+ // Re-fetch when the service is invalidated
175
+ if (isMounted) {
176
+ dispatch({ type: 'loading' })
177
+ void fetchAndSubscribe()
178
+ }
179
+ })
180
+ }
182
181
  } catch (error) {
183
182
  if (isMounted) {
184
183
  dispatch({ type: 'error', error: error as Error })
@@ -190,17 +189,16 @@ export function useService(
190
189
  // Otherwise, fetch async
191
190
  const syncInstance = initialSyncInstanceRef.current
192
191
  if (syncInstance && refetchCounter === 0) {
193
- const instanceName = serviceLocator.getInstanceIdentifier(
194
- token as AnyInjectableType,
195
- args,
196
- )
197
- instanceNameRef.current = instanceName
198
- unsubscribe = eventBus.on(instanceName, 'destroy', () => {
199
- if (isMounted) {
200
- dispatch({ type: 'loading' })
201
- void fetchAndSubscribe()
202
- }
203
- })
192
+ const instanceName = container.calculateInstanceName(token, args)
193
+ if (instanceName) {
194
+ instanceNameRef.current = instanceName
195
+ unsubscribe = eventBus.on(instanceName, 'destroy', () => {
196
+ if (isMounted) {
197
+ dispatch({ type: 'loading' })
198
+ void fetchAndSubscribe()
199
+ }
200
+ })
201
+ }
204
202
  } else {
205
203
  dispatch({ type: 'loading' })
206
204
  void fetchAndSubscribe()
@@ -210,7 +208,7 @@ export function useService(
210
208
  isMounted = false
211
209
  unsubscribe?.()
212
210
  }
213
- }, [container, serviceLocator, token, args, refetchCounter])
211
+ }, [container, rootContainer, token, args, refetchCounter])
214
212
 
215
213
  const refetch = useCallback(() => {
216
214
  setRefetchCounter((c) => c + 1)