├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── workflow.yml
├── .gitignore
├── README.md
├── jest.config.ts
├── lib
└── package.json
├── package.json
├── rollup.config.ts
├── src
├── index.ts
├── injector-context.ts
├── inverse-of-control-container.tsx
├── use-inject.ts
└── use-once.ts
├── test
└── use-inject.spec.tsx
├── tsconfig.build.json
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "@typescript-eslint"
6 | ],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:prettier/recommended",
10 | "prettier",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "rules" : {
14 | "@typescript-eslint/no-var-requires": "off",
15 | "lines-between-class-members": ["error", "always"]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - name: Build & Test Lib
11 | uses: actions/setup-node@v3
12 | with:
13 | node-version: "16.x"
14 | - run: npm install
15 | - run: npm run build
16 | - run: npm test
17 | - name: Upload coverage to Codecov
18 | uses: codecov/codecov-action@v3
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /lib/dist
6 | /lib/README.md
7 | /integration-test/dist
8 | /tmp
9 | /out-tsc
10 |
11 | # dependencies
12 | /node_modules
13 |
14 | # profiling files
15 | chrome-profiler-events*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | package-lock.json
41 | yarn-error.log
42 | testem.log
43 | /typings
44 |
45 | # System Files
46 | .DS_Store
47 | Thumbs.db
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Hooks for injection-js
2 |
3 | 
4 | [](https://codecov.io/gh/vthinkxie/use-inject)
5 |
6 | React Hooks for [injection-js](https://github.com/mgechev/injection-js).
7 |
8 | ## Get Started
9 |
10 | ```shell
11 | npm install use-inject
12 | ```
13 |
14 | [Online Demo](https://stackblitz.com/edit/use-inject?file=App.tsx)
15 |
16 | ```tsx
17 | import * as React from 'react';
18 | import { Injectable } from 'injection-js';
19 | import { useInject, DIContainer } from 'use-inject';
20 | import 'reflect-metadata';
21 |
22 | abstract class LogService {
23 | name: string;
24 | }
25 |
26 | @Injectable()
27 | class LogServiceOneImpl implements LogService {
28 | name = 'log-one';
29 | }
30 |
31 | @Injectable()
32 | class LogServiceTwoImpl implements LogService {
33 | name = 'log-two';
34 | }
35 |
36 | @Injectable()
37 | class ClientService {
38 | public name: string;
39 | constructor(private logService: LogService) {
40 | this.name = `client-with-${this.logService.name}`;
41 | }
42 | }
43 |
44 | function Component() {
45 | const name = useInject(ClientService).name;
46 | return
{name}
;
47 | }
48 |
49 | export default function App() {
50 | return (
51 |
52 |
58 |
59 |
60 |
66 |
67 |
68 |
69 | );
70 | }
71 | ```
72 |
73 |
74 | ## License
75 | MIT
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@jest/types";
2 | import { defaults as tsjPreset } from "ts-jest/presets";
3 |
4 | const config: Config.InitialOptions = {
5 | testEnvironment: "node",
6 | collectCoverage: true,
7 | transform: {
8 | ...tsjPreset.transform,
9 | "/test/.*\\.spec\\.tsx$": [
10 | "ts-jest",
11 | {
12 | tsconfig: { emitDecoratorMetadata: true, experimentalDecorators: true },
13 | },
14 | ],
15 | },
16 | coverageReporters: ["lcov", "html"],
17 | testRegex: "/test/.*\\.spec\\.tsx$",
18 | collectCoverageFrom: ["src/**/*"],
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-inject",
3 | "version": "0.0.2",
4 | "description": "injection-js react hooks",
5 | "main": "dist/use-inject.umd.js",
6 | "module": "dist/use-inject.es5.js",
7 | "typings": "dist/index.d.ts",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/vthinkxie/use-inject.git"
11 | },
12 | "keywords": [
13 | "react",
14 | "hooks",
15 | "DI",
16 | "dependency",
17 | "injection",
18 | "dependency injection",
19 | "injector",
20 | "typescript"
21 | ],
22 | "author": "yadongxie",
23 | "license": "MIT",
24 | "devDependencies": {
25 | "react-test-renderer": "^18.2.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-inject",
3 | "description": "injection-js react hooks",
4 | "scripts": {
5 | "test": "jest",
6 | "prebuild": "rimraf lib/dist",
7 | "build": "tsc --build tsconfig.build.json && rollup -c rollup.config.ts && rimraf out-tsc && cp README.md ./lib"
8 | },
9 | "author": "yadongxie",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "@abraham/reflection": "^0.10.0",
13 | "@rollup/plugin-commonjs": "^22.0.2",
14 | "@rollup/plugin-typescript": "^8.5.0",
15 | "@types/jest": "^29.0.3",
16 | "@types/node": "^18.7.18",
17 | "@types/react": "^18.0.20",
18 | "@types/react-test-renderer": "^18.0.0",
19 | "@types/rollup-plugin-peer-deps-external": "^2.2.1",
20 | "@typescript-eslint/eslint-plugin": "^5.37.0",
21 | "@typescript-eslint/parser": "^5.37.0",
22 | "eslint": "8.22.0",
23 | "eslint-config-prettier": "^8.5.0",
24 | "eslint-plugin-prettier": "^4.2.1",
25 | "injection-js": "^2.4.0",
26 | "injection-js-transformer": "^0.0.3",
27 | "jest": "^29.0.3",
28 | "react": "^18.2.0",
29 | "react-test-renderer": "^18.2.0",
30 | "rimraf": "^3.0.2",
31 | "rollup": "^2.79.0",
32 | "rollup-plugin-peer-deps-external": "^2.2.4",
33 | "ts-jest": "^29.0.1",
34 | "ts-node": "^10.9.1",
35 | "typescript": "~4.8.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import commonjs from "@rollup/plugin-commonjs";
2 | import peerDepsExternal from "rollup-plugin-peer-deps-external";
3 | import typescript from "@rollup/plugin-typescript";
4 | const pkg = require("./lib/package.json");
5 |
6 | export default {
7 | input: `src/index.ts`,
8 | output: [
9 | {
10 | file: `./lib/${pkg.main}`,
11 | name: "useInject",
12 | format: "umd",
13 | sourcemap: false,
14 | },
15 | { file: `./lib/${pkg.module}`, format: "es", sourcemap: false },
16 | ],
17 | plugins: [
18 | peerDepsExternal(),
19 | typescript({
20 | tsconfig: "tsconfig.build.json",
21 | }),
22 | commonjs(),
23 | ],
24 | };
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./inverse-of-control-container";
2 | export * from "./use-inject";
3 | export * from "./injector-context";
4 |
--------------------------------------------------------------------------------
/src/injector-context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { Injector } from "injection-js";
3 |
4 | export const InjectorContext = createContext(Injector.NULL);
5 |
--------------------------------------------------------------------------------
/src/inverse-of-control-container.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useContext, ReactNode } from "react";
2 | import { useOnce } from "./use-once";
3 | import { Provider, ReflectiveInjector, Injector } from "injection-js";
4 | import { InjectorContext } from "./injector-context";
5 |
6 | export const DIContainer: FC<{
7 | providers: Provider[];
8 | children: ReactNode;
9 | }> = ({ children, providers }) => {
10 | const rootInjector = useContext(InjectorContext) as ReflectiveInjector;
11 | const contextInjector = useOnce(() => {
12 | if (rootInjector === Injector.NULL) {
13 | return ReflectiveInjector.resolveAndCreate(providers);
14 | }
15 | return rootInjector.resolveAndCreateChild(providers);
16 | });
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/use-inject.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { InjectionToken, Type } from "injection-js";
3 | import { InjectorContext } from "./injector-context";
4 |
5 | export function useInject(
6 | token: Type | InjectionToken,
7 | notFoundValue?: T
8 | ): T {
9 | return useContext(InjectorContext).get(token, notFoundValue);
10 | }
11 |
--------------------------------------------------------------------------------
/src/use-once.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 |
3 | type OnceValue = { value: T };
4 |
5 | export function useOnce(fn: () => T): T {
6 | const ref = useRef | null>(null);
7 |
8 | // use {value: fn()} to avoid recreating when fn() equals null
9 | if (ref.current === null) {
10 | ref.current = { value: fn() };
11 | }
12 |
13 | return ref.current?.value;
14 | }
15 |
--------------------------------------------------------------------------------
/test/use-inject.spec.tsx:
--------------------------------------------------------------------------------
1 | import { useInject, DIContainer } from "../src";
2 | import { act, create } from "react-test-renderer";
3 | import { useEffect } from "react";
4 | import { Injectable } from "injection-js";
5 | import "@abraham/reflection";
6 | describe("useInject", () => {
7 | it("should useInject work", () => {
8 | let data = null;
9 | @Injectable()
10 | class LowLevelClass {
11 | level = "low-level";
12 | }
13 |
14 | @Injectable()
15 | class HighLevelClass {
16 | constructor(public lowLevelClass: LowLevelClass) {}
17 | }
18 | function Component() {
19 | const level = useInject(HighLevelClass).lowLevelClass.level;
20 | useEffect(() => {
21 | data = level;
22 | }, []);
23 | return <>>;
24 | }
25 |
26 | function DI() {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 | const fixtureNode = ;
34 | const testRenderer = create(fixtureNode);
35 | expect(data).toBe(null);
36 | act(() => testRenderer.update(fixtureNode));
37 | expect(data).toBe("low-level");
38 | });
39 | it("should useInject nested work", () => {
40 | let data = null;
41 | @Injectable()
42 | class LowLevelClass {
43 | level = "low-level";
44 | }
45 |
46 | @Injectable()
47 | class HighLevelClass {
48 | constructor(public lowLevelClass: LowLevelClass) {}
49 | }
50 | function Component() {
51 | const level = useInject(HighLevelClass).lowLevelClass.level;
52 | useEffect(() => {
53 | data = level;
54 | }, []);
55 | return <>>;
56 | }
57 |
58 | function DI() {
59 | return (
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 | const fixtureNode = ;
68 | const testRenderer = create(fixtureNode);
69 | expect(data).toBe(null);
70 | act(() => testRenderer.update(fixtureNode));
71 | expect(data).toBe("low-level");
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "outDir": "./out-tsc",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "noImplicitOverride": true,
9 | "jsx": "react-jsx",
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": true,
15 | "downlevelIteration": true,
16 | "skipLibCheck": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "es2017",
20 | "module": "es2020",
21 | "lib": [
22 | "es2020"
23 | ]
24 | },
25 | "files": [
26 | "./src/index.ts"
27 | ],
28 | "jsx": "react-jsx"
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "outDir": "./dist/out-tsc",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "sourceMap": true,
13 | "declaration": false,
14 | "downlevelIteration": true,
15 | "experimentalDecorators": true,
16 | "moduleResolution": "node",
17 | "importHelpers": true,
18 | "target": "es2017",
19 | "module": "commonjs",
20 | "lib": [
21 | "es2020"
22 | ],
23 | "jsx": "react-jsx"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------