@adimm/x-injection-reactjs 1.0.4 → 1.0.6
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 +924 -196
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,41 +12,134 @@
|
|
|
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
|
+
|
|
17
|
+
> **TL;DR** — Mark classes with `@Injectable()`, declare a `ProviderModule.blueprint()`, wrap your component with `provideModuleToComponent(MyModuleBp, () => { ... })`, then call `useInject(MyService)` inside. Dependencies are resolved automatically — no providers, no prop drilling, no manual wiring.
|
|
16
18
|
|
|
17
19
|
## Table of Contents
|
|
18
20
|
|
|
19
21
|
- [Table of Contents](#table-of-contents)
|
|
20
|
-
- [
|
|
22
|
+
- [What Problems Does This Solve?](#what-problems-does-this-solve)
|
|
23
|
+
- [1. Provider Hell](#1-provider-hell)
|
|
24
|
+
- [2. Prop Drilling](#2-prop-drilling)
|
|
25
|
+
- [3. Manual Dependency Wiring](#3-manual-dependency-wiring)
|
|
26
|
+
- [4. Business Logic Mixed with UI](#4-business-logic-mixed-with-ui)
|
|
21
27
|
- [Installation](#installation)
|
|
22
28
|
- [Quick Start](#quick-start)
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
29
|
+
- [How It Works](#how-it-works)
|
|
30
|
+
- [1. Services: Your Business Logic](#1-services-your-business-logic)
|
|
31
|
+
- [2. Modules: Organizing Dependencies](#2-modules-organizing-dependencies)
|
|
32
|
+
- [3. Injecting Services into Components](#3-injecting-services-into-components)
|
|
33
|
+
- [The Power of Component-Scoped Modules](#the-power-of-component-scoped-modules)
|
|
34
|
+
- [What Are Component-Scoped Modules?](#what-are-component-scoped-modules)
|
|
35
|
+
- [Pattern 1: Multiple Independent Instances](#pattern-1-multiple-independent-instances)
|
|
36
|
+
- [Pattern 2: Parent-Child Dependency Control](#pattern-2-parent-child-dependency-control)
|
|
37
|
+
- [Why Use the HoC Approach?](#why-use-the-hoc-approach)
|
|
38
|
+
- [1. Lifecycle-Bound Isolated Containers](#1-lifecycle-bound-isolated-containers)
|
|
39
|
+
- [2. Composition and Reusability](#2-composition-and-reusability)
|
|
40
|
+
- [Hierarchical Dependency Injection](#hierarchical-dependency-injection)
|
|
41
|
+
- [Creating Custom Hooks with Dependencies](#creating-custom-hooks-with-dependencies)
|
|
42
|
+
- [Parent Components Controlling Child Dependencies](#parent-components-controlling-child-dependencies)
|
|
35
43
|
- [Module Imports and Exports](#module-imports-and-exports)
|
|
36
|
-
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
44
|
+
- [Real-World Examples](#real-world-examples)
|
|
45
|
+
- [Zustand Store Integration](#zustand-store-integration)
|
|
46
|
+
- [Complex Form with Shared State](#complex-form-with-shared-state)
|
|
47
|
+
- [Testing Your Code](#testing-your-code)
|
|
48
|
+
- [Mocking an Entire Module](#mocking-an-entire-module)
|
|
49
|
+
- [Mocking on-the-fly](#mocking-on-the-fly)
|
|
50
|
+
- [FAQ](#faq)
|
|
51
|
+
- [How do I add global services?](#how-do-i-add-global-services)
|
|
52
|
+
- [When should I use global modules vs component-scoped modules?](#when-should-i-use-global-modules-vs-component-scoped-modules)
|
|
53
|
+
- [Can I use this with Redux/MobX/Zustand?](#can-i-use-this-with-reduxmobxzustand)
|
|
54
|
+
- [How does this compare to React Context?](#how-does-this-compare-to-react-context)
|
|
55
|
+
- [If I want Angular patterns, why not just use Angular?](#if-i-want-angular-patterns-why-not-just-use-angular)
|
|
56
|
+
- [Can I migrate gradually from an existing React app?](#can-i-migrate-gradually-from-an-existing-react-app)
|
|
57
|
+
- [When do I actually need `provideModuleToComponent`?](#when-do-i-actually-need-providemoduletocomponent)
|
|
58
|
+
- [What's the performance impact?](#whats-the-performance-impact)
|
|
59
|
+
- [Why use classes for services instead of custom hooks?](#why-use-classes-for-services-instead-of-custom-hooks)
|
|
60
|
+
- [Links](#links)
|
|
39
61
|
- [Contributing](#contributing)
|
|
40
62
|
- [License](#license)
|
|
41
63
|
|
|
42
|
-
##
|
|
64
|
+
## What Problems Does This Solve?
|
|
65
|
+
|
|
66
|
+
If you've built React apps, you've probably encountered these pain points:
|
|
67
|
+
|
|
68
|
+
### 1. Provider Hell
|
|
69
|
+
|
|
70
|
+
Your `App.tsx` becomes a nightmare of nested providers:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<AuthProvider>
|
|
74
|
+
<ThemeProvider>
|
|
75
|
+
<ApiProvider>
|
|
76
|
+
<ToastProvider>
|
|
77
|
+
<UserProvider>
|
|
78
|
+
<App />
|
|
79
|
+
</UserProvider>
|
|
80
|
+
</ToastProvider>
|
|
81
|
+
</ApiProvider>
|
|
82
|
+
</ThemeProvider>
|
|
83
|
+
</AuthProvider>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Prop Drilling
|
|
87
|
+
|
|
88
|
+
You pass props through 5 levels of components just to reach the one that needs them:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Dashboard user={user}>
|
|
92
|
+
<Sidebar user={user}>
|
|
93
|
+
<UserMenu user={user}>
|
|
94
|
+
<UserAvatar user={user} /> {/* Finally! */}
|
|
95
|
+
</UserMenu>
|
|
96
|
+
</Sidebar>
|
|
97
|
+
</Dashboard>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Manual Dependency Wiring
|
|
101
|
+
|
|
102
|
+
When a service needs dependencies, you manually create them in the right order:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
function UserProfile() {
|
|
106
|
+
// Must create ALL dependencies manually in correct order
|
|
107
|
+
const toastService = new ToastService();
|
|
108
|
+
const apiService = new ApiService();
|
|
109
|
+
const authService = new AuthService(apiService);
|
|
110
|
+
const userProfileService = new UserProfileService(apiService, authService, toastService);
|
|
111
|
+
|
|
112
|
+
// If AuthService adds a new dependency tomorrow, THIS BREAKS!
|
|
113
|
+
return <div>{userProfileService.displayName}</div>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4. Business Logic Mixed with UI
|
|
118
|
+
|
|
119
|
+
Your components become bloated with API calls, state management, and validation:
|
|
43
120
|
|
|
44
|
-
|
|
121
|
+
```tsx
|
|
122
|
+
function UserDashboard() {
|
|
123
|
+
const [user, setUser] = useState(null);
|
|
124
|
+
const [loading, setLoading] = useState(false);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
setLoading(true);
|
|
128
|
+
fetch('/api/user')
|
|
129
|
+
.then((res) => res.json())
|
|
130
|
+
.then((data) => {
|
|
131
|
+
setUser(data);
|
|
132
|
+
setLoading(false);
|
|
133
|
+
});
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
// 50 more lines of business logic...
|
|
137
|
+
|
|
138
|
+
return <div>{/* Your actual UI */}</div>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
45
141
|
|
|
46
|
-
|
|
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
|
|
142
|
+
xInjection solves all of the above by bringing **Inversion of Control (IoC)** and **Dependency Injection (DI)** to React: instead of components creating and managing their own dependencies, they just ask for what they need and xInjection provides it — automatically, type-safely, and testably.
|
|
50
143
|
|
|
51
144
|
This is the official [ReactJS](https://react.dev/) implementation of [xInjection](https://github.com/AdiMarianMutu/x-injection).
|
|
52
145
|
|
|
@@ -56,6 +149,21 @@ This is the official [ReactJS](https://react.dev/) implementation of [xInjection
|
|
|
56
149
|
npm i @adimm/x-injection-reactjs reflect-metadata
|
|
57
150
|
```
|
|
58
151
|
|
|
152
|
+
> [!IMPORTANT]
|
|
153
|
+
> Import `reflect-metadata` at the very top of your app entry point:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// main.tsx or index.tsx
|
|
157
|
+
|
|
158
|
+
import 'reflect-metadata';
|
|
159
|
+
|
|
160
|
+
import { createRoot } from 'react-dom/client';
|
|
161
|
+
|
|
162
|
+
import App from './App';
|
|
163
|
+
|
|
164
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
165
|
+
```
|
|
166
|
+
|
|
59
167
|
**TypeScript Configuration**
|
|
60
168
|
|
|
61
169
|
Add to your `tsconfig.json`:
|
|
@@ -69,171 +177,605 @@ Add to your `tsconfig.json`:
|
|
|
69
177
|
}
|
|
70
178
|
```
|
|
71
179
|
|
|
180
|
+
> **📚 Advanced Concepts**
|
|
181
|
+
>
|
|
182
|
+
> 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).
|
|
183
|
+
>
|
|
184
|
+
> The base library provides the core IoC/DI engine that powers this React integration.
|
|
185
|
+
|
|
72
186
|
## Quick Start
|
|
73
187
|
|
|
188
|
+
Three files, three concepts: global services declared once, a component-scoped module, and a component that injects both.
|
|
189
|
+
|
|
190
|
+
**Step 1 — Declare global services** in your entry point:
|
|
191
|
+
|
|
74
192
|
```tsx
|
|
75
|
-
|
|
193
|
+
// main.tsx - Your app entry point
|
|
194
|
+
|
|
195
|
+
import 'reflect-metadata';
|
|
196
|
+
|
|
197
|
+
import { Injectable, ProviderModule } from '@adimm/x-injection';
|
|
198
|
+
import { createRoot } from 'react-dom/client';
|
|
199
|
+
|
|
200
|
+
import App from './App';
|
|
76
201
|
|
|
77
|
-
//
|
|
202
|
+
// Global services (singletons)
|
|
78
203
|
@Injectable()
|
|
79
|
-
class
|
|
80
|
-
|
|
81
|
-
|
|
204
|
+
class ApiService {
|
|
205
|
+
get(url: string) {
|
|
206
|
+
return fetch(url).then((r) => r.json());
|
|
207
|
+
}
|
|
82
208
|
}
|
|
83
209
|
|
|
84
|
-
|
|
210
|
+
@Injectable()
|
|
211
|
+
class AuthService {
|
|
212
|
+
constructor(private readonly apiService: ApiService) {}
|
|
213
|
+
|
|
214
|
+
isLoggedIn = false;
|
|
215
|
+
|
|
216
|
+
login() {
|
|
217
|
+
this.isLoggedIn = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create global module - automatically imported into built-in AppModule
|
|
222
|
+
ProviderModule.blueprint({
|
|
223
|
+
id: 'AppBootstrapModule',
|
|
224
|
+
isGlobal: true,
|
|
225
|
+
providers: [ApiService, AuthService],
|
|
226
|
+
exports: [ApiService, AuthService], // Exported services available everywhere
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Now render your app
|
|
230
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Step 2 — Create a component-scoped module and inject services:**
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// UserDashboard.tsx - A component with its own service
|
|
237
|
+
|
|
238
|
+
import { Injectable, ProviderModule } from '@adimm/x-injection';
|
|
239
|
+
import { provideModuleToComponent, useInject } from '@adimm/x-injection-reactjs';
|
|
240
|
+
|
|
241
|
+
// Component-scoped service
|
|
242
|
+
@Injectable()
|
|
243
|
+
class UserDashboardService {
|
|
244
|
+
constructor(private readonly apiService: ApiService) {} // Gets global ApiService
|
|
245
|
+
|
|
246
|
+
async loadUser() {
|
|
247
|
+
return this.apiService.get('/user');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Component-scoped module
|
|
85
252
|
const UserDashboardModuleBp = ProviderModule.blueprint({
|
|
86
253
|
id: 'UserDashboardModule',
|
|
87
|
-
providers: [
|
|
254
|
+
providers: [UserDashboardService],
|
|
88
255
|
});
|
|
89
256
|
|
|
90
|
-
//
|
|
91
|
-
const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
|
|
92
|
-
const
|
|
257
|
+
// Component with injected service
|
|
258
|
+
export const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
|
|
259
|
+
const dashboardService = useInject(UserDashboardService);
|
|
260
|
+
const authService = useInject(AuthService); // Can also inject global services
|
|
93
261
|
|
|
94
262
|
return (
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
263
|
+
<div>
|
|
264
|
+
<h1>Dashboard</h1>
|
|
265
|
+
<p>Logged in: {authService.isLoggedIn ? 'Yes' : 'No'}</p>
|
|
266
|
+
</div>
|
|
98
267
|
);
|
|
99
268
|
});
|
|
100
269
|
```
|
|
101
270
|
|
|
102
|
-
|
|
271
|
+
**Step 3 — Use the component** — each instance gets its own module:
|
|
103
272
|
|
|
104
|
-
|
|
273
|
+
```tsx
|
|
274
|
+
// App.tsx
|
|
105
275
|
|
|
106
|
-
|
|
276
|
+
import { UserDashboard } from './UserDashboard';
|
|
277
|
+
|
|
278
|
+
export default function App() {
|
|
279
|
+
return (
|
|
280
|
+
<div>
|
|
281
|
+
<UserDashboard />
|
|
282
|
+
<UserDashboard /> {/* Each gets its own UserDashboardService */}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
> [!TIP] Global vs component-scoped services:
|
|
289
|
+
>
|
|
290
|
+
> - Global services (`ApiService`, `AuthService`): Defined in a global blueprint, automatically imported into the built-in `AppModule`
|
|
291
|
+
> - Component-scoped services (`UserDashboardService`): Fresh instance per `<UserDashboard />`
|
|
292
|
+
> - Component-scoped services can inject global services automatically
|
|
293
|
+
|
|
294
|
+
## How It Works
|
|
295
|
+
|
|
296
|
+
Let's break down the three main concepts you'll use:
|
|
297
|
+
|
|
298
|
+
### 1. Services: Your Business Logic
|
|
299
|
+
|
|
300
|
+
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
301
|
|
|
108
302
|
```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>;
|
|
303
|
+
@Injectable()
|
|
304
|
+
class TodoService {
|
|
305
|
+
private todos: Todo[] = [];
|
|
306
|
+
|
|
307
|
+
addTodo(text: string) {
|
|
308
|
+
this.todos.push({ id: Date.now(), text, completed: false });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getTodos() {
|
|
312
|
+
return this.todos;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
toggleTodo(id: number) {
|
|
316
|
+
const todo = this.todos.find((t) => t.id === id);
|
|
317
|
+
if (todo) todo.completed = !todo.completed;
|
|
318
|
+
}
|
|
128
319
|
}
|
|
129
320
|
```
|
|
130
321
|
|
|
131
|
-
|
|
322
|
+
The `@Injectable()` decorator marks this class as something that can be injected (either into components or other services/modules).
|
|
323
|
+
|
|
324
|
+
**Services can depend on other services:**
|
|
132
325
|
|
|
133
326
|
```tsx
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
327
|
+
@Injectable()
|
|
328
|
+
class UserProfileService {
|
|
329
|
+
// Dependencies are automatically injected via constructor
|
|
330
|
+
constructor(
|
|
331
|
+
private readonly apiService: ApiService,
|
|
332
|
+
private readonly authService: AuthService,
|
|
333
|
+
private readonly toastService: ToastService
|
|
334
|
+
) {}
|
|
335
|
+
|
|
336
|
+
async loadProfile() {
|
|
337
|
+
try {
|
|
338
|
+
const userId = this.authService.getCurrentUserId();
|
|
339
|
+
const profile = await this.apiService.get(`/users/${userId}`);
|
|
340
|
+
return profile;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.toastService.error('Failed to load profile');
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Notice how `UserProfileService` asks for its dependencies in the constructor? xInjection will automatically provide them.
|
|
350
|
+
|
|
351
|
+
**Alternative: Property Injection**
|
|
352
|
+
|
|
353
|
+
You can also use the `@Inject` decorator from the base library for property injection:
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { Inject, Injectable } from '@adimm/x-injection';
|
|
357
|
+
|
|
358
|
+
@Injectable()
|
|
359
|
+
class UserProfileService {
|
|
360
|
+
@Inject(ApiService)
|
|
361
|
+
private readonly apiService!: ApiService;
|
|
362
|
+
|
|
363
|
+
@Inject(AuthService)
|
|
364
|
+
private readonly authService!: AuthService;
|
|
365
|
+
|
|
366
|
+
async loadProfile() {
|
|
367
|
+
const userId = this.authService.getCurrentUserId();
|
|
368
|
+
return this.apiService.get(`/users/${userId}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Both approaches work! Constructor injection is generally preferred for better type safety and easier testing.
|
|
374
|
+
|
|
375
|
+
### 2. Modules: Organizing Dependencies
|
|
376
|
+
|
|
377
|
+
A **module** is a container that tells xInjection which services are available. Think of it as a "package" of services.
|
|
378
|
+
|
|
379
|
+
**Modules come in two flavors:**
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
// Global module: Created once, shared everywhere
|
|
383
|
+
ProviderModule.blueprint({
|
|
384
|
+
id: 'AppBootstrapModule',
|
|
385
|
+
isGlobal: true,
|
|
386
|
+
providers: [ApiService, AuthService, ToastService],
|
|
387
|
+
exports: [ApiService, AuthService, ToastService], // Only exported services become globally available
|
|
139
388
|
});
|
|
140
389
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
id: '
|
|
144
|
-
providers: [
|
|
390
|
+
// Component-scoped module: Each component instance gets its own
|
|
391
|
+
const TodoListModuleBp = ProviderModule.blueprint({
|
|
392
|
+
id: 'TodoListModule',
|
|
393
|
+
providers: [TodoService], // Gets a fresh TodoService per component
|
|
145
394
|
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
> [!IMPORTANT]
|
|
398
|
+
> When using `isGlobal: true`, only services listed in the `exports` array become globally available. Non-exported providers remain private to the module.
|
|
399
|
+
|
|
400
|
+
> [!CAUTION] Global modules cannot be used with `provideModuleToComponent`
|
|
401
|
+
> Attempting to provide a global module to a component will throw an `InjectionProviderModuleError`. Global services are accessed directly via `useInject` without the HoC.
|
|
402
|
+
|
|
403
|
+
**`blueprint()` vs `create()`:**
|
|
404
|
+
|
|
405
|
+
- **`blueprint()`**: A deferred module template. Each time it is imported or used with `provideModuleToComponent`, a **new independent instance** is created. Use for the global bootstrap module and for component-scoped modules. [Learn more](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#blueprints).
|
|
406
|
+
- **`create()`**: Immediately instantiates a module. The resulting instance is a **single shared object** — every module that imports it shares the exact same instance. Use when you need a module that is instantiated once and shared across multiple other modules.
|
|
407
|
+
|
|
408
|
+
See [Module Imports and Exports](#module-imports-and-exports) for examples of both.
|
|
146
409
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
410
|
+
> [!CAUTION] Never import `AppModule` into other modules
|
|
411
|
+
> `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`.
|
|
412
|
+
|
|
413
|
+
### 3. Injecting Services into Components
|
|
414
|
+
|
|
415
|
+
Use the `provideModuleToComponent` Higher-Order Component (HoC) to give your component access to services:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
|
|
419
|
+
// Inject the service you need
|
|
420
|
+
const userProfileService = useInject(UserProfileService);
|
|
421
|
+
|
|
422
|
+
return <div>{userProfileService.displayName}</div>;
|
|
151
423
|
});
|
|
152
424
|
```
|
|
153
425
|
|
|
154
|
-
|
|
426
|
+
The HoC does two things:
|
|
155
427
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
428
|
+
1. Creates an instance of your module (and all its services)
|
|
429
|
+
2. Makes those services available via the `useInject` hook
|
|
162
430
|
|
|
163
|
-
|
|
431
|
+
**You can also inject multiple services at once:**
|
|
164
432
|
|
|
165
|
-
|
|
433
|
+
```tsx
|
|
434
|
+
const MyComponent = provideModuleToComponent(MyModuleBp, () => {
|
|
435
|
+
const [userService, apiService] = useInjectMany(UserService, ApiService);
|
|
166
436
|
|
|
167
|
-
|
|
437
|
+
// Use your services...
|
|
438
|
+
});
|
|
439
|
+
```
|
|
168
440
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
441
|
+
## The Power of Component-Scoped Modules
|
|
442
|
+
|
|
443
|
+
One of the most powerful features of xInjection is **component-scoped modules**. This is something you can't easily achieve with React Context alone.
|
|
444
|
+
|
|
445
|
+
### What Are Component-Scoped Modules?
|
|
446
|
+
|
|
447
|
+
When you use `provideModuleToComponent`, each instance of your component gets its **own copy** of the module and all its services. This enables powerful patterns:
|
|
448
|
+
|
|
449
|
+
### Pattern 1: Multiple Independent Instances
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
@Injectable()
|
|
453
|
+
class CounterService {
|
|
454
|
+
count = 0;
|
|
455
|
+
increment() {
|
|
456
|
+
this.count++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const CounterModuleBp = ProviderModule.blueprint({
|
|
461
|
+
id: 'CounterModule',
|
|
462
|
+
providers: [CounterService],
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const Counter = provideModuleToComponent(CounterModuleBp, () => {
|
|
466
|
+
const counterService = useInject(CounterService);
|
|
467
|
+
return (
|
|
468
|
+
<div>
|
|
469
|
+
<p>Count: {counterService.count}</p>
|
|
470
|
+
<button onClick={() => counterService.increment()}>+</button>
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
175
473
|
});
|
|
474
|
+
|
|
475
|
+
function App() {
|
|
476
|
+
return (
|
|
477
|
+
<div>
|
|
478
|
+
<Counter /> {/* Count: 0 */}
|
|
479
|
+
<Counter /> {/* Count: 0 (separate instance!) */}
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
176
483
|
```
|
|
177
484
|
|
|
178
|
-
|
|
485
|
+
Each `<Counter />` has its own `CounterService`, so they don't interfere with each other.
|
|
179
486
|
|
|
180
|
-
###
|
|
487
|
+
### Pattern 2: Parent-Child Dependency Control
|
|
181
488
|
|
|
182
|
-
|
|
489
|
+
Parent components can "inject" specific service instances into their children:
|
|
183
490
|
|
|
184
|
-
```
|
|
185
|
-
|
|
491
|
+
```tsx
|
|
492
|
+
const ParentModuleBp = ProviderModule.blueprint({
|
|
493
|
+
id: 'ParentModule',
|
|
494
|
+
providers: [SharedService, ParentService],
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const ChildModuleBp = ProviderModule.blueprint({
|
|
498
|
+
id: 'ChildModule',
|
|
499
|
+
providers: [SharedService, ChildService],
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const Child = provideModuleToComponent(ChildModuleBp, () => {
|
|
503
|
+
const sharedService = useInject(SharedService);
|
|
504
|
+
return <div>{sharedService.data}</div>;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const Parent = provideModuleToComponent(ParentModuleBp, () => {
|
|
508
|
+
const sharedService = useInject(SharedService);
|
|
509
|
+
|
|
510
|
+
// Pass the parent's SharedService instance to the child
|
|
511
|
+
return <Child inject={[{ provide: SharedService, useValue: sharedService }]} />;
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
This enables complex patterns like form components sharing validation services, or composite UI components coordinating state.
|
|
516
|
+
|
|
517
|
+
## Why Use the HoC Approach?
|
|
518
|
+
|
|
519
|
+
You might wonder: "Why wrap my component with `provideModuleToComponent` instead of just using `useInject` directly everywhere?"
|
|
520
|
+
|
|
521
|
+
**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`.
|
|
522
|
+
|
|
523
|
+
The Higher-Order Component (HoC) pattern provides several key benefits:
|
|
524
|
+
|
|
525
|
+
### 1. Lifecycle-Bound Isolated Containers
|
|
526
|
+
|
|
527
|
+
Each wrapped component gets its **own** dependency container, created on mount and disposed on unmount. Two instances of `<TodoList />` each get their own `TodoService` — they never share state. When the component unmounts, `onDispose` runs automatically, cleaning up only that component's services. Imported global services remain unaffected.
|
|
528
|
+
|
|
529
|
+
### 2. Composition and Reusability
|
|
530
|
+
|
|
531
|
+
The HoC pattern works seamlessly with React's component composition model:
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
// Reusable component with its own dependencies
|
|
535
|
+
const TodoList = provideModuleToComponent(TodoListModuleBp, () => {
|
|
536
|
+
const todoService = useInject(TodoService);
|
|
537
|
+
// ...
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Use it multiple times, each with isolated state
|
|
541
|
+
function App() {
|
|
542
|
+
return (
|
|
543
|
+
<>
|
|
544
|
+
<TodoList /> {/* Gets its own TodoService */}
|
|
545
|
+
<TodoList /> {/* Gets a different TodoService */}
|
|
546
|
+
</>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Hierarchical Dependency Injection
|
|
552
|
+
|
|
553
|
+
Every component wrapped with `provideModuleToComponent` gets its own module container. When `useInject` is called inside that component, xInjection walks a well-defined lookup chain:
|
|
554
|
+
|
|
555
|
+
1. **Own module** — services declared in the component's own blueprint
|
|
556
|
+
2. **Imported modules** — exported services from modules listed in `imports`
|
|
557
|
+
3. **AppModule** — globally available services (from `isGlobal: true` blueprints)
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
useInject(SomeService) ← called inside <MyComponent />
|
|
561
|
+
│
|
|
562
|
+
▼
|
|
563
|
+
┌─────────────────────────┐
|
|
564
|
+
│ MyComponent's module │ ← providers: [MyService, ...]
|
|
565
|
+
│ (own container) │
|
|
566
|
+
└───────────┬─────────────┘
|
|
567
|
+
│ not found
|
|
568
|
+
▼
|
|
569
|
+
┌─────────────────────────┐
|
|
570
|
+
│ Imported modules │ ← imports: [SharedModule]
|
|
571
|
+
│ (exported only) │ SharedModule.exports: [SharedService]
|
|
572
|
+
└───────────┬─────────────┘
|
|
573
|
+
│ not found
|
|
574
|
+
▼
|
|
575
|
+
┌─────────────────────────┐
|
|
576
|
+
│ AppModule │ ← AppBootstrapModule { isGlobal: true }
|
|
577
|
+
│ (global services) │ exports: [ApiService, AuthService, ...]
|
|
578
|
+
└───────────┬─────────────┘
|
|
579
|
+
│ not found
|
|
580
|
+
▼
|
|
581
|
+
throws error
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Component example:**
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
// ① Global services — live in AppModule, available everywhere
|
|
186
588
|
@Injectable()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
589
|
+
class ApiService {}
|
|
590
|
+
@Injectable()
|
|
591
|
+
class AuthService {}
|
|
190
592
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
593
|
+
ProviderModule.blueprint({
|
|
594
|
+
id: 'AppBootstrapModule',
|
|
595
|
+
isGlobal: true,
|
|
596
|
+
providers: [ApiService, AuthService],
|
|
597
|
+
exports: [ApiService, AuthService],
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ② Shared module — created once, imported into component blueprints
|
|
601
|
+
@Injectable()
|
|
602
|
+
class AnalyticsService {}
|
|
603
|
+
|
|
604
|
+
const SharedModule = ProviderModule.create({
|
|
605
|
+
id: 'SharedModule',
|
|
606
|
+
providers: [AnalyticsService],
|
|
607
|
+
exports: [AnalyticsService], // ✅ visible to importers
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ③ Component-scoped service — private to this component
|
|
611
|
+
@Injectable()
|
|
612
|
+
class DashboardService {
|
|
613
|
+
constructor(
|
|
614
|
+
private readonly api: ApiService, // resolved from ③ AppModule
|
|
615
|
+
private readonly analytics: AnalyticsService // resolved from ② SharedModule
|
|
616
|
+
) {}
|
|
194
617
|
}
|
|
618
|
+
|
|
619
|
+
const DashboardModuleBp = ProviderModule.blueprint({
|
|
620
|
+
id: 'DashboardModule',
|
|
621
|
+
imports: [SharedModule],
|
|
622
|
+
providers: [DashboardService], // ① own container
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const Dashboard = provideModuleToComponent(DashboardModuleBp, () => {
|
|
626
|
+
const dashboard = useInject(DashboardService); // ✅ ① own module
|
|
627
|
+
const analytics = useInject(AnalyticsService); // ✅ ② SharedModule export
|
|
628
|
+
const auth = useInject(AuthService); // ✅ ③ AppModule (global)
|
|
629
|
+
|
|
630
|
+
// useInject(SomePrivateService) // ❌ not found → error
|
|
631
|
+
});
|
|
195
632
|
```
|
|
196
633
|
|
|
197
|
-
|
|
634
|
+
> [!TIP]
|
|
635
|
+
> A service that is not listed in a module's `exports` is completely invisible to any component that imports that module. This is how xInjection enforces encapsulation — only what you explicitly export crosses the module boundary.
|
|
198
636
|
|
|
199
|
-
|
|
637
|
+
### Creating Custom Hooks with Dependencies
|
|
638
|
+
|
|
639
|
+
The `hookFactory` function lets you create reusable custom hooks that automatically receive injected dependencies:
|
|
200
640
|
|
|
201
641
|
```tsx
|
|
202
|
-
|
|
203
|
-
|
|
642
|
+
// Define a custom hook with dependencies
|
|
643
|
+
const useUserProfile = hookFactory({
|
|
644
|
+
use: ({ userId, deps: [apiService, authService] }) => {
|
|
645
|
+
const [profile, setProfile] = useState(null);
|
|
204
646
|
|
|
205
|
-
|
|
647
|
+
useEffect(() => {
|
|
648
|
+
apiService.get(`/users/${userId}`).then(setProfile);
|
|
649
|
+
}, [userId]);
|
|
650
|
+
|
|
651
|
+
return profile;
|
|
652
|
+
},
|
|
653
|
+
inject: [ApiService, AuthService],
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Use it in any component
|
|
657
|
+
const UserProfile = provideModuleToComponent<{ userId: number }>(UserModuleBp, ({ userId }) => {
|
|
658
|
+
const profile = useUserProfile({ userId });
|
|
659
|
+
return <div>{profile?.name}</div>;
|
|
206
660
|
});
|
|
207
661
|
```
|
|
208
662
|
|
|
209
|
-
|
|
663
|
+
**Type-safe hooks with `HookWithDeps`:**
|
|
210
664
|
|
|
211
|
-
|
|
665
|
+
Use the `HookWithDeps<P, D>` type utility for full TypeScript support:
|
|
212
666
|
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
667
|
+
```tsx
|
|
668
|
+
import type { HookWithDeps } from '@adimm/x-injection-reactjs';
|
|
669
|
+
|
|
670
|
+
// Hook with no parameters - use void as first generic
|
|
671
|
+
const useTestHook = hookFactory({
|
|
672
|
+
use: ({ deps: [testService] }: HookWithDeps<void, [TestService]>) => {
|
|
673
|
+
return testService.value;
|
|
674
|
+
},
|
|
675
|
+
inject: [TestService],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Hook with parameters - specify parameter type as first generic
|
|
679
|
+
const useUserData = hookFactory({
|
|
680
|
+
use: ({ userId, deps: [apiService] }: HookWithDeps<{ userId: number }, [ApiService]>) => {
|
|
681
|
+
const [data, setData] = useState(null);
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
apiService.get(`/users/${userId}`).then(setData);
|
|
684
|
+
}, [userId]);
|
|
685
|
+
return data;
|
|
219
686
|
},
|
|
220
|
-
inject: [
|
|
687
|
+
inject: [ApiService],
|
|
221
688
|
});
|
|
222
689
|
|
|
223
|
-
//
|
|
224
|
-
|
|
690
|
+
// Usage:
|
|
691
|
+
useTestHook(); // No parameters
|
|
692
|
+
useUserData({ userId: 123 }); // With parameters
|
|
225
693
|
```
|
|
226
694
|
|
|
227
|
-
|
|
695
|
+
**`HookWithDeps<P, D>` generics:**
|
|
696
|
+
|
|
697
|
+
- **`P`**: Hook parameter type (use `void` if no parameters, or `{ param1: type, ... }` for parameters)
|
|
698
|
+
- **`D`**: Tuple type matching your `inject` array (e.g., `[ApiService, AuthService]`)
|
|
699
|
+
|
|
700
|
+
> [!TIP] Why use hookFactory?
|
|
701
|
+
>
|
|
702
|
+
> - Dependencies are automatically injected
|
|
703
|
+
> - Hooks are reusable across components
|
|
704
|
+
> - Type-safe with TypeScript
|
|
705
|
+
> - Easier to test (mock dependencies)
|
|
706
|
+
|
|
707
|
+
### Parent Components Controlling Child Dependencies
|
|
708
|
+
|
|
709
|
+
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.
|
|
710
|
+
|
|
711
|
+
### Module Imports and Exports
|
|
712
|
+
|
|
713
|
+
Modules can import other modules. The key question is: **should the imported module be shared or duplicated per component?**
|
|
714
|
+
|
|
715
|
+
**Shared module instance → `ProviderModule.create()`:**
|
|
716
|
+
|
|
717
|
+
Use `create()` when a module should exist as one instance and be shared by all blueprints that import it:
|
|
718
|
+
|
|
719
|
+
```tsx
|
|
720
|
+
// Instantiated once — all importers share the same instance and the same singletons
|
|
721
|
+
const CoreModule = ProviderModule.create({
|
|
722
|
+
id: 'CoreModule',
|
|
723
|
+
providers: [SomeSharedService],
|
|
724
|
+
exports: [SomeSharedService],
|
|
725
|
+
});
|
|
228
726
|
|
|
229
|
-
|
|
727
|
+
const UserModuleBp = ProviderModule.blueprint({
|
|
728
|
+
id: 'UserModule',
|
|
729
|
+
imports: [CoreModule], // every <UserComponent /> shares the same CoreModule
|
|
730
|
+
providers: [UserService],
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const ProductModuleBp = ProviderModule.blueprint({
|
|
734
|
+
id: 'ProductModule',
|
|
735
|
+
imports: [CoreModule], // same CoreModule instance
|
|
736
|
+
providers: [ProductService],
|
|
737
|
+
});
|
|
738
|
+
```
|
|
230
739
|
|
|
231
|
-
|
|
740
|
+
**Per-component isolation → blueprint imports:**
|
|
741
|
+
|
|
742
|
+
Import a blueprint when each component instance should get its own independent copy of those providers:
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
const UserModuleBp = ProviderModule.blueprint({
|
|
746
|
+
id: 'UserModule',
|
|
747
|
+
imports: [FormValidationModuleBp], // each <UserComponent /> gets its own FormValidationService
|
|
748
|
+
providers: [UserService],
|
|
749
|
+
});
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Re-exporting:**
|
|
753
|
+
|
|
754
|
+
```tsx
|
|
755
|
+
const CoreModule = ProviderModule.create({
|
|
756
|
+
id: 'CoreModule',
|
|
757
|
+
imports: [DatabaseModule, CacheModule],
|
|
758
|
+
exports: [DatabaseModule, CacheModule], // expose both to importers
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Real-World Examples
|
|
763
|
+
|
|
764
|
+
### Zustand Store Integration
|
|
765
|
+
|
|
766
|
+
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.
|
|
767
|
+
|
|
768
|
+
**Why this pattern?**
|
|
769
|
+
|
|
770
|
+
- Business logic stays in services (testable, reusable)
|
|
771
|
+
- Components subscribe to state reactively (optimal re-renders)
|
|
772
|
+
- Store is scoped to the component (no global state pollution)
|
|
773
|
+
- Type-safe and easy to test
|
|
232
774
|
|
|
233
775
|
```ts
|
|
234
776
|
// counter.service.ts
|
|
235
777
|
|
|
236
|
-
import { Injectable } from '@adimm/x-injection
|
|
778
|
+
import { Injectable } from '@adimm/x-injection';
|
|
237
779
|
import { create } from 'zustand';
|
|
238
780
|
|
|
239
781
|
interface CounterStore {
|
|
@@ -293,7 +835,7 @@ export class CounterService {
|
|
|
293
835
|
```ts
|
|
294
836
|
// counter.module.ts
|
|
295
837
|
|
|
296
|
-
import { ProviderModule } from '@adimm/x-injection
|
|
838
|
+
import { ProviderModule } from '@adimm/x-injection';
|
|
297
839
|
|
|
298
840
|
import { CounterService } from './counter.service';
|
|
299
841
|
|
|
@@ -342,138 +884,324 @@ export default Counter;
|
|
|
342
884
|
- **Reusability**: Services with stores can be shared across components via dependency injection
|
|
343
885
|
- **Type safety**: Full TypeScript support throughout
|
|
344
886
|
|
|
345
|
-
###
|
|
887
|
+
### Complex Form with Shared State
|
|
346
888
|
|
|
347
|
-
|
|
889
|
+
This example demonstrates a powerful pattern: a parent form component controlling the state of multiple child input components.
|
|
348
890
|
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
const ChildModuleBp = ProviderModule.blueprint({
|
|
352
|
-
id: 'ChildModule',
|
|
353
|
-
providers: [ChildService],
|
|
354
|
-
exports: [ChildService],
|
|
355
|
-
});
|
|
891
|
+
```tsx
|
|
892
|
+
import { Inject, Injectable, InjectionScope } from '@adimm/x-injection';
|
|
356
893
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
894
|
+
// 1. Input service - manages a single input's state
|
|
895
|
+
@Injectable()
|
|
896
|
+
class InputService {
|
|
897
|
+
value = '';
|
|
898
|
+
error = '';
|
|
363
899
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
900
|
+
setValue(value: string) {
|
|
901
|
+
this.value = value;
|
|
902
|
+
this.validate();
|
|
903
|
+
}
|
|
367
904
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
905
|
+
validate() {
|
|
906
|
+
if (!this.value) {
|
|
907
|
+
this.error = 'Required';
|
|
908
|
+
} else if (this.value.length < 3) {
|
|
909
|
+
this.error = 'Too short';
|
|
910
|
+
} else {
|
|
911
|
+
this.error = '';
|
|
912
|
+
}
|
|
913
|
+
return !this.error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
372
916
|
|
|
373
|
-
|
|
917
|
+
// 2. Form service - manages the entire form
|
|
918
|
+
@Injectable()
|
|
919
|
+
class FormService {
|
|
920
|
+
constructor(
|
|
921
|
+
public readonly nameInput: InputService,
|
|
922
|
+
public readonly emailInput: InputService
|
|
923
|
+
) {
|
|
924
|
+
// Initialize with default values
|
|
925
|
+
this.nameInput.setValue('');
|
|
926
|
+
this.emailInput.setValue('');
|
|
927
|
+
}
|
|
374
928
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
929
|
+
isValid() {
|
|
930
|
+
return this.nameInput.validate() && this.emailInput.validate();
|
|
931
|
+
}
|
|
378
932
|
|
|
379
|
-
|
|
933
|
+
submit() {
|
|
934
|
+
if (this.isValid()) {
|
|
935
|
+
console.log('Submitting:', {
|
|
936
|
+
name: this.nameInput.value,
|
|
937
|
+
email: this.emailInput.value,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
380
942
|
|
|
381
|
-
|
|
943
|
+
// 3. Input component
|
|
944
|
+
const InputModuleBp = ProviderModule.blueprint({
|
|
945
|
+
id: 'InputModule',
|
|
946
|
+
providers: [InputService],
|
|
947
|
+
exports: [InputService],
|
|
948
|
+
});
|
|
382
949
|
|
|
383
|
-
|
|
950
|
+
const Input = provideModuleToComponent<{ label: string }>(InputModuleBp, ({ label }) => {
|
|
951
|
+
const inputService = useInject(InputService);
|
|
952
|
+
const [value, setValue] = useState(inputService.value);
|
|
384
953
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
954
|
+
return (
|
|
955
|
+
<div>
|
|
956
|
+
<label>{label}</label>
|
|
957
|
+
<input
|
|
958
|
+
value={value}
|
|
959
|
+
onChange={(e) => {
|
|
960
|
+
setValue(e.target.value);
|
|
961
|
+
inputService.setValue(e.target.value);
|
|
962
|
+
}}
|
|
963
|
+
/>
|
|
964
|
+
{inputService.error && <span style={{ color: 'red' }}>{inputService.error}</span>}
|
|
965
|
+
</div>
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// 4. Form component - injects its InputService instances into child Input components
|
|
970
|
+
const FormModuleBp = ProviderModule.blueprint({
|
|
971
|
+
id: 'FormModule',
|
|
972
|
+
imports: [
|
|
973
|
+
// Clone InputModuleBp and override its defaultScope to Transient for this specific use.
|
|
974
|
+
// Without Transient, both `nameInput` and `emailInput` in FormService would resolve to
|
|
975
|
+
// the same singleton — they'd share state. Transient ensures each @Inject(InputService)
|
|
976
|
+
// parameter in FormService's constructor gets its own independent instance.
|
|
977
|
+
// This is also a good showcase of blueprint dynamicity: the original InputModuleBp is
|
|
978
|
+
// left untouched, and only this consumer opts into Transient behavior.
|
|
979
|
+
InputModuleBp.clone().updateDefinition({
|
|
980
|
+
...InputModuleBp.getDefinition(),
|
|
981
|
+
defaultScope: InjectionScope.Transient,
|
|
982
|
+
}),
|
|
393
983
|
],
|
|
984
|
+
providers: [FormService],
|
|
985
|
+
exports: [FormService],
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
const Form = provideModuleToComponent(FormModuleBp, () => {
|
|
989
|
+
const formService = useInject(FormService);
|
|
990
|
+
|
|
991
|
+
return (
|
|
992
|
+
<form>
|
|
993
|
+
{/* Pass the form's InputService instances to the inputs */}
|
|
994
|
+
<Input inject={[{ provide: InputService, useValue: formService.nameInput }]} label="Name" />
|
|
995
|
+
<Input inject={[{ provide: InputService, useValue: formService.emailInput }]} label="Email" />
|
|
996
|
+
<button type="button" onClick={() => formService.submit()}>
|
|
997
|
+
Submit
|
|
998
|
+
</button>
|
|
999
|
+
</form>
|
|
1000
|
+
);
|
|
394
1001
|
});
|
|
395
1002
|
```
|
|
396
1003
|
|
|
397
|
-
|
|
1004
|
+
**What's happening here?**
|
|
398
1005
|
|
|
399
|
-
|
|
1006
|
+
1. Each `Input` component normally gets its own `InputService`
|
|
1007
|
+
2. The `Form` component creates two `InputService` instances in its constructor
|
|
1008
|
+
3. The form **overrides** the input's services using the `inject` prop
|
|
1009
|
+
4. All inputs share state through the parent form's services
|
|
400
1010
|
|
|
401
|
-
|
|
402
|
-
const [userService, apiService] = useInjectMany([UserService, ApiService]);
|
|
403
|
-
```
|
|
1011
|
+
## Testing Your Code
|
|
404
1012
|
|
|
405
|
-
|
|
1013
|
+
xInjection makes testing easy. You can mock entire modules or individual services.
|
|
406
1014
|
|
|
407
|
-
|
|
1015
|
+
### Mocking an Entire Module
|
|
408
1016
|
|
|
409
1017
|
```tsx
|
|
410
1018
|
import { act, render } from '@testing-library/react';
|
|
411
1019
|
|
|
412
1020
|
// Original module
|
|
413
|
-
const
|
|
414
|
-
id: '
|
|
1021
|
+
const UserModuleBp = ProviderModule.blueprint({
|
|
1022
|
+
id: 'UserModule',
|
|
415
1023
|
providers: [UserService, ApiService],
|
|
416
1024
|
});
|
|
417
1025
|
|
|
418
|
-
// Create mocked version
|
|
419
|
-
const
|
|
420
|
-
id: '
|
|
1026
|
+
// Create a mocked version
|
|
1027
|
+
const UserModuleMocked = UserModuleBp.clone().updateDefinition({
|
|
1028
|
+
id: 'UserModuleMocked',
|
|
421
1029
|
providers: [
|
|
422
|
-
{
|
|
1030
|
+
{
|
|
1031
|
+
provide: UserService,
|
|
1032
|
+
useClass: UserServiceMock, // Your mock class
|
|
1033
|
+
},
|
|
423
1034
|
{
|
|
424
1035
|
provide: ApiService,
|
|
425
1036
|
useValue: {
|
|
426
|
-
|
|
1037
|
+
get: vi.fn().mockResolvedValue({ name: 'Test User' }),
|
|
1038
|
+
post: vi.fn(),
|
|
427
1039
|
},
|
|
428
1040
|
},
|
|
429
1041
|
],
|
|
430
1042
|
});
|
|
431
1043
|
|
|
432
|
-
// Test with mocked module
|
|
433
|
-
|
|
1044
|
+
// Test with the mocked module
|
|
1045
|
+
it('should render user data', async () => {
|
|
1046
|
+
await act(async () => render(<UserProfile module={UserModuleMocked} />));
|
|
1047
|
+
|
|
1048
|
+
// Assert...
|
|
1049
|
+
});
|
|
434
1050
|
```
|
|
435
1051
|
|
|
436
|
-
|
|
1052
|
+
### Mocking on-the-fly
|
|
437
1053
|
|
|
438
1054
|
```tsx
|
|
439
|
-
import { act,
|
|
1055
|
+
import { act, render } from '@testing-library/react';
|
|
440
1056
|
|
|
441
|
-
|
|
1057
|
+
it('should render user data', async () => {
|
|
1058
|
+
await act(async () =>
|
|
1059
|
+
render(
|
|
1060
|
+
<UserProfile
|
|
1061
|
+
inject={{
|
|
1062
|
+
provide: ApiService,
|
|
1063
|
+
useValue: {
|
|
1064
|
+
get: vi.fn().mockResolvedValue({ name: 'Test User' }),
|
|
1065
|
+
post: vi.fn(),
|
|
1066
|
+
},
|
|
1067
|
+
}}
|
|
1068
|
+
/>
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
442
1071
|
|
|
443
|
-
|
|
444
|
-
|
|
1072
|
+
// Assert...
|
|
1073
|
+
});
|
|
1074
|
+
```
|
|
445
1075
|
|
|
446
|
-
|
|
1076
|
+
## FAQ
|
|
447
1077
|
|
|
448
|
-
|
|
1078
|
+
### How do I add global services?
|
|
449
1079
|
|
|
450
|
-
|
|
451
|
-
service.increment();
|
|
452
|
-
});
|
|
1080
|
+
**Recommended:** Use a global blueprint with `isGlobal: true` in your entry point — see [Quick Start](#quick-start) and [Modules: Organizing Dependencies](#2-modules-organizing-dependencies) for the full pattern.
|
|
453
1081
|
|
|
454
|
-
|
|
455
|
-
});
|
|
1082
|
+
**For runtime additions**, use the built-in `AppModule` directly:
|
|
456
1083
|
|
|
457
|
-
|
|
458
|
-
|
|
1084
|
+
```tsx
|
|
1085
|
+
import { AppModule } from '@adimm/x-injection';
|
|
459
1086
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
1087
|
+
AppModule.update.addProvider(ApiService, true); // true = also export
|
|
1088
|
+
```
|
|
463
1089
|
|
|
464
|
-
|
|
465
|
-
|
|
1090
|
+
> [!WARNING]
|
|
1091
|
+
> The library provides a built-in `AppModule`. Don't create your own module named "AppModule"—use one of the methods above instead.
|
|
1092
|
+
|
|
1093
|
+
### When should I use global modules vs component-scoped modules?
|
|
1094
|
+
|
|
1095
|
+
**Global** (`isGlobal: true` + `exports`): API clients, auth state, routing, theme, toast notifications — accessed directly via `useInject` without a HoC.
|
|
1096
|
+
|
|
1097
|
+
**Component-scoped** (blueprint without `isGlobal`): Form state, component-specific business logic, UI state — must use `provideModuleToComponent`; each instance gets its own module.
|
|
1098
|
+
|
|
1099
|
+
### Can I use this with Redux/MobX/Zustand?
|
|
1100
|
+
|
|
1101
|
+
Yes! xInjection is state-library agnostic. Encapsulate your state management library inside a service:
|
|
1102
|
+
|
|
1103
|
+
```tsx
|
|
1104
|
+
@Injectable()
|
|
1105
|
+
class TodoStore {
|
|
1106
|
+
private store = create<TodoState>(...);
|
|
1107
|
+
|
|
1108
|
+
get useStore() {
|
|
1109
|
+
return this.store;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
addTodo(text: string) {
|
|
1113
|
+
this.store.setState(...);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
466
1116
|
```
|
|
467
1117
|
|
|
468
|
-
|
|
1118
|
+
### How does this compare to React Context?
|
|
1119
|
+
|
|
1120
|
+
| Feature | xInjection | React Context |
|
|
1121
|
+
| ------------------------------- | ---------- | ------------- |
|
|
1122
|
+
| Automatic dependency resolution | ✅ | ❌ |
|
|
1123
|
+
| Component-scoped instances | ✅ | ❌ |
|
|
1124
|
+
| No provider hell | ✅ | ❌ |
|
|
1125
|
+
| Parent-child dependency control | ✅ | ❌ |
|
|
1126
|
+
| Works with class-based logic | ✅ | ❌ |
|
|
1127
|
+
| Testability | ✅ | ⚠️ |
|
|
1128
|
+
| TypeScript support | ✅ | ⚠️ |
|
|
1129
|
+
|
|
1130
|
+
### If I want Angular patterns, why not just use Angular?
|
|
1131
|
+
|
|
1132
|
+
Because you want React's component model, hooks, and ecosystem — but need better architecture for complex business logic. xInjection brings IoC/DI to React without the framework lock-in.
|
|
1133
|
+
|
|
1134
|
+
That said, if your app is simple, React Context + hooks is perfectly fine. xInjection shines in larger codebases with complex business logic, many modules, or a need for component-scoped service instances.
|
|
1135
|
+
|
|
1136
|
+
### Can I migrate gradually from an existing React app?
|
|
1137
|
+
|
|
1138
|
+
Absolutely! Start with one component:
|
|
1139
|
+
|
|
1140
|
+
1. Extract business logic into a service
|
|
1141
|
+
2. Create a module for that service
|
|
1142
|
+
3. Wrap the component with `provideModuleToComponent`
|
|
1143
|
+
|
|
1144
|
+
You can use xInjection alongside Context, Redux, or any other state management.
|
|
1145
|
+
|
|
1146
|
+
### When do I actually need `provideModuleToComponent`?
|
|
1147
|
+
|
|
1148
|
+
**Don't need it (just use `useInject`):** All your services are global/singleton — API client, auth service, theme service.
|
|
1149
|
+
|
|
1150
|
+
**Need it:** You want multiple independent component instances (forms, modals, dialogs), or parent needs to control child dependencies via the `inject` prop.
|
|
1151
|
+
|
|
1152
|
+
See [Why Use the HoC Approach?](#why-use-the-hoc-approach) for a full explanation.
|
|
1153
|
+
|
|
1154
|
+
### What's the performance impact?
|
|
1155
|
+
|
|
1156
|
+
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.
|
|
1157
|
+
|
|
1158
|
+
**Runtime vs Build-time:** This library works entirely at runtime (not build-time):
|
|
1159
|
+
|
|
1160
|
+
- Runtime DI is more flexible (dynamic module loading, testing)
|
|
1161
|
+
- Performance impact is negligible (container operations are fast)
|
|
1162
|
+
- You get runtime debugging and introspection
|
|
1163
|
+
- Works with all bundlers/tools without special configuration
|
|
1164
|
+
|
|
1165
|
+
### Why use classes for services instead of custom hooks?
|
|
1166
|
+
|
|
1167
|
+
Both approaches work! Here's when classes shine:
|
|
1168
|
+
|
|
1169
|
+
**Classes are better for:**
|
|
1170
|
+
|
|
1171
|
+
- Complex business logic (multiple methods, private state)
|
|
1172
|
+
- Dependency injection (automatic wiring)
|
|
1173
|
+
- Testing (easier to mock)
|
|
1174
|
+
- Encapsulation (private members, getters/setters)
|
|
1175
|
+
|
|
1176
|
+
**Hooks are better for:**
|
|
1177
|
+
|
|
1178
|
+
- Simple component logic
|
|
1179
|
+
- React-specific features (useState, useEffect)
|
|
1180
|
+
- Functional programming style
|
|
1181
|
+
|
|
1182
|
+
**You can use both!** Use classes for services, hooks for UI logic. The `hookFactory` even lets you create hooks that inject class-based services.
|
|
1183
|
+
|
|
1184
|
+
**Note:** Services are classes, but components are still functional! You write normal React functional components with hooks—only the business logic is in classes.
|
|
1185
|
+
|
|
1186
|
+
## Links
|
|
469
1187
|
|
|
470
1188
|
📚 **Full API Documentation:** [https://adimarianmutu.github.io/x-injection-reactjs](https://adimarianmutu.github.io/x-injection-reactjs/index.html)
|
|
471
1189
|
|
|
472
|
-
|
|
1190
|
+
🔧 **Base Library:** [xInjection](https://github.com/AdiMarianMutu/x-injection)
|
|
1191
|
+
|
|
1192
|
+
💡 **Issues & Feature Requests:** [GitHub Issues](https://github.com/AdiMarianMutu/x-injection-reactjs/issues)
|
|
473
1193
|
|
|
474
1194
|
## Contributing
|
|
475
1195
|
|
|
476
|
-
|
|
1196
|
+
Contributions are welcome! Please:
|
|
1197
|
+
|
|
1198
|
+
1. Fork the repository
|
|
1199
|
+
2. Create a feature branch
|
|
1200
|
+
3. Make your changes
|
|
1201
|
+
4. Add tests
|
|
1202
|
+
5. Submit a pull request
|
|
1203
|
+
|
|
1204
|
+
Please ensure your code follows the project's style and all tests pass.
|
|
477
1205
|
|
|
478
1206
|
## License
|
|
479
1207
|
|
|
@@ -481,4 +1209,4 @@ MIT © [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
|
|
|
481
1209
|
|
|
482
1210
|
---
|
|
483
1211
|
|
|
484
|
-
|
|
1212
|
+
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)!
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "https://github.com/AdiMarianMutu/x-injection-reactjs"
|
|
6
6
|
},
|
|
7
7
|
"description": "ReactJS integration of the `xInjection` library.",
|
|
8
|
-
"version": "1.0.
|
|
8
|
+
"version": "1.0.6",
|
|
9
9
|
"author": "Adi-Marian Mutu",
|
|
10
10
|
"homepage": "https://github.com/AdiMarianMutu/x-injection-reactjs#readme",
|
|
11
11
|
"bugs": "https://github.com/AdiMarianMutu/x-injection-reactjs/issues",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"v:bump-major": "npm version major -m \"chore: update lib major version %s\""
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@adimm/x-injection": "^3.0.
|
|
45
|
+
"@adimm/x-injection": "^3.0.4",
|
|
46
46
|
"react": ">=18.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|