@adimm/x-injection-reactjs 1.0.4 → 1.0.5

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 (2) hide show
  1. package/README.md +1225 -194
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -12,41 +12,164 @@
12
12
  <a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank"><img src="https://badgen.net/npm/dm/@adimm/x-injection-reactjs"></a>
13
13
  </p>
14
14
 
15
- **Powerful dependency injection for React components using a modular architecture. Build scalable React applications with clean separation of concerns.** _(Inspired by Angular and NestJS IoC/DI)_
15
+ **Stop wrestling with React Context and prop drilling. Build scalable React apps with clean, testable business logic separated from UI.**
16
16
 
17
17
  ## Table of Contents
18
18
 
19
19
  - [Table of Contents](#table-of-contents)
20
- - [Overview](#overview)
20
+ - [What Problems Does This Solve?](#what-problems-does-this-solve)
21
+ - [1. Provider Hell](#1-provider-hell)
22
+ - [2. Prop Drilling](#2-prop-drilling)
23
+ - [3. Manual Dependency Wiring](#3-manual-dependency-wiring)
24
+ - [4. Business Logic Mixed with UI](#4-business-logic-mixed-with-ui)
25
+ - [How xInjection Solves This](#how-xinjection-solves-this)
21
26
  - [Installation](#installation)
22
27
  - [Quick Start](#quick-start)
23
- - [The Problem](#the-problem)
24
- - [Without xInjection](#without-xinjection)
25
- - [With xInjection](#with-xinjection)
26
- - [Core Concepts](#core-concepts)
27
- - [Component Modules](#component-modules)
28
- - [Services](#services)
29
- - [Dependency Injection](#dependency-injection)
30
- - [Custom Hooks](#custom-hooks)
31
- - [Examples](#examples)
32
- - [Zustand Integration](#zustand-integration)
33
- - [Parent-Child Provider Control](#parent-child-provider-control)
34
- - [Advanced Usage](#advanced-usage)
28
+ - [How It Works](#how-it-works)
29
+ - [1. Services: Your Business Logic](#1-services-your-business-logic)
30
+ - [2. Modules: Organizing Dependencies](#2-modules-organizing-dependencies)
31
+ - [3. Injecting Services into Components](#3-injecting-services-into-components)
32
+ - [Why Use the HoC Approach?](#why-use-the-hoc-approach)
33
+ - [1. Component Lifecycle Integration](#1-component-lifecycle-integration)
34
+ - [2. Isolated Dependency Trees](#2-isolated-dependency-trees)
35
+ - [3. Composition and Reusability](#3-composition-and-reusability)
36
+ - [4. Works with Standard React Patterns](#4-works-with-standard-react-patterns)
37
+ - [The Power of Component-Scoped Modules](#the-power-of-component-scoped-modules)
38
+ - [What Are Component-Scoped Modules?](#what-are-component-scoped-modules)
39
+ - [Pattern 1: Multiple Independent Instances](#pattern-1-multiple-independent-instances)
40
+ - [Pattern 2: Parent-Child Dependency Control](#pattern-2-parent-child-dependency-control)
41
+ - [Pattern 3: Global + Component-Scoped Mixing](#pattern-3-global--component-scoped-mixing)
42
+ - [All Ways to Use This Library](#all-ways-to-use-this-library)
43
+ - [Basic Service Injection](#basic-service-injection)
44
+ - [Injecting Multiple Services](#injecting-multiple-services)
45
+ - [Creating Custom Hooks with Dependencies](#creating-custom-hooks-with-dependencies)
46
+ - [Global vs Component-Scoped Services](#global-vs-component-scoped-services)
47
+ - [Parent Components Controlling Child Dependencies](#parent-components-controlling-child-dependencies)
35
48
  - [Module Imports and Exports](#module-imports-and-exports)
36
- - [Multiple Dependency Injection](#multiple-dependency-injection)
37
- - [Unit Testing](#unit-testing)
38
- - [Documentation](#documentation)
49
+ - [Real-World Examples](#real-world-examples)
50
+ - [Zustand Store Integration](#zustand-store-integration)
51
+ - [Complex Form with Shared State](#complex-form-with-shared-state)
52
+ - [Testing Your Code](#testing-your-code)
53
+ - [Mocking an Entire Module](#mocking-an-entire-module)
54
+ - [Mocking on-the-fly](#mocking-on-the-fly)
55
+ - [API Reference](#api-reference)
56
+ - [Core Functions](#core-functions)
57
+ - [`provideModuleToComponent(module, component)`](#providemoduletocomponentmodule-component)
58
+ - [`useInject(ServiceClass, options?)`](#useinjectserviceclass-options)
59
+ - [`useInjectMany(...services)`](#useinjectmanyservices)
60
+ - [`hookFactory({ use, inject })`](#hookfactory-use-inject-)
61
+ - [`@Injectable()`](#injectable)
62
+ - [`ProviderModule.blueprint(definition)`](#providermoduleblueprintdefinition)
63
+ - [`ProviderModule.create(definition)`](#providermodulecreatedefinition)
64
+ - [FAQ](#faq)
65
+ - [How do I add global services?](#how-do-i-add-global-services)
66
+ - [When should I use global modules vs component-scoped modules?](#when-should-i-use-global-modules-vs-component-scoped-modules)
67
+ - [Can I use this with Redux/MobX/Zustand?](#can-i-use-this-with-reduxmobxzustand)
68
+ - [How does this compare to React Context?](#how-does-this-compare-to-react-context)
69
+ - [Do I need to understand Dependency Injection to use this?](#do-i-need-to-understand-dependency-injection-to-use-this)
70
+ - [If I want Angular patterns, why not just use Angular?](#if-i-want-angular-patterns-why-not-just-use-angular)
71
+ - [Can I migrate gradually from an existing React app?](#can-i-migrate-gradually-from-an-existing-react-app)
72
+ - [When do I actually need `provideModuleToComponent`?](#when-do-i-actually-need-providemoduletocomponent)
73
+ - [What's the performance impact?](#whats-the-performance-impact)
74
+ - [Is this production-ready?](#is-this-production-ready)
75
+ - [Is "provider hell" really that bad?](#is-provider-hell-really-that-bad)
76
+ - [Why use classes for services instead of custom hooks?](#why-use-classes-for-services-instead-of-custom-hooks)
77
+ - [Links](#links)
39
78
  - [Contributing](#contributing)
40
79
  - [License](#license)
41
80
 
42
- ## Overview
81
+ ## What Problems Does This Solve?
43
82
 
44
- xInjection for React brings dependency injection to your React components, enabling:
83
+ If you've built React apps, you've probably encountered these pain points:
45
84
 
46
- - **Service-based architecture**: Separate business logic from UI components
47
- - **Modular design**: Create reusable, testable component modules
48
- - **State management integration**: Works seamlessly with Zustand, Redux, or any state library
49
- - **Parent-child provider control**: Parent components can control child component dependencies
85
+ ### 1. Provider Hell
86
+
87
+ Your `App.tsx` becomes a nightmare of nested providers:
88
+
89
+ ```tsx
90
+ <AuthProvider>
91
+ <ThemeProvider>
92
+ <ApiProvider>
93
+ <ToastProvider>
94
+ <UserProvider>
95
+ <App />
96
+ </UserProvider>
97
+ </ToastProvider>
98
+ </ApiProvider>
99
+ </ThemeProvider>
100
+ </AuthProvider>
101
+ ```
102
+
103
+ ### 2. Prop Drilling
104
+
105
+ You pass props through 5 levels of components just to reach the one that needs them:
106
+
107
+ ```tsx
108
+ <Dashboard user={user}>
109
+ <Sidebar user={user}>
110
+ <UserMenu user={user}>
111
+ <UserAvatar user={user} /> {/* Finally! */}
112
+ </UserMenu>
113
+ </Sidebar>
114
+ </Dashboard>
115
+ ```
116
+
117
+ ### 3. Manual Dependency Wiring
118
+
119
+ When a service needs dependencies, you manually create them in the right order:
120
+
121
+ ```tsx
122
+ function UserProfile() {
123
+ // Must create ALL dependencies manually in correct order
124
+ const toastService = new ToastService();
125
+ const apiService = new ApiService();
126
+ const authService = new AuthService(apiService);
127
+ const userProfileService = new UserProfileService(apiService, authService, toastService);
128
+
129
+ // If AuthService adds a new dependency tomorrow, THIS BREAKS!
130
+ return <div>{userProfileService.displayName}</div>;
131
+ }
132
+ ```
133
+
134
+ ### 4. Business Logic Mixed with UI
135
+
136
+ Your components become bloated with API calls, state management, and validation:
137
+
138
+ ```tsx
139
+ function UserDashboard() {
140
+ const [user, setUser] = useState(null);
141
+ const [loading, setLoading] = useState(false);
142
+
143
+ useEffect(() => {
144
+ setLoading(true);
145
+ fetch('/api/user')
146
+ .then((res) => res.json())
147
+ .then((data) => {
148
+ setUser(data);
149
+ setLoading(false);
150
+ });
151
+ }, []);
152
+
153
+ // 50 more lines of business logic...
154
+
155
+ return <div>{/* Your actual UI */}</div>;
156
+ }
157
+ ```
158
+
159
+ ## How xInjection Solves This
160
+
161
+ xInjection brings **Inversion of Control (IoC)** and **Dependency Injection (DI)** to React—concepts from Angular and NestJS that solve these exact problems. Don't worry if those terms sound fancy; the idea is simple:
162
+
163
+ **Instead of components creating and managing their own dependencies, they just ask for what they need, and xInjection provides it.**
164
+
165
+ - **No Provider Hell** - One module replaces nested providers
166
+ - **No Prop Drilling** - Services are injected directly where needed
167
+ - **Automatic Dependency Resolution** - Dependencies are wired automatically
168
+ - **Clean Separation** - Business logic lives in services, UI stays in components
169
+ - **Fully Testable** - Mock services easily for testing
170
+ - **Type-Safe** - Full TypeScript support
171
+
172
+ ---
50
173
 
51
174
  This is the official [ReactJS](https://react.dev/) implementation of [xInjection](https://github.com/AdiMarianMutu/x-injection).
52
175
 
@@ -56,6 +179,22 @@ This is the official [ReactJS](https://react.dev/) implementation of [xInjection
56
179
  npm i @adimm/x-injection-reactjs reflect-metadata
57
180
  ```
58
181
 
182
+ [!IMPORTANT]
183
+
184
+ > Import `reflect-metadata` at the very top of your app entry point:
185
+
186
+ ```tsx
187
+ // main.tsx or index.tsx
188
+
189
+ import 'reflect-metadata';
190
+
191
+ import { createRoot } from 'react-dom/client';
192
+
193
+ import App from './App';
194
+
195
+ createRoot(document.getElementById('root')!).render(<App />);
196
+ ```
197
+
59
198
  **TypeScript Configuration**
60
199
 
61
200
  Add to your `tsconfig.json`:
@@ -69,171 +208,649 @@ Add to your `tsconfig.json`:
69
208
  }
70
209
  ```
71
210
 
211
+ > **📚 Advanced Concepts**
212
+ >
213
+ > This documentation covers React-specific usage patterns. For advanced features like **lifecycle hooks** (`onReady`, `onDispose`), **injection scopes** (Singleton, Transient, Request), **middlewares**, **events**, and **dynamic module updates**, refer to the [base xInjection library documentation](https://github.com/AdiMarianMutu/x-injection).
214
+ >
215
+ > The base library provides the core IoC/DI engine that powers this React integration.
216
+
72
217
  ## Quick Start
73
218
 
219
+ Here's a complete example showing both global and component-scoped services:
220
+
74
221
  ```tsx
75
- import { Injectable, provideModuleToComponent, ProviderModule, useInject } from '@adimm/x-injection-reactjs';
222
+ // main.tsx - Your app entry point
223
+
224
+ import 'reflect-metadata';
76
225
 
77
- // 1. Define a service
226
+ import { Injectable, ProviderModule } from '@adimm/x-injection';
227
+ import { createRoot } from 'react-dom/client';
228
+
229
+ import App from './App';
230
+
231
+ // Global services (singletons)
78
232
  @Injectable()
79
- class UserService {
80
- firstName = 'John';
81
- lastName = 'Doe';
233
+ class ApiService {
234
+ get(url: string) {
235
+ return fetch(url).then((r) => r.json());
236
+ }
82
237
  }
83
238
 
84
- // 2. Create a module blueprint
239
+ @Injectable()
240
+ class AuthService {
241
+ constructor(private readonly apiService: ApiService) {}
242
+
243
+ isLoggedIn = false;
244
+
245
+ login() {
246
+ this.isLoggedIn = true;
247
+ }
248
+ }
249
+
250
+ // Create global module - automatically imported into built-in AppModule
251
+ ProviderModule.blueprint({
252
+ id: 'AppBootstrapModule',
253
+ isGlobal: true,
254
+ providers: [ApiService, AuthService],
255
+ exports: [ApiService, AuthService], // Exported services available everywhere
256
+ });
257
+
258
+ // Now render your app
259
+ createRoot(document.getElementById('root')!).render(<App />);
260
+ ```
261
+
262
+ ```tsx
263
+ // UserDashboard.tsx - A component with its own service
264
+
265
+ import { Injectable, ProviderModule } from '@adimm/x-injection';
266
+ import { provideModuleToComponent, useInject } from '@adimm/x-injection-reactjs';
267
+
268
+ // Component-scoped service
269
+ @Injectable()
270
+ class UserDashboardService {
271
+ constructor(private readonly apiService: ApiService) {} // Gets global ApiService
272
+
273
+ async loadUser() {
274
+ return this.apiService.get('/user');
275
+ }
276
+ }
277
+
278
+ // Component-scoped module
85
279
  const UserDashboardModuleBp = ProviderModule.blueprint({
86
280
  id: 'UserDashboardModule',
87
- providers: [UserService],
281
+ providers: [UserDashboardService],
88
282
  });
89
283
 
90
- // 3. Create a component with dependency injection
91
- const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
92
- const userService = useInject(UserService);
284
+ // Component with injected service
285
+ export const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
286
+ const dashboardService = useInject(UserDashboardService);
287
+ const authService = useInject(AuthService); // Can also inject global services
93
288
 
94
289
  return (
95
- <h1>
96
- Hello {userService.firstName} {userService.lastName}!
97
- </h1>
290
+ <div>
291
+ <h1>Dashboard</h1>
292
+ <p>Logged in: {authService.isLoggedIn ? 'Yes' : 'No'}</p>
293
+ </div>
98
294
  );
99
295
  });
100
296
  ```
101
297
 
102
- ## The Problem
298
+ ```tsx
299
+ // App.tsx
300
+
301
+ import { UserDashboard } from './UserDashboard';
302
+
303
+ export default function App() {
304
+ return (
305
+ <div>
306
+ <UserDashboard />
307
+ <UserDashboard /> {/* Each gets its own UserDashboardService */}
308
+ </div>
309
+ );
310
+ }
311
+ ```
312
+
313
+ [!TIP]
103
314
 
104
- React apps often suffer from **provider hell**, **prop drilling**, and **manual dependency wiring**:
315
+ > **Key points:**
316
+ >
317
+ > - Global services (`ApiService`, `AuthService`): Defined in a global blueprint, automatically imported into the built-in `AppModule`
318
+ > - Component-scoped services (`UserDashboardService`): Fresh instance per `<UserDashboard />`
319
+ > - Component-scoped services can inject global services automatically
105
320
 
106
- ### Without xInjection
321
+ ## How It Works
322
+
323
+ Let's break down the three main concepts you'll use:
324
+
325
+ ### 1. Services: Your Business Logic
326
+
327
+ A **service** is just a class that contains your business logic. Think of it as extracting all the "smart stuff" from your component into a reusable, testable class.
107
328
 
108
329
  ```tsx
109
- // Problem 1: Provider Hell
110
- <AuthProvider>
111
- <ApiProvider>
112
- <ToastProvider>
113
- <App />
114
- </ToastProvider>
115
- </ApiProvider>
116
- </AuthProvider>;
117
-
118
- // Problem 2: Manual Dependency Wiring
119
- function UserProfile() {
120
- // Must manually create ALL dependencies in correct order
121
- const toast = new ToastService();
122
- const api = new ApiService();
123
- const auth = new AuthService(api);
124
- const userProfile = new UserProfileService(api, auth, toast);
125
-
126
- // If AuthService adds a dependency, ALL consumers break!
127
- return <div>{userProfile.displayName}</div>;
330
+ @Injectable()
331
+ class TodoService {
332
+ private todos: Todo[] = [];
333
+
334
+ addTodo(text: string) {
335
+ this.todos.push({ id: Date.now(), text, completed: false });
336
+ }
337
+
338
+ getTodos() {
339
+ return this.todos;
340
+ }
341
+
342
+ toggleTodo(id: number) {
343
+ const todo = this.todos.find((t) => t.id === id);
344
+ if (todo) todo.completed = !todo.completed;
345
+ }
128
346
  }
129
347
  ```
130
348
 
131
- ### With xInjection
349
+ The `@Injectable()` decorator marks this class as something that can be injected (either into components or other services/modules).
350
+
351
+ **Services can depend on other services:**
132
352
 
133
353
  ```tsx
134
- // 1. Define global services (shared across all components) - Usually in your app entrypoint/bootstrap file.
135
- const AppModuleBp = ProviderModule.blueprint({
136
- id: 'AppModule',
137
- isGlobal: true, // Available everywhere, only created once
138
- providers: [ToastService, ApiService, AuthService],
139
- });
354
+ @Injectable()
355
+ class UserProfileService {
356
+ // Dependencies are automatically injected via constructor
357
+ constructor(
358
+ private readonly apiService: ApiService,
359
+ private readonly authService: AuthService,
360
+ private readonly toastService: ToastService
361
+ ) {}
362
+
363
+ async loadProfile() {
364
+ try {
365
+ const userId = this.authService.getCurrentUserId();
366
+ const profile = await this.apiService.get(`/users/${userId}`);
367
+ return profile;
368
+ } catch (error) {
369
+ this.toastService.error('Failed to load profile');
370
+ throw error;
371
+ }
372
+ }
373
+ }
374
+ ```
375
+
376
+ Notice how `UserProfileService` asks for its dependencies in the constructor? xInjection will automatically provide them.
377
+
378
+ **Alternative: Property Injection**
379
+
380
+ You can also use the `@Inject` decorator from the base library for property injection:
381
+
382
+ ```tsx
383
+ import { Inject, Injectable } from '@adimm/x-injection';
384
+
385
+ @Injectable()
386
+ class UserProfileService {
387
+ @Inject(ApiService)
388
+ private readonly apiService!: ApiService;
389
+
390
+ @Inject(AuthService)
391
+ private readonly authService!: AuthService;
392
+
393
+ async loadProfile() {
394
+ const userId = this.authService.getCurrentUserId();
395
+ return this.apiService.get(`/users/${userId}`);
396
+ }
397
+ }
398
+ ```
140
399
 
141
- // 2. Define component-specific services - Per component
142
- const UserProfileModuleBp = ProviderModule.blueprint({
143
- id: 'UserProfileModule',
144
- providers: [UserProfileService], // Automatically gets ApiService, AuthService, ToastService
400
+ Both approaches work! Constructor injection is generally preferred for better type safety and easier testing.
401
+
402
+ ### 2. Modules: Organizing Dependencies
403
+
404
+ A **module** is a container that tells xInjection which services are available. Think of it as a "package" of services.
405
+
406
+ **Modules come in two flavors:**
407
+
408
+ ```tsx
409
+ // Global module: Created once, shared everywhere
410
+ ProviderModule.blueprint({
411
+ id: 'AppBootstrapModule',
412
+ isGlobal: true,
413
+ providers: [ApiService, AuthService, ToastService],
414
+ exports: [ApiService, AuthService, ToastService], // Only exported services become globally available
145
415
  });
146
416
 
147
- const UserProfile = provideModuleToComponent(UserProfileModuleBp, () => {
148
- const userProfile = useInject(UserProfileService);
149
- // IoC automatically injects: ToastService → ApiService → AuthService → UserProfileService
150
- return <div>{userProfile.displayName}</div>;
417
+ // Component-scoped module: Each component instance gets its own
418
+ const TodoListModuleBp = ProviderModule.blueprint({
419
+ id: 'TodoListModule',
420
+ providers: [TodoService], // Gets a fresh TodoService per component
151
421
  });
152
422
  ```
153
423
 
154
- **What You Get:**
424
+ [!IMPORTANT]
155
425
 
156
- - **No Provider Hell** - One module replaces nested providers
157
- - **Auto Dependency Resolution** - IoC wires everything automatically
158
- - **Easy Refactoring** - Add/remove dependencies without breaking consumers
159
- - **Clean Separation** - Business logic in services, UI in components
160
- - **Fully Testable** - Mock modules or individual services
161
- - **Type-Safe** - Full TypeScript support
426
+ > When using `isGlobal: true`, only services listed in the `exports` array become globally available. Non-exported providers remain private to the module.
162
427
 
163
- ## Core Concepts
428
+ [!CAUTION]
164
429
 
165
- ### Component Modules
430
+ > **Global modules cannot be used with `provideModuleToComponent`**. Attempting to provide a global module to a component will throw an `InjectionProviderModuleError`. Global services are accessed directly via `useInject` without the HoC.
166
431
 
167
- Create a module blueprint that defines your component's dependencies:
432
+ **`blueprint()` vs `create()`:**
168
433
 
169
- ```ts
170
- // user-dashboard.module.ts
171
- export const UserDashboardModuleBp = ProviderModule.blueprint({
434
+ - **`blueprint()`**: Creates a **deferred module** (template) instantiated when needed. Use for all React modules—both global and component-scoped. [Learn more](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#blueprints).
435
+ - **`create()`**: Immediately initializes a module. Rarely needed in React.
436
+
437
+ **Modules can import other modules to compose functionality:**
438
+
439
+ ```tsx
440
+ // Shared utilities module
441
+ const UtilsModuleBp = ProviderModule.blueprint({
442
+ id: 'UtilsModule',
443
+ providers: [LoggerService, DateService],
444
+ exports: [LoggerService, DateService],
445
+ });
446
+
447
+ // Feature module imports utilities
448
+ const UserDashboardModuleBp = ProviderModule.blueprint({
172
449
  id: 'UserDashboardModule',
173
- providers: [UserService],
174
- exports: [UserService],
450
+ imports: [UtilsModuleBp], // Reuse LoggerService and DateService
451
+ providers: [UserProfileService], // Add UserProfileService
175
452
  });
176
453
  ```
177
454
 
178
- **Blueprint vs Module:** Use a **blueprint** for reusable components (multiple instances), use a raw **module** for singleton components (single instance).
455
+ See [Module Imports and Exports](#module-imports-and-exports) for more advanced patterns.
179
456
 
180
- ### Services
457
+ [!CAUTION]
181
458
 
182
- Define services using the `@Injectable()` decorator:
459
+ > **Never import `AppModule`** into other modules. `AppModule` is the built-in global container and importing it will throw an error. Use global blueprints with `isGlobal: true` instead, which are automatically imported into `AppModule`.
183
460
 
184
- ```ts
185
- // user-dashboard.service.ts
186
- @Injectable()
187
- export class UserDashboardService {
188
- firstName: string;
189
- lastName: string;
461
+ ### 3. Injecting Services into Components
462
+
463
+ Use the `provideModuleToComponent` Higher-Order Component (HoC) to give your component access to services:
464
+
465
+ ```tsx
466
+ const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
467
+ // Inject the service you need
468
+ const userProfileService = useInject(UserProfileService);
469
+
470
+ return <div>{userProfileService.displayName}</div>;
471
+ });
472
+ ```
473
+
474
+ The HoC does two things:
475
+
476
+ 1. Creates an instance of your module (and all its services)
477
+ 2. Makes those services available via the `useInject` hook
478
+
479
+ **You can also inject multiple services at once:**
480
+
481
+ ```tsx
482
+ const MyComponent = provideModuleToComponent(MyModuleBp, () => {
483
+ const [userService, apiService] = useInjectMany(UserService, ApiService);
484
+
485
+ // Use your services...
486
+ });
487
+ ```
488
+
489
+ ## Why Use the HoC Approach?
490
+
491
+ You might wonder: "Why wrap my component with `provideModuleToComponent` instead of just using `useInject` directly everywhere?"
492
+
493
+ **Short answer:** You don't always need it! If you only use global services, you can just call `useInject` anywhere. But for **component-scoped modules** (where each component instance needs its own services), you need `provideModuleToComponent`.
494
+
495
+ The Higher-Order Component (HoC) pattern provides several key benefits:
496
+
497
+ ### 1. Component Lifecycle Integration
498
+
499
+ The HoC automatically manages the lifecycle of the module and its services. When the component mounts, the module is created along with its services. When it unmounts, the module is disposed (the `onDispose` hook runs), cleaning up only the services defined in that module.
500
+
501
+ **With HoC** The component's module and its own services are created/destroyed with the component. Imported services from other modules (including global services) remain unaffected.
502
+
503
+ ### 2. Isolated Dependency Trees
504
+
505
+ Each component wrapped with `provideModuleToComponent` gets its own isolated dependency container. This means:
506
+
507
+ - Two instances of the same component can have different service instances
508
+ - Parent components can control child component dependencies
509
+ - Services are scoped to the component tree
510
+
511
+ ### 3. Composition and Reusability
512
+
513
+ The HoC pattern works seamlessly with React's component composition model:
514
+
515
+ ```tsx
516
+ // Reusable component with its own dependencies
517
+ const TodoList = provideModuleToComponent(TodoListModuleBp, () => {
518
+ const todoService = useInject(TodoService);
519
+ // ...
520
+ });
190
521
 
191
- getFullName() {
192
- return `${this.firstName} ${this.lastName}`;
522
+ // Use it multiple times, each with isolated state
523
+ function App() {
524
+ return (
525
+ <>
526
+ <TodoList /> {/* Gets its own TodoService */}
527
+ <TodoList /> {/* Gets a different TodoService */}
528
+ </>
529
+ );
530
+ }
531
+ ```
532
+
533
+ ### 4. Works with Standard React Patterns
534
+
535
+ The HoC approach works with `React.memo`, context, hooks, and all other React features:
536
+
537
+ ```tsx
538
+ // Works with React.memo
539
+ const MemoizedComponent = React.memo(
540
+ provideModuleToComponent(MyModuleBp, () => {
541
+ // ...
542
+ })
543
+ );
544
+ ```
545
+
546
+ ## The Power of Component-Scoped Modules
547
+
548
+ One of the most powerful features of xInjection is **component-scoped modules**. This is something you can't easily achieve with React Context alone.
549
+
550
+ ### What Are Component-Scoped Modules?
551
+
552
+ When you use `provideModuleToComponent`, each instance of your component gets its **own copy** of the module and all its services. This enables powerful patterns:
553
+
554
+ ### Pattern 1: Multiple Independent Instances
555
+
556
+ ```tsx
557
+ @Injectable()
558
+ class CounterService {
559
+ count = 0;
560
+ increment() {
561
+ this.count++;
193
562
  }
194
563
  }
564
+
565
+ const CounterModuleBp = ProviderModule.blueprint({
566
+ id: 'CounterModule',
567
+ providers: [CounterService],
568
+ });
569
+
570
+ const Counter = provideModuleToComponent(CounterModuleBp, () => {
571
+ const counterService = useInject(CounterService);
572
+ return (
573
+ <div>
574
+ <p>Count: {counterService.count}</p>
575
+ <button onClick={() => counterService.increment()}>+</button>
576
+ </div>
577
+ );
578
+ });
579
+
580
+ function App() {
581
+ return (
582
+ <div>
583
+ <Counter /> {/* Count: 0 */}
584
+ <Counter /> {/* Count: 0 (separate instance!) */}
585
+ </div>
586
+ );
587
+ }
195
588
  ```
196
589
 
197
- ### Dependency Injection
590
+ Each `<Counter />` has its own `CounterService`, so they don't interfere with each other.
198
591
 
199
- Use `useInject` to access services in your components:
592
+ ### Pattern 2: Parent-Child Dependency Control
593
+
594
+ Parent components can "inject" specific service instances into their children:
200
595
 
201
596
  ```tsx
597
+ const ParentModuleBp = ProviderModule.blueprint({
598
+ id: 'ParentModule',
599
+ providers: [SharedService, ParentService],
600
+ });
601
+
602
+ const ChildModuleBp = ProviderModule.blueprint({
603
+ id: 'ChildModule',
604
+ providers: [ChildService],
605
+ });
606
+
607
+ const Child = provideModuleToComponent(ChildModuleBp, () => {
608
+ const sharedService = useInject(SharedService);
609
+ return <div>{sharedService.data}</div>;
610
+ });
611
+
612
+ const Parent = provideModuleToComponent(ParentModuleBp, () => {
613
+ const sharedService = useInject(SharedService);
614
+
615
+ // Pass the parent's SharedService instance to the child
616
+ return <Child inject={[{ provide: SharedService, useValue: sharedService }]} />;
617
+ });
618
+ ```
619
+
620
+ This enables complex patterns like form components sharing validation services, or composite UI components coordinating state.
621
+
622
+ ### Pattern 3: Global + Component-Scoped Mixing
623
+
624
+ Combine global services (singletons) with component-scoped services:
625
+
626
+ ```tsx
627
+ // Global: Shared across the entire app
628
+ ProviderModule.blueprint({
629
+ id: 'AppBootstrapModule',
630
+ isGlobal: true,
631
+ providers: [ApiService, AuthService],
632
+ exports: [ApiService, AuthService], // Only exported services are globally available
633
+ });
634
+
635
+ // Component-scoped: Fresh instance per component
636
+ const UserDashboardModuleBp = ProviderModule.blueprint({
637
+ id: 'UserDashboardModule',
638
+ providers: [UserDashboardService], // Gets global ApiService + AuthService
639
+ });
640
+
202
641
  const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
203
- const userService = useInject(UserService);
642
+ const dashboardService = useInject(UserDashboardService);
643
+ // UserDashboardService automatically receives the global ApiService and AuthService
644
+ });
645
+ ```
646
+
647
+ This pattern is powerful: global services (API, auth) are singletons, while component-specific services (UserDashboardService) are created per instance.
648
+
649
+ ## All Ways to Use This Library
650
+
651
+ This section covers every way you can use xInjection in your React app.
652
+
653
+ ### Basic Service Injection
654
+
655
+ The most common pattern: create a service, add it to a module, inject it into a component.
656
+
657
+ ```tsx
658
+ @Injectable()
659
+ class GreetingService {
660
+ getGreeting(name: string) {
661
+ return `Hello, ${name}!`;
662
+ }
663
+ }
664
+
665
+ const GreetingModuleBp = ProviderModule.blueprint({
666
+ id: 'GreetingModule',
667
+ providers: [GreetingService],
668
+ });
204
669
 
205
- return <div>{userService.getFullName()}</div>;
670
+ const Greeting = provideModuleToComponent(GreetingModuleBp, ({ name }: { name: string }) => {
671
+ const greetingService = useInject(GreetingService);
672
+ return <h1>{greetingService.getGreeting(name)}</h1>;
206
673
  });
207
674
  ```
208
675
 
209
- ### Custom Hooks
676
+ ### Injecting Multiple Services
210
677
 
211
- Create reusable hooks with dependency injection using `hookFactory`:
678
+ Use `useInjectMany` to inject multiple services at once:
212
679
 
213
- ```ts
214
- const useUserFullName = hookFactory({
215
- use: ({ firstName, lastName, deps: [userService] }) => {
216
- userService.firstName = firstName;
217
- userService.lastName = lastName;
218
- return userService.getFullName();
680
+ ```tsx
681
+ const MyComponent = provideModuleToComponent(MyModuleBp, () => {
682
+ const [userService, apiService, toastService] = useInjectMany(UserService, ApiService, ToastService);
683
+
684
+ // Use all three services...
685
+ });
686
+ ```
687
+
688
+ **Optional dependencies:**
689
+
690
+ ```tsx
691
+ const [requiredService, optionalService] = useInjectMany(RequiredService, {
692
+ provider: OptionalService,
693
+ isOptional: true,
694
+ });
695
+
696
+ // optionalService will be undefined if not provided
697
+ ```
698
+
699
+ ### Creating Custom Hooks with Dependencies
700
+
701
+ The `hookFactory` function lets you create reusable custom hooks that automatically receive injected dependencies:
702
+
703
+ ```tsx
704
+ // Define a custom hook with dependencies
705
+ const useUserProfile = hookFactory({
706
+ use: ({ userId, deps: [apiService, authService] }) => {
707
+ const [profile, setProfile] = useState(null);
708
+
709
+ useEffect(() => {
710
+ apiService.get(`/users/${userId}`).then(setProfile);
711
+ }, [userId]);
712
+
713
+ return profile;
714
+ },
715
+ inject: [ApiService, AuthService],
716
+ });
717
+
718
+ // Use it in any component
719
+ const UserProfile = provideModuleToComponent(UserModuleBp, ({ userId }: { userId: number }) => {
720
+ const profile = useUserProfile({ userId });
721
+ return <div>{profile?.name}</div>;
722
+ });
723
+ ```
724
+
725
+ **Type-safe hooks with `HookWithDeps`:**
726
+
727
+ Use the `HookWithDeps<P, D>` type utility for full TypeScript support:
728
+
729
+ ```tsx
730
+ import type { HookWithDeps } from '@adimm/x-injection-reactjs';
731
+
732
+ // Hook with no parameters - use void as first generic
733
+ const useTestHook = hookFactory({
734
+ use: ({ deps: [testService] }: HookWithDeps<void, [TestService]>) => {
735
+ return testService.value;
219
736
  },
220
- inject: [UserService],
737
+ inject: [TestService],
738
+ });
739
+
740
+ // Hook with parameters - specify parameter type as first generic
741
+ const useUserData = hookFactory({
742
+ use: ({ userId, deps: [apiService] }: HookWithDeps<{ userId: number }, [ApiService]>) => {
743
+ const [data, setData] = useState(null);
744
+ useEffect(() => {
745
+ apiService.get(`/users/${userId}`).then(setData);
746
+ }, [userId]);
747
+ return data;
748
+ },
749
+ inject: [ApiService],
750
+ });
751
+
752
+ // Usage:
753
+ useTestHook(); // No parameters
754
+ useUserData({ userId: 123 }); // With parameters
755
+ ```
756
+
757
+ **`HookWithDeps<P, D>` generics:**
758
+
759
+ - **`P`**: Hook parameter type (use `void` if no parameters, or `{ param1: type, ... }` for parameters)
760
+ - **`D`**: Tuple type matching your `inject` array (e.g., `[ApiService, AuthService]`)
761
+
762
+ [!TIP]
763
+
764
+ > **Why use hookFactory?**
765
+ >
766
+ > - Dependencies are automatically injected
767
+ > - Hooks are reusable across components
768
+ > - Type-safe with TypeScript
769
+ > - Easier to test (mock dependencies)
770
+
771
+ ### Global vs Component-Scoped Services
772
+
773
+ **When to use which?**
774
+
775
+ - **Global (`blueprint` + `isGlobal: true` + `exports`)**: API clients, auth state, routing, theme, toast notifications
776
+
777
+ - Only services in the `exports` array become globally available
778
+ - **Cannot use `provideModuleToComponent`** - will throw an error
779
+ - Just call `useInject` directly anywhere
780
+
781
+ - **Component-scoped (`blueprint`)**: Form state, component-specific business logic, UI state
782
+ - MUST use `provideModuleToComponent`
783
+ - Each component instance gets its own module
784
+
785
+ See [Pattern 1](#pattern-1-multiple-independent-instances) and [Pattern 3](#pattern-3-global--component-scoped-mixing) for detailed examples.
786
+
787
+ ### Parent Components Controlling Child Dependencies
788
+
789
+ The `inject` prop allows parent components to override child component dependencies. See [Pattern 2](#pattern-2-parent-child-dependency-control) for a basic example and the [Complex Form example](#complex-form-with-shared-state) for a real-world use case.
790
+
791
+ ### Module Imports and Exports
792
+
793
+ Modules can import other modules to reuse their services:
794
+
795
+ ```tsx
796
+ // Base module with shared services
797
+ const CoreModuleBp = ProviderModule.blueprint({
798
+ id: 'CoreModule',
799
+ providers: [ApiService, LoggerService],
800
+ exports: [ApiService, LoggerService], // Make these available to importers
801
+ });
802
+
803
+ // Feature module imports CoreModule
804
+ const UserModuleBp = ProviderModule.blueprint({
805
+ id: 'UserModule',
806
+ imports: [CoreModuleBp], // Get ApiService and LoggerService
807
+ providers: [UserService], // Add UserService (can use ApiService and LoggerService)
808
+ });
809
+
810
+ // Another feature module
811
+ const ProductModuleBp = ProviderModule.blueprint({
812
+ id: 'ProductModule',
813
+ imports: [CoreModuleBp], // Also gets ApiService and LoggerService
814
+ providers: [ProductService],
815
+ });
816
+ ```
817
+
818
+ **Re-exporting modules:**
819
+
820
+ ```tsx
821
+ const SharedModuleBp = ProviderModule.blueprint({
822
+ id: 'SharedModule',
823
+ imports: [CoreModuleBp, UtilsModuleBp],
824
+ exports: [
825
+ CoreModuleBp, // Re-export CoreModule
826
+ UtilsModuleBp, // Re-export UtilsModule
827
+ ],
221
828
  });
222
829
 
223
- // Use in any component
224
- const fullName = useUserFullName({ firstName: 'John', lastName: 'Doe' });
830
+ // Other modules can import SharedModule to get everything
831
+ const FeatureModuleBp = ProviderModule.blueprint({
832
+ id: 'FeatureModule',
833
+ imports: [SharedModuleBp], // Gets CoreModule + UtilsModule
834
+ });
225
835
  ```
226
836
 
227
- ## Examples
837
+ ## Real-World Examples
228
838
 
229
- ### Zustand Integration
839
+ ### Zustand Store Integration
230
840
 
231
- This example shows how to integrate Zustand store within a service, allowing the service to manipulate the store while components only subscribe to state changes.
841
+ xInjection works beautifully with Zustand. The pattern is simple: **encapsulate the Zustand store inside a service**. This keeps your business logic in services while using Zustand for reactive state.
842
+
843
+ **Why this pattern?**
844
+
845
+ - Business logic stays in services (testable, reusable)
846
+ - Components subscribe to state reactively (optimal re-renders)
847
+ - Store is scoped to the component (no global state pollution)
848
+ - Type-safe and easy to test
232
849
 
233
850
  ```ts
234
851
  // counter.service.ts
235
852
 
236
- import { Injectable } from '@adimm/x-injection-reactjs';
853
+ import { Injectable } from '@adimm/x-injection';
237
854
  import { create } from 'zustand';
238
855
 
239
856
  interface CounterStore {
@@ -293,7 +910,7 @@ export class CounterService {
293
910
  ```ts
294
911
  // counter.module.ts
295
912
 
296
- import { ProviderModule } from '@adimm/x-injection-reactjs';
913
+ import { ProviderModule } from '@adimm/x-injection';
297
914
 
298
915
  import { CounterService } from './counter.service';
299
916
 
@@ -342,138 +959,552 @@ export default Counter;
342
959
  - **Reusability**: Services with stores can be shared across components via dependency injection
343
960
  - **Type safety**: Full TypeScript support throughout
344
961
 
345
- ### Parent-Child Provider Control
962
+ ### Complex Form with Shared State
346
963
 
347
- Parent components can control child component dependencies using the `inject` prop:
964
+ This example demonstrates a powerful pattern: a parent form component controlling the state of multiple child input components.
348
965
 
349
- ```ts
350
- // Child module and service
351
- const ChildModuleBp = ProviderModule.blueprint({
352
- id: 'ChildModule',
353
- providers: [ChildService],
354
- exports: [ChildService],
355
- });
966
+ ```tsx
967
+ import { Inject, Injectable, InjectionScope } from '@adimm/x-injection';
356
968
 
357
- // Parent module
358
- const ParentModuleBp = ProviderModule.blueprint({
359
- id: 'ParentModule',
360
- providers: [ParentService, ChildService],
361
- exports: [ParentService, ChildService],
362
- });
969
+ // 1. Input service - manages a single input's state
970
+ @Injectable()
971
+ class InputService {
972
+ value = '';
973
+ error = '';
363
974
 
364
- // Parent component controls child's service
365
- const ParentComponent = provideModuleToComponent(ParentModuleBp, () => {
366
- const childService = useInject(ChildService);
975
+ setValue(value: string) {
976
+ this.value = value;
977
+ this.validate();
978
+ }
367
979
 
368
- // Override child's ChildService with parent's instance
369
- return <ChildComponent inject={[{ provide: ChildService, useValue: childService }]} />;
370
- });
371
- ```
980
+ validate() {
981
+ if (!this.value) {
982
+ this.error = 'Required';
983
+ } else if (this.value.length < 3) {
984
+ this.error = 'Too short';
985
+ } else {
986
+ this.error = '';
987
+ }
988
+ return !this.error;
989
+ }
990
+ }
372
991
 
373
- This pattern is useful for:
992
+ // 2. Form service - manages the entire form
993
+ @Injectable()
994
+ class FormService {
995
+ constructor(
996
+ @Inject({ provide: InputService, useClass: InputService, scope: InjectionScope.Transient })
997
+ public readonly nameInput: InputService,
998
+ @Inject({ provide: InputService, useClass: InputService, scope: InjectionScope.Transient })
999
+ public readonly emailInput: InputService
1000
+ ) {
1001
+ // Initialize with default values
1002
+ this.nameInput.setValue('');
1003
+ this.emailInput.setValue('');
1004
+ }
374
1005
 
375
- - Building composable component hierarchies
376
- - Sharing state between parent and child components
377
- - Creating flexible component APIs
1006
+ isValid() {
1007
+ return this.nameInput.validate() && this.emailInput.validate();
1008
+ }
378
1009
 
379
- ## Advanced Usage
1010
+ submit() {
1011
+ if (this.isValid()) {
1012
+ console.log('Submitting:', {
1013
+ name: this.nameInput.value,
1014
+ email: this.emailInput.value,
1015
+ });
1016
+ }
1017
+ }
1018
+ }
380
1019
 
381
- ### Module Imports and Exports
1020
+ // 3. Input component
1021
+ const InputModuleBp = ProviderModule.blueprint({
1022
+ id: 'InputModule',
1023
+ providers: [InputService],
1024
+ });
382
1025
 
383
- Modules can import and re-export other modules:
1026
+ const Input = provideModuleToComponent(InputModuleBp, ({ label }: { label: string }) => {
1027
+ const inputService = useInject(InputService);
1028
+ const [value, setValue] = useState(inputService.value);
384
1029
 
385
- ```ts
386
- const DropdownModuleBp = ProviderModule.blueprint({
387
- id: 'DropdownModule',
388
- imports: [ListviewModuleBp], // Import ListviewModule
389
- providers: [DropdownService],
390
- exports: [
391
- ListviewModuleBp, // Re-export imported module
392
- DropdownService,
393
- ],
1030
+ return (
1031
+ <div>
1032
+ <label>{label}</label>
1033
+ <input
1034
+ value={value}
1035
+ onChange={(e) => {
1036
+ setValue(e.target.value);
1037
+ inputService.setValue(e.target.value);
1038
+ }}
1039
+ />
1040
+ {inputService.error && <span style={{ color: 'red' }}>{inputService.error}</span>}
1041
+ </div>
1042
+ );
394
1043
  });
395
- ```
396
1044
 
397
- ### Multiple Dependency Injection
1045
+ // 4. Form component - injects its InputService instances into child Input components
1046
+ const FormModuleBp = ProviderModule.blueprint({
1047
+ id: 'FormModule',
1048
+ providers: [FormService, InputService],
1049
+ exports: [FormService],
1050
+ });
398
1051
 
399
- Use `useInjectMany` to inject multiple dependencies:
1052
+ const Form = provideModuleToComponent(FormModuleBp, () => {
1053
+ const formService = useInject(FormService);
400
1054
 
401
- ```ts
402
- const [userService, apiService] = useInjectMany([UserService, ApiService]);
1055
+ return (
1056
+ <form>
1057
+ {/* Pass the form's InputService instances to the inputs */}
1058
+ <Input inject={[{ provide: InputService, useValue: formService.nameInput }]} label="Name" />
1059
+ <Input inject={[{ provide: InputService, useValue: formService.emailInput }]} label="Email" />
1060
+ <button type="button" onClick={() => formService.submit()}>
1061
+ Submit
1062
+ </button>
1063
+ </form>
1064
+ );
1065
+ });
403
1066
  ```
404
1067
 
405
- ## Unit Testing
1068
+ **What's happening here?**
1069
+
1070
+ 1. Each `Input` component normally gets its own `InputService`
1071
+ 2. The `Form` component creates two `InputService` instances in its constructor
1072
+ 3. The form **overrides** the input's services using the `inject` prop
1073
+ 4. All inputs share state through the parent form's services
1074
+
1075
+ ## Testing Your Code
406
1076
 
407
- Mock modules easily for testing:
1077
+ xInjection makes testing easy. You can mock entire modules or individual services.
1078
+
1079
+ ### Mocking an Entire Module
408
1080
 
409
1081
  ```tsx
410
1082
  import { act, render } from '@testing-library/react';
411
1083
 
412
1084
  // Original module
413
- const ApiModuleBp = ProviderModule.blueprint({
414
- id: 'ApiModule',
1085
+ const UserModuleBp = ProviderModule.blueprint({
1086
+ id: 'UserModule',
415
1087
  providers: [UserService, ApiService],
416
1088
  });
417
1089
 
418
- // Create mocked version
419
- const ApiModuleBpMocked = ApiModuleBp.clone().updateDefinition({
420
- id: 'ApiModuleMocked',
1090
+ // Create a mocked version
1091
+ const UserModuleMocked = UserModuleBp.clone().updateDefinition({
1092
+ id: 'UserModuleMocked',
421
1093
  providers: [
422
- { provide: UserService, useClass: UserServiceMock },
1094
+ {
1095
+ provide: UserService,
1096
+ useClass: UserServiceMock, // Your mock class
1097
+ },
423
1098
  {
424
1099
  provide: ApiService,
425
1100
  useValue: {
426
- sendRequest: vi.fn().mockResolvedValue({ data: 'mocked' }),
1101
+ get: vi.fn().mockResolvedValue({ name: 'Test User' }),
1102
+ post: vi.fn(),
427
1103
  },
428
1104
  },
429
1105
  ],
430
1106
  });
431
1107
 
432
- // Test with mocked module
433
- await act(async () => render(<MyComponent module={ApiModuleBpMocked} />));
1108
+ // Test with the mocked module
1109
+ it('should render user data', async () => {
1110
+ await act(async () => render(<UserProfile module={UserModuleMocked} />));
1111
+
1112
+ // Assert...
1113
+ });
434
1114
  ```
435
1115
 
436
- **Testing with Zustand:**
1116
+ ### Mocking on-the-fly
437
1117
 
438
1118
  ```tsx
439
- import { act, renderHook } from '@testing-library/react';
1119
+ import { act, render } from '@testing-library/react';
440
1120
 
441
- import { CounterService } from './counter.service';
1121
+ it('should render user data', async () => {
1122
+ await act(async () =>
1123
+ render(
1124
+ <UserProfile
1125
+ inject={{
1126
+ provide: ApiService,
1127
+ useValue: {
1128
+ get: vi.fn().mockResolvedValue({ name: 'Test User' }),
1129
+ post: vi.fn(),
1130
+ },
1131
+ }}
1132
+ />
1133
+ )
1134
+ );
1135
+
1136
+ // Assert...
1137
+ });
1138
+ ```
442
1139
 
443
- it('should increment counter via service', () => {
444
- const service = new CounterService();
1140
+ ## API Reference
445
1141
 
446
- const { result } = renderHook(() => service.useStore((s) => s.count));
1142
+ ### Core Functions
447
1143
 
448
- expect(result.current).toBe(0);
1144
+ #### `provideModuleToComponent(module, component)`
449
1145
 
450
- act(() => {
451
- service.increment();
452
- });
1146
+ Wraps a component to provide it with a module's services.
453
1147
 
454
- expect(result.current).toBe(1);
1148
+ ```tsx
1149
+ const MyComponent = provideModuleToComponent(MyModuleBp, (props) => {
1150
+ // Component body
455
1151
  });
1152
+ ```
1153
+
1154
+ [!CAUTION]
1155
+
1156
+ > The module must NOT have `isGlobal: true`. Global modules are accessed directly via `useInject` without the HoC. Providing a global module will throw an `InjectionProviderModuleError`.
1157
+
1158
+ #### `useInject(ServiceClass, options?)`
1159
+
1160
+ Injects a single service into a component.
1161
+
1162
+ ```tsx
1163
+ const service = useInject(MyService);
1164
+ const optionalService = useInject(OptionalService, { isOptional: true });
1165
+ ```
1166
+
1167
+ #### `useInjectMany(...services)`
1168
+
1169
+ Injects multiple services at once.
456
1170
 
457
- it('should handle complex business logic', () => {
458
- const service = new CounterService();
1171
+ ```tsx
1172
+ const [service1, service2] = useInjectMany(Service1, Service2);
1173
+ ```
1174
+
1175
+ #### `hookFactory({ use, inject })`
1176
+
1177
+ Creates a custom hook with injected dependencies.
459
1178
 
460
- act(() => {
461
- service.incrementBy(10);
462
- });
1179
+ **Parameters:**
463
1180
 
464
- expect(service.useStore.getState().count).toBe(10);
1181
+ - `use`: Hook function that receives parameters and `deps` array
1182
+ - `inject`: Array of provider tokens to inject
1183
+
1184
+ **Type signature:**
1185
+
1186
+ ```tsx
1187
+ function hookFactory<P extends HookParams, D extends any[], T>({
1188
+ use: (params: HookWithDeps<P, D>) => T,
1189
+ inject: ProviderToken[],
1190
+ }): (params: P) => T
1191
+ ```
1192
+
1193
+ **Usage:**
1194
+
1195
+ ```tsx
1196
+ import type { HookWithDeps } from '@adimm/x-injection-reactjs';
1197
+
1198
+ // Hook with no parameters
1199
+ const useTestHook = hookFactory({
1200
+ use: ({ deps: [testService] }: HookWithDeps<void, [TestService]>) => {
1201
+ return testService.value;
1202
+ },
1203
+ inject: [TestService],
465
1204
  });
1205
+
1206
+ // Hook with parameters
1207
+ const useUserData = hookFactory({
1208
+ use: ({ userId, deps: [apiService] }: HookWithDeps<{ userId: number }, [ApiService]>) => {
1209
+ const [data, setData] = useState(null);
1210
+ useEffect(() => {
1211
+ apiService.get(`/users/${userId}`).then(setData);
1212
+ }, [userId]);
1213
+ return data;
1214
+ },
1215
+ inject: [ApiService],
1216
+ });
1217
+
1218
+ // Usage in components
1219
+ const value = useTestHook(); // No params
1220
+ const data = useUserData({ userId: 123 }); // With params
1221
+ ```
1222
+
1223
+ **`HookWithDeps<P, D>` type utility:**
1224
+
1225
+ - **`P`**: Hook parameter type (`void` for no parameters, or object type for parameters)
1226
+ - **`D`**: Tuple type matching your `inject` array (e.g., `[ApiService, AuthService]`)
1227
+ - Automatically merges your parameters with the injected `deps` array
1228
+
1229
+ #### `@Injectable()`
1230
+
1231
+ Decorator that marks a class as injectable.
1232
+
1233
+ ```tsx
1234
+ @Injectable()
1235
+ class MyService {
1236
+ // Service implementation
1237
+ }
1238
+ ```
1239
+
1240
+ #### `ProviderModule.blueprint(definition)`
1241
+
1242
+ Creates a module **blueprint** (template) that can be instantiated multiple times. Use this for components that can have multiple instances.
1243
+
1244
+ ```tsx
1245
+ const MyModuleBp = ProviderModule.blueprint({
1246
+ id: 'MyModule',
1247
+ providers: [MyService],
1248
+ imports: [OtherModuleBp],
1249
+ exports: [MyService],
1250
+ });
1251
+ ```
1252
+
1253
+ **When to use:** Components that can have multiple instances (forms, lists, dialogs, etc.).
1254
+
1255
+ #### `ProviderModule.create(definition)`
1256
+
1257
+ Creates and immediately initializes a module instance. In React apps, **you rarely need this**—use `blueprint()` instead.
1258
+
1259
+ ```tsx
1260
+ const MyModule = ProviderModule.create({
1261
+ id: 'MyModule',
1262
+ providers: [MyService],
1263
+ });
1264
+
1265
+ // Access the module directly and use it as a service locator.
1266
+ const service = MyModule.get(MyService);
1267
+ ```
1268
+
1269
+ **When to use:** Direct module access outside of components, testing, or advanced scenarios. For normal React apps, use `blueprint()` for both global and component-scoped modules.
1270
+
1271
+ **Key difference:**
1272
+
1273
+ - `blueprint()`: Template - can create many instances. Use for **all** React modules (global and component-scoped).
1274
+ - `create()`: Immediate instance - for direct access outside React components.
1275
+
1276
+ ## FAQ
1277
+
1278
+ ### How do I add global services?
1279
+
1280
+ There are two ways to add global services:
1281
+
1282
+ **Method 1: Global Blueprint (Recommended)**
1283
+
1284
+ Create a global blueprint in your app's entry point. The library automatically imports it into the built-in `AppModule`:
1285
+
1286
+ ```tsx
1287
+ // main.tsx
1288
+
1289
+ import 'reflect-metadata';
1290
+
1291
+ import { Injectable, ProviderModule } from '@adimm/x-injection';
1292
+
1293
+ @Injectable()
1294
+ class ApiService {
1295
+ get(url: string) {
1296
+ /* ... */
1297
+ }
1298
+ }
1299
+
1300
+ // Automatically imported into AppModule
1301
+ ProviderModule.blueprint({
1302
+ id: 'AppBootstrapModule',
1303
+ isGlobal: true,
1304
+ providers: [ApiService],
1305
+ exports: [ApiService], // Make it available everywhere
1306
+ });
1307
+ ```
1308
+
1309
+ **Method 2: Dynamic Updates**
1310
+
1311
+ For runtime additions, use the built-in `AppModule` directly:
1312
+
1313
+ ```tsx
1314
+ import { AppModule } from '@adimm/x-injection';
1315
+
1316
+ // Add providers dynamically
1317
+ AppModule.update.addProvider(ApiService, true); // true = also export
1318
+ ```
1319
+
1320
+ [!WARNING]
1321
+
1322
+ > The library provides a built-in `AppModule`. Don't create your own module named "AppModule"—use one of the methods above instead.
1323
+
1324
+ ### When should I use global modules vs component-scoped modules?
1325
+
1326
+ See the decision guide in [Global vs Component-Scoped Services](#global-vs-component-scoped-services).
1327
+
1328
+ ### Can I use this with Redux/MobX/Zustand?
1329
+
1330
+ Yes! xInjection is state-library agnostic. Encapsulate your state management library inside a service:
1331
+
1332
+ ```tsx
1333
+ @Injectable()
1334
+ class TodoStore {
1335
+ private store = create<TodoState>(...);
1336
+
1337
+ get useStore() {
1338
+ return this.store;
1339
+ }
1340
+
1341
+ addTodo(text: string) {
1342
+ this.store.setState(...);
1343
+ }
1344
+ }
466
1345
  ```
467
1346
 
468
- ## Documentation
1347
+ ### How does this compare to React Context?
1348
+
1349
+ | Feature | xInjection | React Context |
1350
+ | ------------------------------- | ---------- | ------------- |
1351
+ | Automatic dependency resolution | ✅ | ❌ |
1352
+ | Component-scoped instances | ✅ | ❌ |
1353
+ | No provider hell | ✅ | ❌ |
1354
+ | Parent-child dependency control | ✅ | ❌ |
1355
+ | Works with class-based logic | ✅ | ❌ |
1356
+ | Testability | ✅ | ⚠️ |
1357
+ | TypeScript support | ✅ | ⚠️ |
1358
+
1359
+ ### Do I need to understand Dependency Injection to use this?
1360
+
1361
+ No! Think of it as a better way to organize your code:
1362
+
1363
+ 1. **Services** = Your business logic (API calls, state, validation)
1364
+ 2. **Modules** = Packages of services
1365
+ 3. **Inject** = Get a service in your component
1366
+
1367
+ That's it. The fancy terms (IoC, DI, containers) describe what's happening under the hood, but you don't need to understand them to be productive.
1368
+
1369
+ ### If I want Angular patterns, why not just use Angular?
1370
+
1371
+ Fair question! You shouldn't blindly adopt Angular patterns in React. Here's when this library makes sense:
1372
+
1373
+ **Use xInjection if:**
1374
+
1375
+ - You have complex business logic that's hard to test
1376
+ - You're building enterprise apps with many modules/features
1377
+ - You need component-scoped services (multiple instances of same component)
1378
+ - You want class-based services (better for complex logic than hooks)
1379
+ - You're migrating from Angular and want familiar patterns
1380
+
1381
+ **Don't use xInjection if:**
1382
+
1383
+ - Your app is simple (Context + hooks is fine)
1384
+ - You prefer functional programming over classes
1385
+ - Your team isn't comfortable with IoC/DI
1386
+ - You're building a small project
1387
+
1388
+ **Why not just use Angular?** Because you love React's component model, hooks, and ecosystem, but you need better architecture for complex business logic. This gives you the best of both worlds.
1389
+
1390
+ ### Can I migrate gradually from an existing React app?
1391
+
1392
+ Absolutely! Start with one component:
1393
+
1394
+ 1. Extract business logic into a service
1395
+ 2. Create a module for that service
1396
+ 3. Wrap the component with `provideModuleToComponent`
1397
+
1398
+ You can use xInjection alongside Context, Redux, or any other state management.
1399
+
1400
+ ### When do I actually need `provideModuleToComponent`?
1401
+
1402
+ This is a common point of confusion. Here's a simple decision tree:
1403
+
1404
+ **Don't need it (just use `useInject`):**
1405
+
1406
+ - All your services are global/singleton
1407
+ - Example: API client, auth service, theme service
1408
+
1409
+ **Need it (must use `provideModuleToComponent`):**
1410
+
1411
+ - You want multiple instances of a component, each with its own services
1412
+ - Component-specific state that shouldn't be global
1413
+ - Forms, modals, dialogs, reusable widgets
1414
+ - Parent needs to control child dependencies via `inject` prop
1415
+
1416
+ **Example:**
1417
+
1418
+ ```tsx
1419
+ // Global - NO provideModuleToComponent needed
1420
+ ProviderModule.blueprint({
1421
+ id: 'AppBootstrapModule',
1422
+ isGlobal: true,
1423
+ providers: [ApiService],
1424
+ exports: [ApiService],
1425
+ });
1426
+
1427
+ function MyComponent() {
1428
+ const apiService = useInject(ApiService); // Works! No HoC needed
1429
+ }
1430
+
1431
+ // Component-scoped - MUST use provideModuleToComponent
1432
+ const FormModuleBp = ProviderModule.blueprint({
1433
+ id: 'Form',
1434
+ providers: [FormService],
1435
+ });
1436
+
1437
+ const Form = provideModuleToComponent(FormModuleBp, () => {
1438
+ const formService = useInject(FormService); // Each <Form /> gets its own FormService
1439
+ });
1440
+ ```
1441
+
1442
+ ### What's the performance impact?
1443
+
1444
+ Minimal. The dependency container is lightweight, and services are created lazily (only when first requested). The HoC pattern has no performance overhead compared to standard React patterns.
1445
+
1446
+ **Runtime vs Build-time:** This library works entirely at runtime (not build-time):
1447
+
1448
+ - Runtime DI is more flexible (dynamic module loading, testing)
1449
+ - Performance impact is negligible (container operations are fast)
1450
+ - You get runtime debugging and introspection
1451
+ - Works with all bundlers/tools without special configuration
1452
+
1453
+ ### Is this production-ready?
1454
+
1455
+ Yes! This library is inspired by battle-tested patterns from Angular and NestJS, adapted for React.
1456
+
1457
+ ### Is "provider hell" really that bad?
1458
+
1459
+ You're right to question this! Provider hell becomes a real problem when:
1460
+
1461
+ - You need to pass providers down through component trees
1462
+ - Different parts of your app need different provider configurations
1463
+ - You want multiple instances of the same component with isolated state
1464
+ - You're constantly adding/removing providers as features change
1465
+
1466
+ If you're happy with your current Context setup, stick with it! This library is for teams that have **outgrown** simple Context patterns.
1467
+
1468
+ ### Why use classes for services instead of custom hooks?
1469
+
1470
+ Both approaches work! Here's when classes shine:
1471
+
1472
+ **Classes are better for:**
1473
+
1474
+ - Complex business logic (multiple methods, private state)
1475
+ - Dependency injection (automatic wiring)
1476
+ - Testing (easier to mock)
1477
+ - Encapsulation (private members, getters/setters)
1478
+
1479
+ **Hooks are better for:**
1480
+
1481
+ - Simple component logic
1482
+ - React-specific features (useState, useEffect)
1483
+ - Functional programming style
1484
+
1485
+ **You can use both!** Use classes for services, hooks for UI logic. The `hookFactory` even lets you create hooks that inject class-based services.
1486
+
1487
+ **Note:** Services are classes, but components are still functional! You write normal React functional components with hooks—only the business logic is in classes.
1488
+
1489
+ ## Links
469
1490
 
470
1491
  📚 **Full API Documentation:** [https://adimarianmutu.github.io/x-injection-reactjs](https://adimarianmutu.github.io/x-injection-reactjs/index.html)
471
1492
 
472
- For more information about the base library, see [xInjection Documentation](https://github.com/AdiMarianMutu/x-injection#readme).
1493
+ 🔧 **Base Library:** [xInjection](https://github.com/AdiMarianMutu/x-injection)
1494
+
1495
+ 💡 **Issues & Feature Requests:** [GitHub Issues](https://github.com/AdiMarianMutu/x-injection-reactjs/issues)
473
1496
 
474
1497
  ## Contributing
475
1498
 
476
- Pull requests are welcome! Please ensure your contributions follow the project's code style.
1499
+ Contributions are welcome! Please:
1500
+
1501
+ 1. Fork the repository
1502
+ 2. Create a feature branch
1503
+ 3. Make your changes
1504
+ 4. Add tests
1505
+ 5. Submit a pull request
1506
+
1507
+ Please ensure your code follows the project's style and all tests pass.
477
1508
 
478
1509
  ## License
479
1510
 
@@ -481,4 +1512,4 @@ MIT © [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
481
1512
 
482
1513
  ---
483
1514
 
484
- **Questions or issues?** Open an [issue on GitHub](https://github.com/AdiMarianMutu/x-injection-reactjs/issues)
1515
+ Made with ❤️ for the React community. If you find this library helpful, consider giving it a ⭐ on [GitHub](https://github.com/AdiMarianMutu/x-injection-reactjs)!