├── .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 | ![build](https://github.com/vthinkxie/use-inject/actions/workflows/workflow.yml/badge.svg) 4 | [![codecov](https://codecov.io/gh/vthinkxie/use-inject/branch/master/graph/badge.svg?token=61PSEDRQpv)](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 | --------------------------------------------------------------------------------