@navios/di-react 0.1.1 → 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/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
- ## Usage
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
- ### Setting up the Provider
53
+ function MyComponent() {
54
+ const { data, isLoading, isError, error } = useService(MyService)
16
55
 
17
- Wrap your application with `ContainerProvider` and pass a `Container` instance:
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 injection tokens and arguments:
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 UserToken = InjectionToken.create<User, typeof UserSchema>(
85
- 'User',
86
- z.object({ userId: z.string() })
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
- const { data: user, isLoading } = useService(UserToken, { userId })
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 | Type | Description |
127
- |------|------|-------------|
128
- | `container` | `Container` | The DI container instance |
129
- | `children` | `ReactNode` | Child components |
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(): Container
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 | InjectionToken<T>, args?: unknown): UseServiceResult<T>
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 | InjectionToken<T>, args?: unknown): T
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
- ## Service Invalidation
616
+ **Note**: Must be used within a `Suspense` boundary and an error boundary.
165
617
 
166
- Both `useService` and `useSuspenseService` subscribe to the service's invalidation events via the DI container's event bus. When a service is invalidated (e.g., via `container.invalidate(service)`), the hooks will automatically:
618
+ ### useOptionalService
167
619
 
168
- 1. Clear the cached instance
169
- 2. Re-fetch the service
170
- 3. Update the component with the new instance
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
- This enables reactive updates when services change.
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