@adimm/x-injection-reactjs 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,567 +1,527 @@
1
- <h1 align="center">
2
- xInjection ReactJS <a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank" alt="Release Version"><img src="https://badgen.net/npm/v/@adimm/x-injection-reactjs"></a>
3
- <img src="https://badgen.net/npm/license/@adimm/x-injection-reactjs" alt="License">
4
- <a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection-reactjs" target="__blank" alt="Release Version"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection-reactjs"></a>
5
- </h1>
6
-
7
- <p align="center">
8
- <a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml/badge.svg?branch=main"></a>
9
- <a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml/badge.svg"></a>
10
- <br>
11
- <img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection-reactjs">
12
- <a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank" alt="Monthly Downloads"><img src="https://badgen.net/npm/dm/@adimm/x-injection-reactjs"></a>
13
- </p>
14
-
15
- ## Table of Contents
16
-
17
- - [Table of Contents](#table-of-contents)
18
- - [Overview](#overview)
19
- - [Installation](#installation)
20
- - [TypeScript Configuration](#typescript-configuration)
21
- - [Getting Started](#getting-started)
22
- - [Component ProviderModules](#component-providermodules)
23
- - [Component Injection](#component-injection)
24
- - [Via anonymous function](#via-anonymous-function)
25
- - [Via named function](#via-named-function)
26
- - [Hook Injection](#hook-injection)
27
- - [Examples](#examples)
28
- - [Composable components](#composable-components)
29
- - [Common Mistakes](#common-mistakes)
30
- - [Global ComponentModule](#global-componentmodule)
31
- - [Unit Tests](#unit-tests)
32
- - [Documentation](#documentation)
33
- - [Contributing](#contributing)
34
-
35
- ## Overview
36
-
37
- **xInjection** is a robust Inversion of Control [(IoC)](https://en.wikipedia.org/wiki/Inversion_of_control) library that extends [InversifyJS](https://github.com/inversify/InversifyJS) with a modular, [NestJS](https://github.com/nestjs/nest)-inspired Dependency Injection [(DI)](https://en.wikipedia.org/wiki/Dependency_injection) system. It enables you to **encapsulate** dependencies with fine-grained control using **[ProviderModule](https://adimarianmutu.github.io/x-injection/classes/ProviderModule.html)** classes, allowing for clean **separation** of concerns and **scalable** architecture.
38
-
39
- Each `ProviderModule` manages its _own_ container, supporting easy **decoupling** and _explicit_ control over which providers are **exported** and **imported** across modules. The global **[AppModule](https://adimarianmutu.github.io/x-injection/variables/AppModule.html)** is always available, ensuring a seamless foundation for your application's DI needs.
40
-
41
- > For more details and info please access the [xInjection](https://github.com/AdiMarianMutu/x-injection) library repository.
42
-
43
- ## Installation
44
-
45
- First, ensure you have [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) installed:
46
-
47
- ```sh
48
- npm i reflect-metadata
49
- ```
50
-
51
- Then install `xInjection` for React:
52
-
53
- ```sh
54
- npm i @adimm/x-injection-reactjs
55
- ```
56
-
57
- > You may also have to install the parent library via `npm i @adimm/x-injection`
58
-
59
- ### TypeScript Configuration
60
-
61
- Add the following options to your `tsconfig.json` to enable decorator metadata:
62
-
63
- ```json
64
- {
65
- "compilerOptions": {
66
- "experimentalDecorators": true,
67
- "emitDecoratorMetadata": true
68
- }
69
- }
70
- ```
71
-
72
- ## Getting Started
73
-
74
- If you never used the parent library (`xInjection`), then please access the official [xInjection Repository](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#getting-started) to better understand how to use its `ReactJS` implementation.
75
-
76
- ### Component ProviderModules
77
-
78
- A [ComponentProviderModule](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html) isn't so different than the original [ProviderModule](https://adimarianmutu.github.io/x-injection/interfaces/IProviderModule.html) from the base `xInjection` library, the main difference being that it'll automatically create a [clone](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#clone) of itself whenever a component is `mounted` and during the `unmount` process it'll [dispose](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#dispose) itself.
79
-
80
- This is needed so:
81
-
82
- - Each instance of a component has its own instance of the `ProviderModule` _(also known as `ContextualizedModule`)_
83
- - Whenever a component is unmounted, the container of that `ContextualizedModule` is destroyed, making sure that the resources can be garbage-collected by the JS garbage collector.
84
-
85
- > **Note:** By default each `ContextualizedModule` has its `InjectionScope` set to `Singleton`, you can of course change it by providing the [defaultScope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#defaultscope) property.
86
-
87
- ### Component Injection
88
-
89
- In order to be able to inject dependencies into your components, you must first supply them with a `ComponentProviderModule`.
90
-
91
- This is how you can create one:
92
-
93
- ```ts
94
- @Injectable()
95
- export class UserService {
96
- firstName: string;
97
- lastName: string;
98
-
99
- generateFullName(): string {
100
- return `${firstName} ${lastName}`;
101
- }
102
- }
103
-
104
- export const UserComponentModule = new ComponentProviderModule({
105
- identifier: Symbol('UserComponentModule'),
106
- providers: [UserService],
107
- });
108
-
109
- interface UserInfoProps {
110
- firstName: string;
111
- lastName: string;
112
- }
113
- ```
114
-
115
- Now you have to actually provide the `UserComponentModule` to your component(s). You can do so with 2 different methods:
116
-
117
- #### Via anonymous function
118
-
119
- If you prefer to use the `const Component = () => {}` syntax, then you must use the [provideModuleToComponent](https://adimarianmutu.github.io/x-injection-reactjs/functions/provideModuleToComponent.html) method as shown below:
120
-
121
- > **Note:** _This is the preferred method as it allows you to avoid wrapping your component within another provider once created._
122
-
123
- ```tsx
124
- // The UserInfo component will correctly infer the interface of `UserInfoProps` automatically!
125
- export const UserInfo = provideModuleToComponent(UserComponentModule, ({ firstName, lastName }: UserInfoProps) => {
126
- const userService = useInject(UserService);
127
-
128
- userService.firstName = firstName;
129
- userService.lastName = lastName;
130
-
131
- return <p>Hello {userService.generateFullName()}!</p>;
132
- });
133
-
134
- function MyApp() {
135
- return <UserInfo firstName="John" lastName="Doe" />;
136
- // Result
137
- //
138
- // <p>Hello John Doe!</p>
139
- }
140
- ```
141
-
142
- #### Via named function
143
-
144
- Or if you prefer to use the `function Component() {}` syntax, then you must use the [ProvideModule](https://adimarianmutu.github.io/x-injection-reactjs/functions/ProvideModule.html) `HoC` as shown below:
145
-
146
- > **Note:** _If you need to access the contextualized `module` forwarded to your component, you can wrap the component props with the [PropsWithModule](https://adimarianmutu.github.io/x-injection-reactjs/types/PropsWithModule.html) generic type._
147
-
148
- ```tsx
149
- export function UserInfo({ firstName, lastName }: UserInfoProps) {
150
- const userService = useInject(UserService);
151
-
152
- userService.firstName = firstName;
153
- userService.lastName = lastName;
154
-
155
- return <p>Hello {userService.generateFullName()}!</p>;
156
- }
157
-
158
- function MyApp() {
159
- return (
160
- <ProvideModule module={UserComponentModule}>
161
- <UserInfo firstName="John" lastName="Doe" />
162
- </ProvideModule>
163
- );
164
- // Result
165
- //
166
- // <p>Hello John Doe!</p>
167
- }
168
- ```
169
-
170
- That's all you need to do, at least for simple components 😃.
171
-
172
- > You can find more complex examples at the [Examples](#examples) section.
173
-
174
- ### Hook Injection
175
-
176
- You already have seen in action the low-level [useInject](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInject.html) hook _(take a look also at the [useInjectMany](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInjectMany.html) hook)_. It is quite useful when you just have to inject quickly some dependencies into a component quite simple.
177
-
178
- What it does under the hood? Finds the nearest contextualized module and resolves from it the required dependencies into your component, that's all.
179
-
180
- But, as your UI will grow, you'll soon discover that you may inject more dependencies into a component, or even in multiple components, therefore you'll end up writing a lot of duplicated code, well, as per the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#:~:text=%22Don't%20repeat%20yourself%22,redundancy%20in%20the%20first%20place.) principle, that's not good! 🥲
181
-
182
- This means that we can actually use the [hookFactory](https://adimarianmutu.github.io/x-injection-reactjs/functions/hookFactory.html) method to compose a _custom_ hook with access to any dependency available in the component contextualized module.
183
-
184
- Having the above examples with the `UserService`, we'll create a custom `generateFullName` hook.
185
-
186
- ```ts
187
- // The `HookWithDeps` generic type will help
188
- // in making sure that the `useGenerateUserFullName` hooks params are correctly visible.
189
- // The 1st generic param must be the hook params (Like `UserInfoProps`)
190
- // and starting from the 2nd generic param you must provide the type of your dependencies.
191
- const useGenerateUserFullName = hookFactory({
192
- // The `use` property is where you write your hook implementation.
193
- use: ({ firstName, lastName, deps: [userService] }: HookWithDeps<UserInfoProps, UserService>) => {
194
- userService.firstName = firstName;
195
- userService.lastName = lastName;
196
-
197
- return userService.generateFullName();
198
- },
199
- // The `inject` array is very important,
200
- // here we basically specify which dependencies should be injected into the custom hook.
201
- // Also, keep in mind that the order of the `inject` array matters, the order of the `deps` prop
202
- // is determined by the order of the `inject` array!
203
- inject: [UserService],
204
- });
205
- ```
206
-
207
- Now you can use it in inside any component which has access to a contextualized module which can provide the `UserService`.
208
-
209
- ```tsx
210
- export function UserInfo({ firstName, lastName }: UserInfoProps) {
211
- const userFullName = useGenerateFullName({ firstName, lastName });
212
-
213
- return <p>Hello {userFullName}!</p>;
214
- }
215
- ```
216
-
217
- ## Examples
218
-
219
- ### Composable components
220
-
221
- In a real world scenario, you'll definitely have custom components which render other custom components and so on... _(like a [Matryoshka doll](https://en.wikipedia.org/wiki/Matryoshka_doll))_
222
-
223
- So you may find yourself wanting to be able to control a dependency/service of a child component from a parent component, with `xInjection` this is very easy to achieve thanks to the `ProviderModule` architecture, because each `module` can `import` and `export` other dependencies _(or modules)_ it fits in perfectly within the [declarative programming](https://en.wikipedia.org/wiki/Declarative_programming) world!
224
-
225
- In this example, we'll build 4 components, each with its own purpose. However, the `autocomplete` component will be the one capable of accessing the services of all of them.
226
-
227
- - An `inputbox`
228
- - A `list viewer`
229
- - A `dropdown`
230
- - An `autocomplete`
231
-
232
- <hr>
233
-
234
- > Inputbox
235
-
236
- `inputbox.service.ts`
237
-
238
- ```ts
239
- @Injectable()
240
- export class InputboxService {
241
- currentValue = '';
242
-
243
- // We'll initialize this soon enough.
244
- setStateValue!: (newValue: string) => void;
245
-
246
- /** Can be used to update the {@link currentValue} of the `inputbox`. */
247
- setValue(newValue: string): void {
248
- this.currentValue = newValue;
249
-
250
- this.setStateValue(this.currentValue);
251
- }
252
- }
253
-
254
- export const InputboxModule = new ComponentProviderModule({
255
- identifier: Symbol('InputboxModule'),
256
- provides: [InputboxService],
257
- exports: [InputboxService],
258
- });
259
- ```
260
-
261
- `inputbox.tsx`
262
-
263
- ```tsx
264
- export interface InputboxProps {
265
- initialValue: string;
266
- }
267
-
268
- export const Inputbox = provideModuleToComponent(InputboxModule, ({ initialValue }: InputboxProps) => {
269
- const service = useInject(InputboxService);
270
- const [, setCurrentValue] = useState(initialValue);
271
- service.setStateValue = setCurrentValue;
272
-
273
- useEffect(() => {
274
- service.currentValue = initialValue;
275
- }, [initialValue]);
276
-
277
- return <input value={service.currentValue} onChange={(e) => service.setValue(e.currentTarget.value)} />;
278
- });
279
- ```
280
-
281
- <hr>
282
-
283
- > Listview
284
-
285
- `listview.service.ts`
286
-
287
- ```ts
288
- @Injectable()
289
- export class ListviewService {
290
- items = [];
291
-
292
- /* Remaining fancy implementation */
293
- }
294
-
295
- export const ListviewModule = new ComponentProviderModule({
296
- identifier: Symbol('ListviewModule'),
297
- provides: [ListviewService],
298
- exports: [ListviewService],
299
- });
300
- ```
301
-
302
- `listview.tsx`
303
-
304
- ```tsx
305
- export interface ListviewProps {
306
- items: any[];
307
- }
308
-
309
- export const Listview = provideModuleToComponent(ListviewModule, ({ items }: ListviewProps) => {
310
- const service = useInject(ListviewService);
311
-
312
- /* Remaining fancy implementation */
313
-
314
- return (
315
- <div>
316
- {service.items.map((item) => (
317
- <span key={item}>{item}</span>
318
- ))}
319
- </div>
320
- );
321
- });
322
- ```
323
-
324
- <hr>
325
-
326
- > Dropdown
327
-
328
- Now keep close attention to how we implement the `Dropdown` component, as it'll actually be the _parent_ controlling the `Listview` component own service.
329
-
330
- `dropdown.service.ts`
331
-
332
- ```ts
333
- @Injectable()
334
- export class DropdownService {
335
- constructor(readonly listviewService: ListviewService) {
336
- // We can already take control of the children `ListviewService`!
337
- this.listviewService.items = [1, 2, 3, 4, 5];
338
- }
339
-
340
- /* Remaining fancy implementation */
341
- }
342
-
343
- export const DropdownModule = new ComponentProviderModule({
344
- identifier: Symbol('DropdownModule'),
345
- // It is very important that we import all the exportable dependencies from the `ListviewModule`!
346
- imports: [ListviewModule],
347
- provides: [DropdownService],
348
- exports: [
349
- // Let's also re-export the dependencies of the `ListviewModule` so once we import the `DropdownModule`
350
- // somewhere elese, we get access to the `ListviewModule` exported dependencies as well!
351
- ListviewModule,
352
- // Let's not forget to also export our `DropdownService` :)
353
- DropdownService,
354
- ],
355
- });
356
- ```
357
-
358
- `dropdown.tsx`
359
-
360
- ```tsx
361
- export interface DropdownProps {
362
- listviewProps: ListviewProps;
363
-
364
- initialSelectedValue: number;
365
- }
366
-
367
- export const Dropdown = provideModuleToComponent(
368
- ListviewModule,
369
- ({
370
- listviewProps,
371
- initialSelectedValue,
372
- // Here it is important that we get access to the contextualized module
373
- // so we can forward it to the `Listview` component!
374
- module,
375
- }: DropdownProps) => {
376
- const service = useInject(DropdownService);
377
-
378
- /* Remaining fancy implementation */
379
-
380
- return (
381
- <div className="fancy-dropdown">
382
- <span>{initialSelectedValue}</span>
383
-
384
- {/* Here we forward the contextualized module which will be sent from the parent component consuming this component,
385
- in our case, the `Autocomplete` component. */}
386
- <Listview module={module} />
387
- </div>
388
- );
389
- }
390
- );
391
- ```
392
-
393
- <hr>
394
-
395
- > Autocomplete
396
-
397
- And finally the grand finale!
398
-
399
- `autocomplete.service.ts`
400
-
401
- ```ts
402
- @Injectable()
403
- export class AutocompleteService {
404
- constructor(
405
- readonly inputboxService: InputboxService,
406
- readonly dropdownService: DropdownService
407
- ) {
408
- // Here we can override even what the `Dropdown` has already overriden!
409
- this.dropdownService.listviewService.items = [29, 9, 1969];
410
-
411
- // However doing the following, will throw an error because the `Inputbox` component
412
- // at this time is not yet mounted, therefore the `setStateValue` state setter
413
- // method doesn't exist yet.
414
- //
415
- // A better way would be to use a store manager so you can generate your application state through
416
- // the services, rather than inside the UI (components should be used only to render the data, not to manipulate/manage it).
417
- this.inputboxService.setValue('xInjection');
418
- }
419
-
420
- /* Remaining fancy implementation */
421
- }
422
-
423
- export const AutocompleteModule = new ComponentProviderModule({
424
- identifier: Symbol('AutocompleteModule'),
425
- imports: [InputboxModule, DropdownModule],
426
- provides: [AutocompleteService],
427
- // If we don't plan to share the internal dependencies of the
428
- // Autocomplete component, then we can omit the `exports` array declaration.
429
- });
430
- ```
431
-
432
- `autocomplete.tsx`
433
-
434
- ```tsx
435
- export interface AutocompleteProps {
436
- inputboxProps: InputboxProps;
437
- dropdownProps: DropdownProps;
438
-
439
- currentText: string;
440
- }
441
-
442
- export const Autocomplete = provideModuleToComponent(
443
- AutocompleteModule,
444
- ({
445
- dropdownProps,
446
- currentText,
447
-
448
- module,
449
- }: AutocompleteProps) => {
450
- const service = useInject(AutocompleteService);
451
-
452
- console.log(service.dropdownService.listviewService.items);
453
- // Produces: [29, 9, 1969]
454
-
455
- /* Remaining fancy implementation */
456
-
457
- return (
458
- <div className="fancy-autocomplete">
459
- {/* Let's not forget to forward the module to both components we want to control */}
460
- <Inputbox {...inputboxProps} module={module} >
461
- <Dropdown {...dropdownProps} module={module} />
462
- </div>
463
- );
464
- }
465
- );
466
- ```
467
-
468
- This should cover the fundamentals of how you can build a scalable UI by using the `xInjection` Dependency Injection 😊
469
-
470
- > **Note:** _Keep in mind that both library ([xInjection](https://www.npmjs.com/package/@adimm/x-injection) & [xInjection ReactJS](https://www.npmjs.com/package/@adimm/x-injection-reactjs)) are still young and being developed, therefore the internals and public API may change in the near future._
471
-
472
- ## Common Mistakes
473
-
474
- ### Global ComponentModule
475
-
476
- When creating a _global_ component module, you may do a subtle mistake which may lead to an unexpected "bug", to better understand see the below example:
477
-
478
- ```tsx
479
- const ModalModule = new ComponentProviderModule({
480
- identifier: Symbol('ModalModule'),
481
- markAsGlobal: true,
482
- provides: [ModalService],
483
- exports: [ModalService],
484
- });
485
-
486
- const ModalComponent = provideModuleToComponent(ModalModule, () => {
487
- const modalService = useInject(ModalService);
488
- });
489
- ```
490
-
491
- The mistake here is wrapping the `ModalComponent` within a module provider _(either the `provideModuleToComponent` HOF or the `ProvideModule` HOC)_, because when you'll use the `useInject` hook, it'll resolve from the _contextualized_ `ModalModule` rather than from the `AppModule`, therefore the resolved `modalService` within the `ModalComponent` will not be the same instance as the one which has been resolved directly from the `AppModule`!
492
-
493
- The correct code:
494
-
495
- ```tsx
496
- const ModalModule = new ComponentProviderModule({
497
- identifier: Symbol('ModalModule'),
498
- markAsGlobal: true,
499
- provides: [ModalService],
500
- exports: [ModalService],
501
- });
502
-
503
- const ModalComponent = () => {
504
- const modalService = useInject(ModalService);
505
- };
506
- ```
507
-
508
- Now the `useInject` hook will correctly resolve the `ModalService` from the `AppModule` instead.
509
-
510
- ## Unit Tests
511
-
512
- It is very easy to create mock modules so you can provide them to your components in your unit tests.
513
-
514
- ```tsx
515
- class ApiService {
516
- constructor(private readonly userService: UserService) {}
517
-
518
- async sendRequest<T>(location: LocationParams): Promise<T> {
519
- // Pseudo Implementation
520
- return this.sendToLocation(user, location);
521
- }
522
-
523
- private async sendToLocation(user: User, location: any): Promise<any> {}
524
- }
525
-
526
- const ApiModule = new ComponentProviderModule({
527
- identifier: Symbol('ApiModule'),
528
- providers: [UserService, ApiService],
529
- });
530
-
531
- const ApiModuleMocked = new ComponentProviderModule({
532
- identifier: Symbol('ApiModule_MOCK'),
533
- providers: [
534
- {
535
- provide: UserService,
536
- useClass: UserService_Mock,
537
- },
538
- {
539
- provide: ApiService,
540
- useValue: {
541
- sendRequest: async (location) => {
542
- console.log(location);
543
- },
544
- },
545
- },
546
- ],
547
- });
548
-
549
- // Now all the dependencies used inside the `RealComponent` will be automatically resolved from the `ApiModuleMocked` component module.
550
- await act(async () => render(<RealComponent module={ApiModuleMocked} />));
551
- ```
552
-
553
- ## Documentation
554
-
555
- Comprehensive, auto-generated documentation is available at:
556
-
557
- 👉 [https://adimarianmutu.github.io/x-injection-reactjs/index.html](https://adimarianmutu.github.io/x-injection-reactjs/index.html)
558
-
559
- ## Contributing
560
-
561
- Pull requests are warmly welcomed! 😃
562
-
563
- Please ensure your contributions adhere to the project's code style. See the repository for more details.
564
-
565
- ---
566
-
567
- > For questions, feature requests, or bug reports, feel free to open an [issue](https://github.com/AdiMarianMutu/x-injection-reactjs/issues) on GitHub!
1
+ <h1 align="center">
2
+ xInjection ReactJS <a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank"><img src="https://badgen.net/npm/v/@adimm/x-injection-reactjs"></a>
3
+ <img src="https://badgen.net/npm/license/@adimm/x-injection-reactjs">
4
+ <a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection-reactjs" target="__blank"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection-reactjs"></a>
5
+ </h1>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml/badge.svg?branch=main"></a>
9
+ <a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml/badge.svg"></a>
10
+ <br>
11
+ <img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection-reactjs">
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
+ </p>
14
+
15
+ ## Table of Contents
16
+
17
+ - [Table of Contents](#table-of-contents)
18
+ - [Overview](#overview)
19
+ - [Installation](#installation)
20
+ - [TypeScript Configuration](#typescript-configuration)
21
+ - [Getting Started](#getting-started)
22
+ - [Component ProviderModules](#component-providermodules)
23
+ - [Component Injection](#component-injection)
24
+ - [Via anonymous function](#via-anonymous-function)
25
+ - [Via named function](#via-named-function)
26
+ - [Hook Injection](#hook-injection)
27
+ - [Examples](#examples)
28
+ - [Composable components](#composable-components)
29
+ - [Unit Tests](#unit-tests)
30
+ - [Documentation](#documentation)
31
+ - [Contributing](#contributing)
32
+
33
+ ## Overview
34
+
35
+ **xInjection** is a robust Inversion of Control [(IoC)](https://en.wikipedia.org/wiki/Inversion_of_control) library that extends [InversifyJS](https://github.com/inversify/InversifyJS) with a modular, [NestJS](https://github.com/nestjs/nest)-inspired Dependency Injection [(DI)](https://en.wikipedia.org/wiki/Dependency_injection) system. It enables you to **encapsulate** dependencies with fine-grained control using **[ProviderModule](https://adimarianmutu.github.io/x-injection/classes/ProviderModule.html)** classes, allowing for clean **separation** of concerns and **scalable** architecture.
36
+
37
+ Each `ProviderModule` manages its _own_ container, supporting easy **decoupling** and _explicit_ control over which providers are **exported** and **imported** across modules. The global **[AppModule](https://adimarianmutu.github.io/x-injection/variables/AppModule.html)** is always available, ensuring a seamless foundation for your application's DI needs.
38
+
39
+ > For more details and info please access the [xInjection](https://github.com/AdiMarianMutu/x-injection) library repository.
40
+
41
+ ## Installation
42
+
43
+ First, ensure you have [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) installed:
44
+
45
+ ```sh
46
+ npm i reflect-metadata
47
+ ```
48
+
49
+ Then install `xInjection` for React:
50
+
51
+ ```sh
52
+ npm i @adimm/x-injection-reactjs
53
+ ```
54
+
55
+ > You may also have to install the parent library via `npm i @adimm/x-injection`
56
+
57
+ ### TypeScript Configuration
58
+
59
+ Add the following options to your `tsconfig.json` to enable decorator metadata:
60
+
61
+ ```json
62
+ {
63
+ "compilerOptions": {
64
+ "experimentalDecorators": true,
65
+ "emitDecoratorMetadata": true
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Getting Started
71
+
72
+ If you never used the parent library (`xInjection`), then please access the official [xInjection Repository](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#getting-started) to better understand how to use its `ReactJS` implementation.
73
+
74
+ ### Component ProviderModules
75
+
76
+ A [ComponentProviderModule](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html) isn't so different than the original [ProviderModule](https://adimarianmutu.github.io/x-injection/interfaces/IProviderModule.html) from the base `xInjection` library, the main difference being that it'll automatically create a [clone](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#clone) of itself whenever a component is `mounted` and during the `unmount` process it'll [dispose](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#dispose) itself.
77
+
78
+ This is needed so:
79
+
80
+ - Each instance of a component has its own instance of the `ProviderModule` _(also known as `ContextualizedModule`)_
81
+ - Whenever a component is unmounted, the container of that `ContextualizedModule` is destroyed, making sure that the resources can be garbage-collected by the JS garbage collector.
82
+
83
+ > **Note:** By default each `ContextualizedModule` has its `InjectionScope` set to `Singleton`, you can of course change it by providing the [defaultScope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#defaultscope) property.
84
+
85
+ ### Component Injection
86
+
87
+ In order to be able to inject dependencies into your components, you must first supply them with a `ComponentProviderModule`.
88
+
89
+ This is how you can create one:
90
+
91
+ ```ts
92
+ @Injectable()
93
+ export class UserService {
94
+ firstName: string;
95
+ lastName: string;
96
+
97
+ generateFullName(): string {
98
+ return `${firstName} ${lastName}`;
99
+ }
100
+ }
101
+
102
+ export const UserComponentModule = new ComponentProviderModule({
103
+ identifier: Symbol('UserComponentModule'),
104
+ providers: [UserService],
105
+ });
106
+
107
+ interface UserInfoProps {
108
+ firstName: string;
109
+ lastName: string;
110
+ }
111
+ ```
112
+
113
+ Now you have to actually provide the `UserComponentModule` to your component(s). You can do so with 2 different methods:
114
+
115
+ #### Via anonymous function
116
+
117
+ If you prefer to use the `const Component = () => {}` syntax, then you must use the [provideModuleToComponent](https://adimarianmutu.github.io/x-injection-reactjs/functions/provideModuleToComponent.html) method as shown below:
118
+
119
+ > **Note:** _This is the preferred method as it allows you to avoid wrapping your component within another provider once created._
120
+
121
+ ```tsx
122
+ // The UserInfo component will correctly infer the interface of `UserInfoProps` automatically!
123
+ export const UserInfo = provideModuleToComponent(UserComponentModule, ({ firstName, lastName }: UserInfoProps) => {
124
+ const userService = useInject(UserService);
125
+
126
+ userService.firstName = firstName;
127
+ userService.lastName = lastName;
128
+
129
+ return <p>Hello {userService.generateFullName()}!</p>;
130
+ });
131
+
132
+ function MyApp() {
133
+ return <UserInfo firstName="John" lastName="Doe" />;
134
+ // Result
135
+ //
136
+ // <p>Hello John Doe!</p>
137
+ }
138
+ ```
139
+
140
+ #### Via named function
141
+
142
+ Or if you prefer to use the `function Component() {}` syntax, then you must use the [ProvideModule](https://adimarianmutu.github.io/x-injection-reactjs/functions/ProvideModule.html) `HoC` as shown below:
143
+
144
+ > **Note:** _If you need to access the contextualized `module` forwarded to your component, you can wrap the component props with the [PropsWithModule](https://adimarianmutu.github.io/x-injection-reactjs/types/PropsWithModule.html) generic type._
145
+
146
+ ```tsx
147
+ export function UserInfo({ firstName, lastName }: UserInfoProps) {
148
+ const userService = useInject(UserService);
149
+
150
+ userService.firstName = firstName;
151
+ userService.lastName = lastName;
152
+
153
+ return <p>Hello {userService.generateFullName()}!</p>;
154
+ }
155
+
156
+ function MyApp() {
157
+ return (
158
+ <ProvideModule module={UserComponentModule}>
159
+ <UserInfo firstName="John" lastName="Doe" />
160
+ </ProvideModule>
161
+ );
162
+ // Result
163
+ //
164
+ // <p>Hello John Doe!</p>
165
+ }
166
+ ```
167
+
168
+ That's all you need to do, at least for simple components 😃.
169
+
170
+ > You can find more complex examples at the [Examples](#examples) section.
171
+
172
+ ### Hook Injection
173
+
174
+ You already have seen in action the low-level [useInject](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInject.html) hook _(take a look also at the [useInjectMany](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInjectMany.html) hook)_. It is quite useful when you just have to inject quickly some dependencies into a component quite simple.
175
+
176
+ What it does under the hood? Finds the nearest contextualized module and resolves from it the required dependencies into your component, that's all.
177
+
178
+ But, as your UI will grow, you'll soon discover that you may inject more dependencies into a component, or even in multiple components, therefore you'll end up writing a lot of duplicated code, well, as per the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#:~:text=%22Don't%20repeat%20yourself%22,redundancy%20in%20the%20first%20place.) principle, that's not good! 🥲
179
+
180
+ This means that we can actually use the [hookFactory](https://adimarianmutu.github.io/x-injection-reactjs/functions/hookFactory.html) method to compose a _custom_ hook with access to any dependency available in the component contextualized module.
181
+
182
+ Having the above examples with the `UserService`, we'll create a custom `generateFullName` hook.
183
+
184
+ ```ts
185
+ // The `HookWithDeps` generic type will help
186
+ // in making sure that the `useGenerateUserFullName` hooks params are correctly visible.
187
+ // The 1st generic param must be the hook params (Like `UserInfoProps`)
188
+ // and starting from the 2nd generic param you must provide the type of your dependencies.
189
+ const useGenerateUserFullName = hookFactory({
190
+ // The `use` property is where you write your hook implementation.
191
+ use: ({ firstName, lastName, deps: [userService] }: HookWithDeps<UserInfoProps, UserService>) => {
192
+ userService.firstName = firstName;
193
+ userService.lastName = lastName;
194
+
195
+ return userService.generateFullName();
196
+ },
197
+ // The `inject` array is very important,
198
+ // here we basically specify which dependencies should be injected into the custom hook.
199
+ // Also, keep in mind that the order of the `inject` array matters, the order of the `deps` prop
200
+ // is determined by the order of the `inject` array!
201
+ inject: [UserService],
202
+ });
203
+ ```
204
+
205
+ Now you can use it in inside any component which has access to a contextualized module which can provide the `UserService`.
206
+
207
+ ```tsx
208
+ export function UserInfo({ firstName, lastName }: UserInfoProps) {
209
+ const userFullName = useGenerateFullName({ firstName, lastName });
210
+
211
+ return <p>Hello {userFullName}!</p>;
212
+ }
213
+ ```
214
+
215
+ ## Examples
216
+
217
+ ### Composable components
218
+
219
+ In a real world scenario, you'll definitely have custom components which render other custom components and so on... _(like a [Matryoshka doll](https://en.wikipedia.org/wiki/Matryoshka_doll))_
220
+
221
+ So you may find yourself wanting to be able to control a dependency/service of a child component from a parent component, with `xInjection` this is very easy to achieve thanks to the `ProviderModule` architecture, because each `module` can `import` and `export` other dependencies _(or modules)_ it fits in perfectly within the [declarative programming](https://en.wikipedia.org/wiki/Declarative_programming) world!
222
+
223
+ In this example, we'll build 4 components, each with its own purpose. However, the `autocomplete` component will be the one capable of accessing the services of all of them.
224
+
225
+ - An `inputbox`
226
+ - A `list viewer`
227
+ - A `dropdown`
228
+ - An `autocomplete`
229
+
230
+ <hr>
231
+
232
+ > Inputbox
233
+
234
+ `inputbox.service.ts`
235
+
236
+ ```ts
237
+ @Injectable()
238
+ export class InputboxService {
239
+ currentValue = '';
240
+
241
+ // We'll initialize this soon enough.
242
+ setStateValue!: (newValue: string) => void;
243
+
244
+ /** Can be used to update the {@link currentValue} of the `inputbox`. */
245
+ setValue(newValue: string): void {
246
+ this.currentValue = newValue;
247
+
248
+ this.setStateValue(this.currentValue);
249
+ }
250
+ }
251
+
252
+ export const InputboxModule = new ComponentProviderModule({
253
+ identifier: Symbol('InputboxModule'),
254
+ provides: [InputboxService],
255
+ exports: [InputboxService],
256
+ });
257
+ ```
258
+
259
+ `inputbox.tsx`
260
+
261
+ ```tsx
262
+ export interface InputboxProps {
263
+ initialValue: string;
264
+ }
265
+
266
+ export const Inputbox = provideModuleToComponent(InputboxModule, ({ initialValue }: InputboxProps) => {
267
+ const service = useInject(InputboxService);
268
+ const [, setCurrentValue] = useState(initialValue);
269
+ service.setStateValue = setCurrentValue;
270
+
271
+ useEffect(() => {
272
+ service.currentValue = initialValue;
273
+ }, [initialValue]);
274
+
275
+ return <input value={service.currentValue} onChange={(e) => service.setValue(e.currentTarget.value)} />;
276
+ });
277
+ ```
278
+
279
+ <hr>
280
+
281
+ > Listview
282
+
283
+ `listview.service.ts`
284
+
285
+ ```ts
286
+ @Injectable()
287
+ export class ListviewService {
288
+ items = [];
289
+
290
+ /* Remaining fancy implementation */
291
+ }
292
+
293
+ export const ListviewModule = new ComponentProviderModule({
294
+ identifier: Symbol('ListviewModule'),
295
+ provides: [ListviewService],
296
+ exports: [ListviewService],
297
+ });
298
+ ```
299
+
300
+ `listview.tsx`
301
+
302
+ ```tsx
303
+ export interface ListviewProps {
304
+ items: any[];
305
+ }
306
+
307
+ export const Listview = provideModuleToComponent(ListviewModule, ({ items }: ListviewProps) => {
308
+ const service = useInject(ListviewService);
309
+
310
+ /* Remaining fancy implementation */
311
+
312
+ return (
313
+ <div>
314
+ {service.items.map((item) => (
315
+ <span key={item}>{item}</span>
316
+ ))}
317
+ </div>
318
+ );
319
+ });
320
+ ```
321
+
322
+ <hr>
323
+
324
+ > Dropdown
325
+
326
+ Now keep close attention to how we implement the `Dropdown` component, as it'll actually be the _parent_ controlling the `Listview` component own service.
327
+
328
+ `dropdown.service.ts`
329
+
330
+ ```ts
331
+ @Injectable()
332
+ export class DropdownService {
333
+ constructor(readonly listviewService: ListviewService) {
334
+ // We can already take control of the children `ListviewService`!
335
+ this.listviewService.items = [1, 2, 3, 4, 5];
336
+ }
337
+
338
+ /* Remaining fancy implementation */
339
+ }
340
+
341
+ export const DropdownModule = new ComponentProviderModule({
342
+ identifier: Symbol('DropdownModule'),
343
+ // It is very important that we import all the exportable dependencies from the `ListviewModule`!
344
+ imports: [ListviewModule],
345
+ provides: [DropdownService],
346
+ exports: [
347
+ // Let's also re-export the dependencies of the `ListviewModule` so once we import the `DropdownModule`
348
+ // somewhere elese, we get access to the `ListviewModule` exported dependencies as well!
349
+ ListviewModule,
350
+ // Let's not forget to also export our `DropdownService` :)
351
+ DropdownService,
352
+ ],
353
+ });
354
+ ```
355
+
356
+ `dropdown.tsx`
357
+
358
+ ```tsx
359
+ export interface DropdownProps {
360
+ listviewProps: ListviewProps;
361
+
362
+ initialSelectedValue: number;
363
+ }
364
+
365
+ export const Dropdown = provideModuleToComponent(
366
+ ListviewModule,
367
+ ({
368
+ listviewProps,
369
+ initialSelectedValue,
370
+ // Here it is important that we get access to the contextualized module
371
+ // so we can forward it to the `Listview` component!
372
+ module,
373
+ }: DropdownProps) => {
374
+ const service = useInject(DropdownService);
375
+
376
+ /* Remaining fancy implementation */
377
+
378
+ return (
379
+ <div className="fancy-dropdown">
380
+ <span>{initialSelectedValue}</span>
381
+
382
+ {/* Here we forward the contextualized module which will be sent from the parent component consuming this component,
383
+ in our case, the `Autocomplete` component. */}
384
+ <Listview module={module} />
385
+ </div>
386
+ );
387
+ }
388
+ );
389
+ ```
390
+
391
+ <hr>
392
+
393
+ > Autocomplete
394
+
395
+ And finally the grand finale!
396
+
397
+ `autocomplete.service.ts`
398
+
399
+ ```ts
400
+ @Injectable()
401
+ export class AutocompleteService {
402
+ constructor(
403
+ readonly inputboxService: InputboxService,
404
+ readonly dropdownService: DropdownService
405
+ ) {
406
+ // Here we can override even what the `Dropdown` has already overriden!
407
+ this.dropdownService.listviewService.items = [29, 9, 1969];
408
+
409
+ // However doing the following, will throw an error because the `Inputbox` component
410
+ // at this time is not yet mounted, therefore the `setStateValue` state setter
411
+ // method doesn't exist yet.
412
+ //
413
+ // A better way would be to use a store manager so you can generate your application state through
414
+ // the services, rather than inside the UI (components should be used only to render the data, not to manipulate/manage it).
415
+ this.inputboxService.setValue('xInjection');
416
+ }
417
+
418
+ /* Remaining fancy implementation */
419
+ }
420
+
421
+ export const AutocompleteModule = new ComponentProviderModule({
422
+ identifier: Symbol('AutocompleteModule'),
423
+ imports: [InputboxModule, DropdownModule],
424
+ provides: [AutocompleteService],
425
+ // If we don't plan to share the internal dependencies of the
426
+ // Autocomplete component, then we can omit the `exports` array declaration.
427
+ });
428
+ ```
429
+
430
+ `autocomplete.tsx`
431
+
432
+ ```tsx
433
+ export interface AutocompleteProps {
434
+ inputboxProps: InputboxProps;
435
+ dropdownProps: DropdownProps;
436
+
437
+ currentText: string;
438
+ }
439
+
440
+ export const Autocomplete = provideModuleToComponent(
441
+ AutocompleteModule,
442
+ ({
443
+ dropdownProps,
444
+ currentText,
445
+
446
+ module,
447
+ }: AutocompleteProps) => {
448
+ const service = useInject(AutocompleteService);
449
+
450
+ console.log(service.dropdownService.listviewService.items);
451
+ // Produces: [29, 9, 1969]
452
+
453
+ /* Remaining fancy implementation */
454
+
455
+ return (
456
+ <div className="fancy-autocomplete">
457
+ {/* Let's not forget to forward the module to both components we want to control */}
458
+ <Inputbox {...inputboxProps} module={module} >
459
+ <Dropdown {...dropdownProps} module={module} />
460
+ </div>
461
+ );
462
+ }
463
+ );
464
+ ```
465
+
466
+ This should cover the fundamentals of how you can build a scalable UI by using the `xInjection` Dependency Injection 😊
467
+
468
+ > **Note:** _Keep in mind that both library ([xInjection](https://www.npmjs.com/package/@adimm/x-injection) & [xInjection ReactJS](https://www.npmjs.com/package/@adimm/x-injection-reactjs)) are still young and being developed, therefore the internals and public API may change in the near future._
469
+
470
+ ## Unit Tests
471
+
472
+ It is very easy to create mock modules so you can provide them to your components in your unit tests.
473
+
474
+ ```tsx
475
+ class ApiService {
476
+ constructor(private readonly userService: UserService) {}
477
+
478
+ async sendRequest<T>(location: LocationParams): Promise<T> {
479
+ // Pseudo Implementation
480
+ return this.sendToLocation(user, location);
481
+ }
482
+
483
+ private async sendToLocation(user: User, location: any): Promise<any> {}
484
+ }
485
+
486
+ const ApiModule = new ComponentProviderModule({
487
+ identifier: Symbol('ApiModule'),
488
+ providers: [UserService, ApiService],
489
+ });
490
+
491
+ const ApiModuleMocked = new ComponentProviderModule({
492
+ identifier: Symbol('ApiModule_MOCK'),
493
+ providers: [
494
+ {
495
+ provide: UserService,
496
+ useClass: UserService_Mock,
497
+ },
498
+ {
499
+ provide: ApiService,
500
+ useValue: {
501
+ sendRequest: async (location) => {
502
+ console.log(location);
503
+ },
504
+ },
505
+ },
506
+ ],
507
+ });
508
+
509
+ // Now all the dependencies used inside the `RealComponent` will be automatically resolved from the `ApiModuleMocked` component module.
510
+ await act(async () => render(<RealComponent module={ApiModuleMocked} />));
511
+ ```
512
+
513
+ ## Documentation
514
+
515
+ Comprehensive, auto-generated documentation is available at:
516
+
517
+ 👉 [https://adimarianmutu.github.io/x-injection-reactjs/index.html](https://adimarianmutu.github.io/x-injection-reactjs/index.html)
518
+
519
+ ## Contributing
520
+
521
+ Pull requests are warmly welcomed! 😃
522
+
523
+ Please ensure your contributions adhere to the project's code style. See the repository for more details.
524
+
525
+ ---
526
+
527
+ > For questions, feature requests, or bug reports, feel free to open an [issue](https://github.com/AdiMarianMutu/x-injection-reactjs/issues) on GitHub!