@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.
- package/README.md +1225 -194
- 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
|
-
**
|
|
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
|
-
- [
|
|
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
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
- [Dependency
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
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
|
-
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
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
|
-
##
|
|
81
|
+
## What Problems Does This Solve?
|
|
43
82
|
|
|
44
|
-
|
|
83
|
+
If you've built React apps, you've probably encountered these pain points:
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
222
|
+
// main.tsx - Your app entry point
|
|
223
|
+
|
|
224
|
+
import 'reflect-metadata';
|
|
76
225
|
|
|
77
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
233
|
+
class ApiService {
|
|
234
|
+
get(url: string) {
|
|
235
|
+
return fetch(url).then((r) => r.json());
|
|
236
|
+
}
|
|
82
237
|
}
|
|
83
238
|
|
|
84
|
-
|
|
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: [
|
|
281
|
+
providers: [UserDashboardService],
|
|
88
282
|
});
|
|
89
283
|
|
|
90
|
-
//
|
|
91
|
-
const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
|
|
92
|
-
const
|
|
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
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
424
|
+
[!IMPORTANT]
|
|
155
425
|
|
|
156
|
-
|
|
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
|
-
|
|
428
|
+
[!CAUTION]
|
|
164
429
|
|
|
165
|
-
|
|
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
|
-
|
|
432
|
+
**`blueprint()` vs `create()`:**
|
|
168
433
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
450
|
+
imports: [UtilsModuleBp], // Reuse LoggerService and DateService
|
|
451
|
+
providers: [UserProfileService], // Add UserProfileService
|
|
175
452
|
});
|
|
176
453
|
```
|
|
177
454
|
|
|
178
|
-
|
|
455
|
+
See [Module Imports and Exports](#module-imports-and-exports) for more advanced patterns.
|
|
179
456
|
|
|
180
|
-
|
|
457
|
+
[!CAUTION]
|
|
181
458
|
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
590
|
+
Each `<Counter />` has its own `CounterService`, so they don't interfere with each other.
|
|
198
591
|
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
676
|
+
### Injecting Multiple Services
|
|
210
677
|
|
|
211
|
-
|
|
678
|
+
Use `useInjectMany` to inject multiple services at once:
|
|
212
679
|
|
|
213
|
-
```
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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: [
|
|
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
|
-
//
|
|
224
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
962
|
+
### Complex Form with Shared State
|
|
346
963
|
|
|
347
|
-
|
|
964
|
+
This example demonstrates a powerful pattern: a parent form component controlling the state of multiple child input components.
|
|
348
965
|
|
|
349
|
-
```
|
|
350
|
-
|
|
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
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
969
|
+
// 1. Input service - manages a single input's state
|
|
970
|
+
@Injectable()
|
|
971
|
+
class InputService {
|
|
972
|
+
value = '';
|
|
973
|
+
error = '';
|
|
363
974
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
975
|
+
setValue(value: string) {
|
|
976
|
+
this.value = value;
|
|
977
|
+
this.validate();
|
|
978
|
+
}
|
|
367
979
|
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1006
|
+
isValid() {
|
|
1007
|
+
return this.nameInput.validate() && this.emailInput.validate();
|
|
1008
|
+
}
|
|
378
1009
|
|
|
379
|
-
|
|
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
|
-
|
|
1020
|
+
// 3. Input component
|
|
1021
|
+
const InputModuleBp = ProviderModule.blueprint({
|
|
1022
|
+
id: 'InputModule',
|
|
1023
|
+
providers: [InputService],
|
|
1024
|
+
});
|
|
382
1025
|
|
|
383
|
-
|
|
1026
|
+
const Input = provideModuleToComponent(InputModuleBp, ({ label }: { label: string }) => {
|
|
1027
|
+
const inputService = useInject(InputService);
|
|
1028
|
+
const [value, setValue] = useState(inputService.value);
|
|
384
1029
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1052
|
+
const Form = provideModuleToComponent(FormModuleBp, () => {
|
|
1053
|
+
const formService = useInject(FormService);
|
|
400
1054
|
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
414
|
-
id: '
|
|
1085
|
+
const UserModuleBp = ProviderModule.blueprint({
|
|
1086
|
+
id: 'UserModule',
|
|
415
1087
|
providers: [UserService, ApiService],
|
|
416
1088
|
});
|
|
417
1089
|
|
|
418
|
-
// Create mocked version
|
|
419
|
-
const
|
|
420
|
-
id: '
|
|
1090
|
+
// Create a mocked version
|
|
1091
|
+
const UserModuleMocked = UserModuleBp.clone().updateDefinition({
|
|
1092
|
+
id: 'UserModuleMocked',
|
|
421
1093
|
providers: [
|
|
422
|
-
{
|
|
1094
|
+
{
|
|
1095
|
+
provide: UserService,
|
|
1096
|
+
useClass: UserServiceMock, // Your mock class
|
|
1097
|
+
},
|
|
423
1098
|
{
|
|
424
1099
|
provide: ApiService,
|
|
425
1100
|
useValue: {
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1116
|
+
### Mocking on-the-fly
|
|
437
1117
|
|
|
438
1118
|
```tsx
|
|
439
|
-
import { act,
|
|
1119
|
+
import { act, render } from '@testing-library/react';
|
|
440
1120
|
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
const service = new CounterService();
|
|
1140
|
+
## API Reference
|
|
445
1141
|
|
|
446
|
-
|
|
1142
|
+
### Core Functions
|
|
447
1143
|
|
|
448
|
-
|
|
1144
|
+
#### `provideModuleToComponent(module, component)`
|
|
449
1145
|
|
|
450
|
-
|
|
451
|
-
service.increment();
|
|
452
|
-
});
|
|
1146
|
+
Wraps a component to provide it with a module's services.
|
|
453
1147
|
|
|
454
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
service.incrementBy(10);
|
|
462
|
-
});
|
|
1179
|
+
**Parameters:**
|
|
463
1180
|
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)!
|