@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.
- package/CHANGELOG.md +63 -0
- package/README.md +14 -67
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +270 -0
- package/coverage/coverage-final.json +10 -0
- package/coverage/favicon.png +0 -0
- package/coverage/hooks/index.html +191 -0
- package/coverage/hooks/use-container.mts.html +268 -0
- package/coverage/hooks/use-invalidate.mts.html +208 -0
- package/coverage/hooks/use-optional-service.mts.html +910 -0
- package/coverage/hooks/use-scope.mts.html +346 -0
- package/coverage/hooks/use-service.mts.html +760 -0
- package/coverage/hooks/use-suspense-service.mts.html +784 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/providers/container-provider.mts.html +139 -0
- package/coverage/providers/context.mts.html +130 -0
- package/coverage/providers/index.html +146 -0
- package/coverage/providers/scope-provider.mts.html +355 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/lib/index.d.mts +3 -57
- package/lib/index.d.mts.map +1 -1
- package/lib/index.d.ts +3 -57
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +59 -72
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +60 -72
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/hooks/__tests__/use-container.spec.mts +95 -4
- package/src/hooks/__tests__/use-invalidate.spec.mts +10 -146
- package/src/hooks/__tests__/use-scope.spec.mts +293 -0
- package/src/hooks/__tests__/use-service.spec.mts +2 -2
- package/src/hooks/index.mts +1 -1
- package/src/hooks/use-container.mts +6 -3
- package/src/hooks/use-invalidate.mts +1 -82
- package/src/hooks/use-optional-service.mts +23 -32
- package/src/hooks/use-service.mts +31 -33
- package/src/hooks/use-suspense-service.mts +16 -24
- package/src/providers/__tests__/scope-provider.spec.mts +5 -2
- 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.
|
|
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
|
|
82
|
+
it('should refetch service when refetch is called', async () => {
|
|
83
83
|
let callCount = 0
|
|
84
84
|
|
|
85
85
|
@Injectable({ registry })
|
package/src/hooks/index.mts
CHANGED
|
@@ -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 {
|
|
7
|
+
export { useInvalidateInstance } from './use-invalidate.mjs'
|
|
8
8
|
export {
|
|
9
9
|
useScope,
|
|
10
10
|
useScopeOrThrow,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { Container,
|
|
1
|
+
import type { Container, ScopedContainer } from '@navios/di'
|
|
2
2
|
|
|
3
3
|
import { useContext } from 'react'
|
|
4
4
|
|
|
5
|
-
import {
|
|
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():
|
|
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
|
|
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.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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,
|
|
227
|
+
}, [container, token, args])
|
|
231
228
|
|
|
232
229
|
// Subscribe to invalidation events
|
|
233
230
|
useEffect(() => {
|
|
234
|
-
const eventBus =
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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,
|
|
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.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 =
|
|
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 =
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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,
|
|
211
|
+
}, [container, rootContainer, token, args, refetchCounter])
|
|
214
212
|
|
|
215
213
|
const refetch = useCallback(() => {
|
|
216
214
|
setRefetchCounter((c) => c + 1)
|