@navios/di-react 0.1.0 → 0.2.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 +39 -0
- package/README.md +609 -28
- package/lib/index.d.mts +297 -18
- package/lib/index.d.mts.map +1 -0
- package/lib/index.d.ts +297 -18
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +536 -346
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +533 -345
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/project.json +2 -2
- package/src/hooks/__tests__/use-service.spec.mts +1 -1
- package/src/hooks/index.mts +8 -2
- package/src/hooks/use-container.mts +47 -2
- package/src/hooks/use-invalidate.mts +3 -3
- package/src/hooks/use-optional-service.mts +59 -34
- package/src/hooks/use-scope.mts +66 -5
- package/src/hooks/use-service.mts +44 -18
- package/src/hooks/use-suspense-service.mts +48 -29
- package/src/providers/__tests__/scope-provider.spec.mts +84 -1
- package/src/providers/container-provider.mts +2 -6
- package/src/providers/context.mts +11 -1
- package/src/providers/index.mts +2 -2
- package/src/providers/scope-provider.mts +34 -25
- package/{tsup.config.mts → tsdown.config.mts} +4 -3
- package/lib/_tsup-dts-rollup.d.mts +0 -304
- package/lib/_tsup-dts-rollup.d.ts +0 -304
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# @navios/di-react
|
|
2
2
|
|
|
3
|
-
React integration for `@navios/di` dependency injection container.
|
|
3
|
+
React integration for `@navios/di` dependency injection container. Provides a set of hooks and providers to seamlessly use dependency injection in React applications with automatic service lifecycle management, invalidation subscriptions, and request-scoped service isolation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **🎯 Type-Safe**: Full TypeScript support with compile-time type checking
|
|
8
|
+
- **🎨 Flexible API**: Support for classes, injection tokens, and factory tokens
|
|
9
|
+
- **⚙️ Zod Integration**: Type-safe arguments with Zod schema validation
|
|
10
|
+
- **🔄 Automatic Invalidation**: Services automatically re-fetch when invalidated
|
|
11
|
+
- **⚡ React Suspense Support**: Use `useSuspenseService` with React Suspense for declarative loading
|
|
12
|
+
- **🔌 Request Scopes**: Isolate services per request/component tree with `ScopeProvider`
|
|
13
|
+
- **📦 Optional Services**: Load services that may not be registered with `useOptionalService`
|
|
14
|
+
- **🚀 Performance**: Synchronous resolution when instances are already cached
|
|
4
15
|
|
|
5
16
|
## Installation
|
|
6
17
|
|
|
@@ -8,13 +19,52 @@ React integration for `@navios/di` dependency injection container.
|
|
|
8
19
|
npm install @navios/di-react @navios/di react
|
|
9
20
|
# or
|
|
10
21
|
yarn add @navios/di-react @navios/di react
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @navios/di-react @navios/di react
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Set up the Container Provider
|
|
29
|
+
|
|
30
|
+
Wrap your application with `ContainerProvider`:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Container } from '@navios/di'
|
|
34
|
+
import { ContainerProvider } from '@navios/di-react'
|
|
35
|
+
|
|
36
|
+
const container = new Container()
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
return (
|
|
40
|
+
<ContainerProvider container={container}>
|
|
41
|
+
<YourApp />
|
|
42
|
+
</ContainerProvider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
11
45
|
```
|
|
12
46
|
|
|
13
|
-
|
|
47
|
+
### 2. Use Services in Components
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useService } from '@navios/di-react'
|
|
51
|
+
import { MyService } from './services/my-service'
|
|
14
52
|
|
|
15
|
-
|
|
53
|
+
function MyComponent() {
|
|
54
|
+
const { data, isLoading, isError, error } = useService(MyService)
|
|
16
55
|
|
|
17
|
-
|
|
56
|
+
if (isLoading) return <div>Loading...</div>
|
|
57
|
+
if (isError) return <div>Error: {error?.message}</div>
|
|
58
|
+
|
|
59
|
+
return <div>{data.someValue}</div>
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Core Concepts
|
|
64
|
+
|
|
65
|
+
### Container Provider
|
|
66
|
+
|
|
67
|
+
The `ContainerProvider` makes the DI container available to all child components via React context. You should wrap your application root with it.
|
|
18
68
|
|
|
19
69
|
```tsx
|
|
20
70
|
import { Container } from '@navios/di'
|
|
@@ -31,9 +81,44 @@ function App() {
|
|
|
31
81
|
}
|
|
32
82
|
```
|
|
33
83
|
|
|
84
|
+
**Note**: The container prop should be stable. Avoid creating a new container on every render. If you need to create the container dynamically, use `useMemo` or `useState`.
|
|
85
|
+
|
|
86
|
+
### Scope Provider
|
|
87
|
+
|
|
88
|
+
`ScopeProvider` creates an isolated request scope for dependency injection. Services with `scope: 'Request'` will be instantiated once per scope and shared among all components within that provider.
|
|
89
|
+
|
|
90
|
+
This is useful for:
|
|
91
|
+
|
|
92
|
+
- **Table rows** that need isolated state
|
|
93
|
+
- **Modal dialogs** with their own service instances
|
|
94
|
+
- **Multi-tenant scenarios**
|
|
95
|
+
- **Any case where you need isolated service instances**
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { ScopeProvider } from '@navios/di-react'
|
|
99
|
+
|
|
100
|
+
function Table({ rows }) {
|
|
101
|
+
return (
|
|
102
|
+
<table>
|
|
103
|
+
{rows.map((row) => (
|
|
104
|
+
<ScopeProvider
|
|
105
|
+
key={row.id}
|
|
106
|
+
scopeId={row.id}
|
|
107
|
+
metadata={{ rowData: row }}
|
|
108
|
+
>
|
|
109
|
+
<TableRow />
|
|
110
|
+
</ScopeProvider>
|
|
111
|
+
))}
|
|
112
|
+
</table>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Hooks
|
|
118
|
+
|
|
34
119
|
### useContainer
|
|
35
120
|
|
|
36
|
-
Access the container directly
|
|
121
|
+
Access the container directly. Automatically returns the `ScopedContainer` if inside a `ScopeProvider`, otherwise returns the root `Container`.
|
|
37
122
|
|
|
38
123
|
```tsx
|
|
39
124
|
import { useContainer } from '@navios/di-react'
|
|
@@ -41,7 +126,6 @@ import { useContainer } from '@navios/di-react'
|
|
|
41
126
|
function MyComponent() {
|
|
42
127
|
const container = useContainer()
|
|
43
128
|
|
|
44
|
-
// Use container methods directly
|
|
45
129
|
const handleClick = async () => {
|
|
46
130
|
const service = await container.get(MyService)
|
|
47
131
|
service.doSomething()
|
|
@@ -51,9 +135,28 @@ function MyComponent() {
|
|
|
51
135
|
}
|
|
52
136
|
```
|
|
53
137
|
|
|
138
|
+
### useRootContainer
|
|
139
|
+
|
|
140
|
+
Get the root container regardless of whether you're inside a `ScopeProvider`. Useful when you need to create new request scopes programmatically.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useRootContainer } from '@navios/di-react'
|
|
144
|
+
|
|
145
|
+
function MyComponent() {
|
|
146
|
+
const rootContainer = useRootContainer()
|
|
147
|
+
|
|
148
|
+
const createNewScope = () => {
|
|
149
|
+
const scopedContainer = rootContainer.beginRequest('new-scope')
|
|
150
|
+
// Use scopedContainer...
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return <button onClick={createNewScope}>Create Scope</button>
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
54
157
|
### useService
|
|
55
158
|
|
|
56
|
-
Fetch a service with loading/error states. Automatically re-fetches when the service is invalidated
|
|
159
|
+
Fetch a service with loading/error states. Automatically re-fetches when the service is invalidated.
|
|
57
160
|
|
|
58
161
|
```tsx
|
|
59
162
|
import { useService } from '@navios/di-react'
|
|
@@ -74,20 +177,24 @@ function MyComponent() {
|
|
|
74
177
|
}
|
|
75
178
|
```
|
|
76
179
|
|
|
77
|
-
With
|
|
180
|
+
#### With Injection Tokens and Arguments
|
|
78
181
|
|
|
79
182
|
```tsx
|
|
80
183
|
import { InjectionToken } from '@navios/di'
|
|
81
184
|
import { useService } from '@navios/di-react'
|
|
185
|
+
import { useMemo } from 'react'
|
|
82
186
|
import { z } from 'zod'
|
|
83
187
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
188
|
+
const UserSchema = z.object({ userId: z.string() })
|
|
189
|
+
const UserToken = InjectionToken.create<
|
|
190
|
+
{ userId: string; name: string },
|
|
191
|
+
typeof UserSchema
|
|
192
|
+
>('User', UserSchema)
|
|
88
193
|
|
|
89
194
|
function UserProfile({ userId }: { userId: string }) {
|
|
90
|
-
|
|
195
|
+
// Important: Memoize args to avoid unnecessary re-fetches
|
|
196
|
+
const args = useMemo(() => ({ userId }), [userId])
|
|
197
|
+
const { data: user, isLoading } = useService(UserToken, args)
|
|
91
198
|
|
|
92
199
|
if (isLoading) return <div>Loading...</div>
|
|
93
200
|
|
|
@@ -95,9 +202,11 @@ function UserProfile({ userId }: { userId: string }) {
|
|
|
95
202
|
}
|
|
96
203
|
```
|
|
97
204
|
|
|
205
|
+
**Important**: Always memoize arguments passed to `useService` to prevent unnecessary re-fetches. The hook uses reference equality to determine if arguments have changed.
|
|
206
|
+
|
|
98
207
|
### useSuspenseService
|
|
99
208
|
|
|
100
|
-
Use with React Suspense for a cleaner loading experience. Also subscribes to service invalidation
|
|
209
|
+
Use with React Suspense for a cleaner loading experience. Also subscribes to service invalidation.
|
|
101
210
|
|
|
102
211
|
```tsx
|
|
103
212
|
import { Suspense } from 'react'
|
|
@@ -119,27 +228,364 @@ function App() {
|
|
|
119
228
|
}
|
|
120
229
|
```
|
|
121
230
|
|
|
231
|
+
#### Error Boundaries
|
|
232
|
+
|
|
233
|
+
When using `useSuspenseService`, errors are thrown to the nearest error boundary. Make sure to wrap your components with an error boundary:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
237
|
+
import { Suspense } from 'react'
|
|
238
|
+
import { useSuspenseService } from '@navios/di-react'
|
|
239
|
+
|
|
240
|
+
function ErrorFallback({ error }) {
|
|
241
|
+
return <div>Error: {error.message}</div>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function App() {
|
|
245
|
+
return (
|
|
246
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
247
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
248
|
+
<MyComponent />
|
|
249
|
+
</Suspense>
|
|
250
|
+
</ErrorBoundary>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### useOptionalService
|
|
256
|
+
|
|
257
|
+
Load a service that may not be registered. Unlike `useService`, this hook does NOT throw an error if the service is not registered. Instead, it returns `isNotFound: true`.
|
|
258
|
+
|
|
259
|
+
This is useful for:
|
|
260
|
+
|
|
261
|
+
- **Optional dependencies** that may or may not be configured
|
|
262
|
+
- **Feature flags** where a service might not be available
|
|
263
|
+
- **Plugins or extensions** that are conditionally registered
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { useOptionalService } from '@navios/di-react'
|
|
267
|
+
|
|
268
|
+
function Analytics() {
|
|
269
|
+
const {
|
|
270
|
+
data: analytics,
|
|
271
|
+
isNotFound,
|
|
272
|
+
isLoading,
|
|
273
|
+
} = useOptionalService(AnalyticsService)
|
|
274
|
+
|
|
275
|
+
if (isLoading) return null
|
|
276
|
+
if (isNotFound) {
|
|
277
|
+
// Analytics service not configured, skip tracking
|
|
278
|
+
return null
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return <AnalyticsTracker service={analytics} />
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### useInvalidate
|
|
286
|
+
|
|
287
|
+
Get a function to invalidate a service by its token. When called, this will destroy the current service instance and trigger re-fetch in all components using `useService`/`useSuspenseService` for that token.
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
import { useService, useInvalidate } from '@navios/di-react'
|
|
291
|
+
|
|
292
|
+
function UserProfile() {
|
|
293
|
+
const { data: user } = useService(UserService)
|
|
294
|
+
const invalidateUser = useInvalidate(UserService)
|
|
295
|
+
|
|
296
|
+
const handleRefresh = () => {
|
|
297
|
+
invalidateUser() // All components using UserService will re-fetch
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div>
|
|
302
|
+
<span>{user?.name}</span>
|
|
303
|
+
<button onClick={handleRefresh}>Refresh</button>
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### With Arguments
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { useMemo } from 'react'
|
|
313
|
+
import { useService, useInvalidate } from '@navios/di-react'
|
|
314
|
+
|
|
315
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
316
|
+
const args = useMemo(() => ({ userId }), [userId])
|
|
317
|
+
const { data: user } = useService(UserToken, args)
|
|
318
|
+
const invalidateUser = useInvalidate(UserToken, args)
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div>
|
|
322
|
+
<span>{user?.name}</span>
|
|
323
|
+
<button onClick={() => invalidateUser()}>Refresh</button>
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### useInvalidateInstance
|
|
330
|
+
|
|
331
|
+
Invalidate a service instance directly without knowing its token.
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { useService, useInvalidateInstance } from '@navios/di-react'
|
|
335
|
+
|
|
336
|
+
function UserProfile() {
|
|
337
|
+
const { data: user } = useService(UserService)
|
|
338
|
+
const invalidateInstance = useInvalidateInstance()
|
|
339
|
+
|
|
340
|
+
const handleRefresh = () => {
|
|
341
|
+
if (user) {
|
|
342
|
+
invalidateInstance(user)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div>
|
|
348
|
+
<span>{user?.name}</span>
|
|
349
|
+
<button onClick={handleRefresh}>Refresh</button>
|
|
350
|
+
</div>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### useScope
|
|
356
|
+
|
|
357
|
+
Get the current scope ID. Returns `null` if not inside a `ScopeProvider`.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import { useScope } from '@navios/di-react'
|
|
361
|
+
|
|
362
|
+
function MyComponent() {
|
|
363
|
+
const scopeId = useScope()
|
|
364
|
+
|
|
365
|
+
if (!scopeId) {
|
|
366
|
+
return <div>Not in a scope</div>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return <div>Current scope: {scopeId}</div>
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### useScopeOrThrow
|
|
374
|
+
|
|
375
|
+
Get the current scope ID, throwing an error if not inside a `ScopeProvider`.
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { useScopeOrThrow } from '@navios/di-react'
|
|
379
|
+
|
|
380
|
+
function MyComponent() {
|
|
381
|
+
const scopeId = useScopeOrThrow() // Throws if not in ScopeProvider
|
|
382
|
+
|
|
383
|
+
return <div>Current scope: {scopeId}</div>
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### useScopedContainer
|
|
388
|
+
|
|
389
|
+
Get the current `ScopedContainer`. Returns `null` if not inside a `ScopeProvider`.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import { useScopedContainer } from '@navios/di-react'
|
|
393
|
+
|
|
394
|
+
function TableRow() {
|
|
395
|
+
const scope = useScopedContainer()
|
|
396
|
+
const rowData = scope?.getMetadata('rowData')
|
|
397
|
+
|
|
398
|
+
return <tr>{/* ... */}</tr>
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### useScopedContainerOrThrow
|
|
403
|
+
|
|
404
|
+
Get the current `ScopedContainer`, throwing an error if not inside a `ScopeProvider`.
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { useScopedContainerOrThrow } from '@navios/di-react'
|
|
408
|
+
|
|
409
|
+
function TableRow() {
|
|
410
|
+
const scope = useScopedContainerOrThrow()
|
|
411
|
+
const rowData = scope.getMetadata('rowData')
|
|
412
|
+
|
|
413
|
+
return <tr>{/* ... */}</tr>
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### useScopeMetadata
|
|
418
|
+
|
|
419
|
+
Get metadata from the current scope. Returns `undefined` if not inside a `ScopeProvider` or if the key doesn't exist.
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
import { useScopeMetadata } from '@navios/di-react'
|
|
423
|
+
|
|
424
|
+
// In parent component:
|
|
425
|
+
;<ScopeProvider metadata={{ userId: '123', theme: 'dark' }}>
|
|
426
|
+
<ChildComponent />
|
|
427
|
+
</ScopeProvider>
|
|
428
|
+
|
|
429
|
+
// In child component:
|
|
430
|
+
function ChildComponent() {
|
|
431
|
+
const userId = useScopeMetadata<string>('userId')
|
|
432
|
+
const theme = useScopeMetadata<'light' | 'dark'>('theme')
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div>
|
|
436
|
+
User: {userId}, Theme: {theme}
|
|
437
|
+
</div>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Service Invalidation
|
|
443
|
+
|
|
444
|
+
Both `useService` and `useSuspenseService` automatically subscribe to service invalidation events via the DI container's event bus. When a service is invalidated (e.g., via `container.invalidate(service)` or `useInvalidate`), the hooks will automatically:
|
|
445
|
+
|
|
446
|
+
1. Clear the cached instance
|
|
447
|
+
2. Re-fetch the service
|
|
448
|
+
3. Update the component with the new instance
|
|
449
|
+
|
|
450
|
+
This enables reactive updates when services change, making it easy to implement features like:
|
|
451
|
+
|
|
452
|
+
- **Cache invalidation** after mutations
|
|
453
|
+
- **Real-time updates** when data changes
|
|
454
|
+
- **Refresh on user action** (e.g., pull-to-refresh)
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
function UserList() {
|
|
458
|
+
const { data: users } = useService(UserService)
|
|
459
|
+
const invalidateUsers = useInvalidate(UserService)
|
|
460
|
+
|
|
461
|
+
const handleCreateUser = async () => {
|
|
462
|
+
await createUser(newUser)
|
|
463
|
+
invalidateUsers() // Automatically refreshes all components using UserService
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div>
|
|
468
|
+
{users.map((user) => (
|
|
469
|
+
<UserItem key={user.id} user={user} />
|
|
470
|
+
))}
|
|
471
|
+
<button onClick={handleCreateUser}>Add User</button>
|
|
472
|
+
</div>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Best Practices
|
|
478
|
+
|
|
479
|
+
### 1. Memoize Arguments
|
|
480
|
+
|
|
481
|
+
Always memoize arguments passed to hooks that accept them:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
// ✅ Good
|
|
485
|
+
const args = useMemo(() => ({ userId }), [userId])
|
|
486
|
+
const { data } = useService(UserToken, args)
|
|
487
|
+
|
|
488
|
+
// ❌ Bad - causes unnecessary re-fetches
|
|
489
|
+
const { data } = useService(UserToken, { userId })
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 2. Stable Container Reference
|
|
493
|
+
|
|
494
|
+
Keep your container reference stable:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
// ✅ Good
|
|
498
|
+
const container = useMemo(() => new Container(), [])
|
|
499
|
+
|
|
500
|
+
// ❌ Bad - creates new container on every render
|
|
501
|
+
const container = new Container()
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 3. Use Error Boundaries with Suspense
|
|
505
|
+
|
|
506
|
+
When using `useSuspenseService`, always wrap with an error boundary:
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
510
|
+
<Suspense fallback={<Loading />}>
|
|
511
|
+
<Component />
|
|
512
|
+
</Suspense>
|
|
513
|
+
</ErrorBoundary>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 4. Scope Isolation
|
|
517
|
+
|
|
518
|
+
Use `ScopeProvider` when you need isolated service instances:
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
// Each row gets its own service instance
|
|
522
|
+
{
|
|
523
|
+
rows.map((row) => (
|
|
524
|
+
<ScopeProvider key={row.id} scopeId={row.id}>
|
|
525
|
+
<TableRow />
|
|
526
|
+
</ScopeProvider>
|
|
527
|
+
))
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### 5. Optional Services for Feature Flags
|
|
532
|
+
|
|
533
|
+
Use `useOptionalService` for conditionally available services:
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
function FeatureComponent() {
|
|
537
|
+
const { data: feature, isNotFound } = useOptionalService(FeatureService)
|
|
538
|
+
|
|
539
|
+
if (isNotFound) return null // Feature not enabled
|
|
540
|
+
|
|
541
|
+
return <FeatureUI service={feature} />
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
122
545
|
## API Reference
|
|
123
546
|
|
|
124
547
|
### ContainerProvider
|
|
125
548
|
|
|
126
|
-
| Prop
|
|
127
|
-
|
|
128
|
-
| `container` | `Container` | The DI container instance |
|
|
129
|
-
| `children`
|
|
549
|
+
| Prop | Type | Description |
|
|
550
|
+
| ----------- | ----------- | -------------------------------------------- |
|
|
551
|
+
| `container` | `Container` | The DI container instance (should be stable) |
|
|
552
|
+
| `children` | `ReactNode` | Child components |
|
|
553
|
+
|
|
554
|
+
### ScopeProvider
|
|
555
|
+
|
|
556
|
+
| Prop | Type | Description | Default |
|
|
557
|
+
| ---------- | -------------------------- | -------------------------------------------------------------------------- | ----------- |
|
|
558
|
+
| `scopeId` | `string?` | Optional explicit scope ID. If not provided, a unique ID will be generated | `undefined` |
|
|
559
|
+
| `metadata` | `Record<string, unknown>?` | Optional metadata to attach to the request context | `undefined` |
|
|
560
|
+
| `priority` | `number?` | Priority for service resolution. Higher priority scopes take precedence | `100` |
|
|
561
|
+
| `children` | `ReactNode` | Child components | - |
|
|
130
562
|
|
|
131
563
|
### useContainer
|
|
132
564
|
|
|
133
565
|
```ts
|
|
134
|
-
function useContainer():
|
|
566
|
+
function useContainer(): IContainer
|
|
135
567
|
```
|
|
136
568
|
|
|
137
|
-
Returns the container from context. Throws if used outside of `ContainerProvider`.
|
|
569
|
+
Returns the container from context. Returns `ScopedContainer` if inside a `ScopeProvider`, otherwise returns the root `Container`. Throws if used outside of `ContainerProvider`.
|
|
570
|
+
|
|
571
|
+
### useRootContainer
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
function useRootContainer(): Container
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Returns the root `Container` regardless of whether you're inside a `ScopeProvider`. Throws if used outside of `ContainerProvider`.
|
|
138
578
|
|
|
139
579
|
### useService
|
|
140
580
|
|
|
141
581
|
```ts
|
|
142
|
-
function useService<T>(token: ClassType
|
|
582
|
+
function useService<T>(token: ClassType): UseServiceResult<InstanceType<T>>
|
|
583
|
+
function useService<T, S>(
|
|
584
|
+
token: InjectionToken<T, S>,
|
|
585
|
+
args: z.input<S>,
|
|
586
|
+
): UseServiceResult<T>
|
|
587
|
+
function useService<T>(token: InjectionToken<T, undefined>): UseServiceResult<T>
|
|
588
|
+
// ... other overloads
|
|
143
589
|
|
|
144
590
|
interface UseServiceResult<T> {
|
|
145
591
|
data: T | undefined
|
|
@@ -156,20 +602,155 @@ Fetches a service asynchronously and subscribes to invalidation events. When the
|
|
|
156
602
|
### useSuspenseService
|
|
157
603
|
|
|
158
604
|
```ts
|
|
159
|
-
function useSuspenseService<T>(token: ClassType
|
|
605
|
+
function useSuspenseService<T>(token: ClassType): InstanceType<T>
|
|
606
|
+
function useSuspenseService<T, S>(
|
|
607
|
+
token: InjectionToken<T, S>,
|
|
608
|
+
args: z.input<S>,
|
|
609
|
+
): T
|
|
610
|
+
function useSuspenseService<T>(token: InjectionToken<T, undefined>): T
|
|
611
|
+
// ... other overloads
|
|
160
612
|
```
|
|
161
613
|
|
|
162
614
|
Fetches a service using React Suspense. Throws a promise during loading and the resolved value on success. Subscribes to invalidation events and triggers re-render when the service is invalidated.
|
|
163
615
|
|
|
164
|
-
|
|
616
|
+
**Note**: Must be used within a `Suspense` boundary and an error boundary.
|
|
165
617
|
|
|
166
|
-
|
|
618
|
+
### useOptionalService
|
|
167
619
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
620
|
+
```ts
|
|
621
|
+
function useOptionalService<T>(
|
|
622
|
+
token: ClassType,
|
|
623
|
+
): UseOptionalServiceResult<InstanceType<T>>
|
|
624
|
+
function useOptionalService<T, S>(
|
|
625
|
+
token: InjectionToken<T, S>,
|
|
626
|
+
args: z.input<S>,
|
|
627
|
+
): UseOptionalServiceResult<T>
|
|
628
|
+
// ... other overloads
|
|
629
|
+
|
|
630
|
+
interface UseOptionalServiceResult<T> {
|
|
631
|
+
data: T | undefined
|
|
632
|
+
error: Error | undefined
|
|
633
|
+
isLoading: boolean
|
|
634
|
+
isSuccess: boolean
|
|
635
|
+
isNotFound: boolean
|
|
636
|
+
isError: boolean
|
|
637
|
+
refetch: () => void
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Loads a service that may not be registered. Returns `isNotFound: true` when the service doesn't exist instead of throwing an error.
|
|
642
|
+
|
|
643
|
+
### useInvalidate
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
function useInvalidate<T>(token: ClassType): () => Promise<void>
|
|
647
|
+
function useInvalidate<T, S>(
|
|
648
|
+
token: InjectionToken<T, S>,
|
|
649
|
+
args: S extends undefined ? never : unknown,
|
|
650
|
+
): () => Promise<void>
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Returns a function to invalidate a service by its token. When called, destroys the current service instance and triggers re-fetch in all components using `useService`/`useSuspenseService` for that token.
|
|
654
|
+
|
|
655
|
+
### useInvalidateInstance
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
function useInvalidateInstance(): (instance: unknown) => Promise<void>
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Returns a function to invalidate a service instance directly without knowing its token.
|
|
662
|
+
|
|
663
|
+
### useScope
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
function useScope(): string | null
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Returns the current scope ID. Returns `null` if not inside a `ScopeProvider`.
|
|
670
|
+
|
|
671
|
+
### useScopeOrThrow
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
function useScopeOrThrow(): string
|
|
675
|
+
```
|
|
171
676
|
|
|
172
|
-
|
|
677
|
+
Returns the current scope ID. Throws an error if not inside a `ScopeProvider`.
|
|
678
|
+
|
|
679
|
+
### useScopedContainer
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
function useScopedContainer(): ScopedContainer | null
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Returns the current `ScopedContainer`. Returns `null` if not inside a `ScopeProvider`.
|
|
686
|
+
|
|
687
|
+
### useScopedContainerOrThrow
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
function useScopedContainerOrThrow(): ScopedContainer
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Returns the current `ScopedContainer`. Throws an error if not inside a `ScopeProvider`.
|
|
694
|
+
|
|
695
|
+
### useScopeMetadata
|
|
696
|
+
|
|
697
|
+
```ts
|
|
698
|
+
function useScopeMetadata<T = unknown>(key: string): T | undefined
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Returns metadata from the current scope. Returns `undefined` if not inside a `ScopeProvider` or if the key doesn't exist.
|
|
702
|
+
|
|
703
|
+
## Troubleshooting
|
|
704
|
+
|
|
705
|
+
### "useContainer must be used within a ContainerProvider"
|
|
706
|
+
|
|
707
|
+
Make sure your component is wrapped with `ContainerProvider`:
|
|
708
|
+
|
|
709
|
+
```tsx
|
|
710
|
+
<ContainerProvider container={container}>
|
|
711
|
+
<YourComponent />
|
|
712
|
+
</ContainerProvider>
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Service re-fetches on every render
|
|
716
|
+
|
|
717
|
+
This usually happens when arguments are not memoized:
|
|
718
|
+
|
|
719
|
+
```tsx
|
|
720
|
+
// ❌ Bad - creates new object on every render
|
|
721
|
+
const { data } = useService(UserToken, { userId })
|
|
722
|
+
|
|
723
|
+
// ✅ Good - stable reference
|
|
724
|
+
const args = useMemo(() => ({ userId }), [userId])
|
|
725
|
+
const { data } = useService(UserToken, args)
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### useSuspenseService throws errors
|
|
729
|
+
|
|
730
|
+
Make sure you've wrapped your component with both `Suspense` and an error boundary:
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
<ErrorBoundary>
|
|
734
|
+
<Suspense fallback={<Loading />}>
|
|
735
|
+
<Component />
|
|
736
|
+
</Suspense>
|
|
737
|
+
</ErrorBoundary>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Services not invalidating
|
|
741
|
+
|
|
742
|
+
Ensure you're using the same token/args combination when invalidating:
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
// ✅ Good - same args
|
|
746
|
+
const args = useMemo(() => ({ userId }), [userId])
|
|
747
|
+
const { data } = useService(UserToken, args)
|
|
748
|
+
const invalidate = useInvalidate(UserToken, args)
|
|
749
|
+
|
|
750
|
+
// ❌ Bad - different args
|
|
751
|
+
const { data } = useService(UserToken, { userId: '1' })
|
|
752
|
+
const invalidate = useInvalidate(UserToken, { userId: '2' }) // Won't invalidate the first one
|
|
753
|
+
```
|
|
173
754
|
|
|
174
755
|
## License
|
|
175
756
|
|