@ts-ioc-container/react 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,67 @@
1
+ # @ts-ioc-container/react — guide for AI coding agents
2
+
3
+ React bindings for [`ts-ioc-container`](https://www.npmjs.com/package/ts-ioc-container).
4
+ This file ships inside the npm package, so it matches the installed version.
5
+ For the container itself, read `node_modules/ts-ioc-container/AGENTS.md` first.
6
+
7
+ ## API
8
+
9
+ | Export | What it does |
10
+ | --- | --- |
11
+ | `ScopeContext` | React context holding the current `IContainer`. Put the root container in it with `ScopeContext.Provider` |
12
+ | `Scope` | `<Scope tags={[...]}>`: creates a child scope for its subtree once and disposes it on unmount |
13
+ | `useScope()` | The surrounding scope, or `null` |
14
+ | `useScopeOrFail()` | The surrounding scope; throws `OutOfScopeError` when there is none |
15
+ | `useResolveOrFail(tokenOrClass)` | Resolves a token (`token.resolve(scope)`) or a class from the surrounding scope |
16
+ | `OutOfScopeError` | Thrown outside a scope. `code: 'IOC_OUT_OF_SCOPE'` |
17
+
18
+ ## Recipe
19
+
20
+ ```tsx
21
+ import 'reflect-metadata';
22
+ import { bindTo, Container, register, Registration as R, scope, singleton, SingleToken } from 'ts-ioc-container';
23
+ import { Scope, ScopeContext, useResolveOrFail } from '@ts-ioc-container/react';
24
+
25
+ interface IGreeter {
26
+ greet(): string;
27
+ }
28
+ const IGreeterToken = new SingleToken<IGreeter>('IGreeter');
29
+
30
+ // one instance per widget scope
31
+ @register(bindTo(IGreeterToken), scope((s) => s.hasTag('widget')), singleton())
32
+ class Greeter implements IGreeter {
33
+ greet() {
34
+ return 'hello';
35
+ }
36
+ }
37
+
38
+ const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Greeter));
39
+
40
+ function Greeting() {
41
+ const greeter = useResolveOrFail(IGreeterToken);
42
+ return <p>{greeter.greet()}</p>;
43
+ }
44
+
45
+ export function App() {
46
+ return (
47
+ <ScopeContext.Provider value={app}>
48
+ <Scope tags={['page']}>
49
+ <Scope tags={['widget']}>
50
+ <Greeting />
51
+ </Scope>
52
+ </Scope>
53
+ </ScopeContext.Provider>
54
+ );
55
+ }
56
+ ```
57
+
58
+ Frontend scope tags are `application` / `page` / `widget`.
59
+
60
+ ## Pitfalls
61
+
62
+ | Symptom | Cause | Fix |
63
+ | --- | --- | --- |
64
+ | `OutOfScopeError` | No `ScopeContext.Provider` above the component | Wrap the root in `<ScopeContext.Provider value={container}>` |
65
+ | `DependencyNotFoundError` in a component | The registration's `scope(...)` rule does not match any scope on the path | Add a `<Scope tags={[...]}>` with the matching tag, or relax the rule |
66
+ | Subtree uses a disposed scope in development | `StrictMode` unmounts and remounts effects, which disposes the `Scope`'s child | Render `Scope` outside `StrictMode` |
67
+ | Registration added later is missing | Child scopes copy registrations when they are created | Register on the root container before rendering |
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OutOfScopeError = void 0;
4
4
  class OutOfScopeError extends Error {
5
5
  name = 'OutOfScopeError';
6
+ code = 'IOC_OUT_OF_SCOPE';
6
7
  constructor(message) {
7
8
  super(message);
8
9
  Object.setPrototypeOf(this, OutOfScopeError.prototype);
@@ -16,7 +16,7 @@ function useScope() {
16
16
  function useScopeOrFail() {
17
17
  const container = useScope();
18
18
  if (!container) {
19
- throw new OutOfScopeError_1.OutOfScopeError('useScopeOrFail must be called inside ScopeContext.Provider');
19
+ throw new OutOfScopeError_1.OutOfScopeError('useScopeOrFail must be called inside ScopeContext.Provider: wrap the tree in <ScopeContext.Provider value={container}>.');
20
20
  }
21
21
  return container;
22
22
  }
@@ -1,5 +1,6 @@
1
1
  export class OutOfScopeError extends Error {
2
2
  name = 'OutOfScopeError';
3
+ code = 'IOC_OUT_OF_SCOPE';
3
4
  constructor(message) {
4
5
  super(message);
5
6
  Object.setPrototypeOf(this, OutOfScopeError.prototype);
@@ -9,7 +9,7 @@ export function useScope() {
9
9
  export function useScopeOrFail() {
10
10
  const container = useScope();
11
11
  if (!container) {
12
- throw new OutOfScopeError('useScopeOrFail must be called inside ScopeContext.Provider');
12
+ throw new OutOfScopeError('useScopeOrFail must be called inside ScopeContext.Provider: wrap the tree in <ScopeContext.Provider value={container}>.');
13
13
  }
14
14
  return container;
15
15
  }
@@ -0,0 +1,10 @@
1
+ export class OutOfScopeError extends Error {
2
+ name = 'OutOfScopeError';
3
+ readonly code = 'IOC_OUT_OF_SCOPE';
4
+
5
+ constructor(message: string) {
6
+ super(message);
7
+
8
+ Object.setPrototypeOf(this, OutOfScopeError.prototype);
9
+ }
10
+ }
@@ -0,0 +1,65 @@
1
+ import { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
2
+ import { type constructor, type IContainer, InjectionToken } from 'ts-ioc-container';
3
+
4
+ import { OutOfScopeError } from './OutOfScopeError';
5
+
6
+ export const ScopeContext = createContext<IContainer | null>(null);
7
+
8
+ /**
9
+ * Reads the surrounding scope, or `null` when there is no `ScopeContext.Provider` above the caller.
10
+ */
11
+ export function useScope(): IContainer | null {
12
+ return useContext(ScopeContext);
13
+ }
14
+
15
+ /**
16
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
17
+ */
18
+ export function useScopeOrFail(): IContainer {
19
+ const container = useScope();
20
+ if (!container) {
21
+ throw new OutOfScopeError(
22
+ 'useScopeOrFail must be called inside ScopeContext.Provider: wrap the tree in <ScopeContext.Provider value={container}>.',
23
+ );
24
+ }
25
+ return container;
26
+ }
27
+
28
+ /**
29
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
30
+ */
31
+ export function useResolveOrFail<T>(key: InjectionToken<T> | constructor<T>): T {
32
+ const container = useScopeOrFail();
33
+ if (key instanceof InjectionToken) {
34
+ return key.resolve(container);
35
+ }
36
+ return container.resolve(key);
37
+ }
38
+
39
+ interface ScopeProps {
40
+ tags: string[];
41
+ children: ReactNode;
42
+ }
43
+
44
+ /**
45
+ * Creates a child scope for its subtree once, when the component instance is
46
+ * first created (lazy state init), and disposes it on unmount.
47
+ *
48
+ * NOTE: relies on the surrounding tree not being wrapped in `StrictMode`.
49
+ * StrictMode's dev-only mount/unmount/remount cycle runs this effect's
50
+ * cleanup (disposing the scope) and then re-runs the effect's setup without
51
+ * re-rendering the component, so there is no render pass left to create a
52
+ * replacement — the subtree would be left on a disposed scope. Render the
53
+ * app root outside `StrictMode`, or scope `StrictMode` to trees that don't
54
+ * mount `Scope`.
55
+ *
56
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
57
+ */
58
+ export function Scope(props: ScopeProps) {
59
+ const parent = useScopeOrFail();
60
+ const [child] = useState(() => parent.createScope({ tags: props.tags }));
61
+
62
+ useEffect(() => () => child.dispose(), [child]);
63
+
64
+ return <ScopeContext.Provider value={child}>{props.children}</ScopeContext.Provider>;
65
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Scope, ScopeContext, useResolveOrFail, useScope, useScopeOrFail } from './ScopeContext';
2
+ export { OutOfScopeError } from './OutOfScopeError';
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@ts-ioc-container/react",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "React bindings for ts-ioc-container: a Scope component that maps container scopes onto the component tree, plus hooks to resolve from the surrounding scope.",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "registry": "https://registry.npmjs.org/"
8
8
  },
9
9
  "author": "ibabkin <igba14@gmail.com>",
10
- "homepage": "https://igorbabkin.github.io/ts-ioc-container",
10
+ "homepage": "https://github.com/IgorBabkin/ts-ioc-container/tree/main/packages/react#readme",
11
11
  "license": "ISC",
12
12
  "sideEffects": false,
13
13
  "main": "cjm/index.js",
@@ -41,7 +41,9 @@
41
41
  "files": [
42
42
  "cjm/**/*",
43
43
  "esm/**/*",
44
- "typings/**/*"
44
+ "typings/**/*",
45
+ "lib/**/*",
46
+ "AGENTS.md"
45
47
  ],
46
48
  "repository": {
47
49
  "type": "git",
@@ -64,7 +66,7 @@
64
66
  "typescript": "5.8.3",
65
67
  "unplugin-swc": "^1.5.9",
66
68
  "vitest": "^4.1.11",
67
- "ts-ioc-container": "72.1.0"
69
+ "ts-ioc-container": "72.2.0"
68
70
  },
69
71
  "engines": {
70
72
  "node": ">= 18"
@@ -1,4 +1,6 @@
1
1
  export declare class OutOfScopeError extends Error {
2
2
  name: string;
3
+ readonly code = "IOC_OUT_OF_SCOPE";
3
4
  constructor(message: string);
4
5
  }
6
+ //# sourceMappingURL=OutOfScopeError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OutOfScopeError.d.ts","sourceRoot":"","sources":["../lib/OutOfScopeError.ts"],"names":[],"mappings":"AAAA,qBAAa,eAAgB,SAAQ,KAAK;IACxC,IAAI,SAAqB;IACzB,QAAQ,CAAC,IAAI,sBAAsB;gBAEvB,OAAO,EAAE,MAAM;CAK5B"}
@@ -1,12 +1,36 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type constructor, type IContainer, InjectionToken } from 'ts-ioc-container';
3
3
  export declare const ScopeContext: import("react").Context<IContainer | null>;
4
+ /**
5
+ * Reads the surrounding scope, or `null` when there is no `ScopeContext.Provider` above the caller.
6
+ */
4
7
  export declare function useScope(): IContainer | null;
8
+ /**
9
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
10
+ */
5
11
  export declare function useScopeOrFail(): IContainer;
12
+ /**
13
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
14
+ */
6
15
  export declare function useResolveOrFail<T>(key: InjectionToken<T> | constructor<T>): T;
7
16
  interface ScopeProps {
8
17
  tags: string[];
9
18
  children: ReactNode;
10
19
  }
20
+ /**
21
+ * Creates a child scope for its subtree once, when the component instance is
22
+ * first created (lazy state init), and disposes it on unmount.
23
+ *
24
+ * NOTE: relies on the surrounding tree not being wrapped in `StrictMode`.
25
+ * StrictMode's dev-only mount/unmount/remount cycle runs this effect's
26
+ * cleanup (disposing the scope) and then re-runs the effect's setup without
27
+ * re-rendering the component, so there is no render pass left to create a
28
+ * replacement — the subtree would be left on a disposed scope. Render the
29
+ * app root outside `StrictMode`, or scope `StrictMode` to trees that don't
30
+ * mount `Scope`.
31
+ *
32
+ * @throws {OutOfScopeError} when there is no `ScopeContext.Provider` above the caller.
33
+ */
11
34
  export declare function Scope(props: ScopeProps): import("react").JSX.Element;
12
35
  export {};
36
+ //# sourceMappingURL=ScopeContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScopeContext.d.ts","sourceRoot":"","sources":["../lib/ScopeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AACvF,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAIrF,eAAO,MAAM,YAAY,4CAAyC,CAAC;AAEnE;;GAEG;AACH,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAE5C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAQ3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAM9E;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,+BAOtC"}
@@ -1,2 +1,3 @@
1
1
  export { Scope, ScopeContext, useResolveOrFail, useScope, useScopeOrFail } from './ScopeContext.js';
2
2
  export { OutOfScopeError } from './OutOfScopeError.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}