├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── TODO ├── examples ├── README.md └── hierarchal-lazy-loading │ ├── index.html │ ├── index.jsx │ └── lib │ └── githubDAO.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── index.tsx ├── test ├── __snapshots__ │ └── react-service-container.test.tsx.snap └── react-service-container.test.tsx ├── tsconfig.json ├── tsconfig.test.json └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | examples/ linguist-documentation 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: https://goel.io/joe 2 | 3 | #####=== Node ===##### 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 31 | node_modules 32 | 33 | # Debug log from npm 34 | npm-debug.log 35 | 36 | umd/ 37 | lib/ 38 | .cache/ 39 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !package*.json 3 | !lib/* 4 | !umd/* 5 | !LICENSE.md 6 | !README.md 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | travis.kaufman@gmail.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | 131 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This is a brand new library and I personally find it useful. I would love feedback for the community and will 2 | happily accept contributions! 3 | 4 | **Please make sure you follow the [code of conduct](./CODE_OF_CONDUCT.md) when interacting with me 5 | or anyone else who contributes to this codebase** 6 | 7 | :heart: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Travis Kaufman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-service-container 2 | 3 | [![Build Status](https://travis-ci.org/traviskaufman/react-service-container.svg?branch=master)](https://travis-ci.org/traviskaufman/react-service-container) 4 | 5 | react-service-container is a library which helps provide services to your components and hooks 6 | in an easy, clean, and testable manner. 7 | 8 | Simply define a service 9 | 10 | ```jsx 11 | // greeter.js 12 | 13 | export default class Greeter { 14 | greet() { 15 | return "👋 Hello there!"; 16 | } 17 | } 18 | ``` 19 | 20 | provide it within a `ServiceContainer` 21 | 22 | ```jsx 23 | // App.js 24 | import React from "react"; 25 | import { ServiceContainer } from "react-service-container"; 26 | import Greeter from "./greeter"; 27 | import Greeting from "./Greeting"; 28 | 29 | export default function App() { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | ``` 37 | 38 | and use it within your components: 39 | 40 | ```jsx 41 | // Greeting.js 42 | import React from "react"; 43 | import { useService } from "react-service-container"; 44 | import Greeter from "./greeter"; 45 | 46 | export default function Greeting() { 47 | const greeter = useService(Greeter); 48 | return

{greeter.greet()}

; 49 | } 50 | ``` 51 | 52 | Testing your components is a breeze: 53 | 54 | ```jsx 55 | import React from "react"; 56 | import { render } from "@testing-library/react"; 57 | import "@testing-library/jest-dom/extend-expect"; 58 | import Greeter from "./greeter"; 59 | import Greeting from "./greeting"; 60 | 61 | // Greeting.spec.js 62 | test("renders a greeting", () => { 63 | const fakeGreet = jest.fn(); 64 | fakeGreet.mockReturnValue("expected greeting"); 65 | 66 | const { asFragment } = render( 67 | 70 | 71 | 72 | ); 73 | 74 | expect(asFragment()).toHaveTextContent("expected greeting"); 75 | }); 76 | ``` 77 | 78 | [View a working example of the above code](https://codesandbox.io/s/simple-example-00drq) 79 | 80 | The library is based off of the [Service Locator](https://martinfowler.com/articles/injection.html#UsingAServiceLocator) pattern, and the API is inspired by [apollo-client](https://github.com/apollographql/apollo-client). 81 | 82 | ### Features 83 | 84 | - [x] Dead simple to use. No annotations, reflect-metadata, etc. needed 85 | - [x] Idiomatically React. First-class support for hooks and components. Includes react-style descriptive error messages. 86 | - [x] Supports hierarchal containers for lazy loading and code splitting. 87 | - [x] First-class TypeScript support 88 | - [x] Fully tested with 100% code coverage 89 | 90 | Unlike similar libraries, this is **not** a dependency injection library. The sole purpose of this library 91 | is to provide components with services. 92 | 93 | ### Motivation 94 | 95 | When developing React applications, I find that using [Context](https://reactjs.org/docs/context.html) together with [Custom Hooks](https://reactjs.org/docs/hooks-custom.html) provides a really clean and powerful way to inject services into components. For example, if I have a `Greeter` service, such as I used in the example above, 96 | I could write something like this: 97 | 98 | ```jsx 99 | // greeter.js 100 | import { createContext, useContext } from "react"; 101 | 102 | export default class Greeter { 103 | greet() { 104 | // ... 105 | } 106 | } 107 | 108 | export const GreeterContext = createContext(null); 109 | 110 | export function useGreeter() { 111 | const instance = useContext(Greeter); 112 | if (!instance) { 113 | throw new Error(`[useGreeter] Greeter was never provided`); 114 | } 115 | return instance; 116 | } 117 | ``` 118 | 119 | Then, in my application code I can provide Greeter at runtime, again as shown above: 120 | 121 | ```jsx 122 | // App.js 123 | import React from "react"; 124 | import Greeter, { GreeterContext } from "./greeter"; 125 | import Greeting from "./Greeting"; 126 | 127 | const greeter = new Greeter(); 128 | export default function App() { 129 | return ( 130 | 131 | 132 | 133 | ); 134 | } 135 | ``` 136 | 137 | > **NOTE**: In a real app, `Greeting` may be nested somewhere deep down in the component tree, while in this 138 | > example it may look like it would just be easier to pass it as a prop, in a real application you'd have to pass 139 | > it down an entire component tree, making this method more appealing (to me at least). 140 | 141 | Finally, I could use my custom hook in my `Greeting` component: 142 | 143 | ```jsx 144 | // Greeting.js 145 | import React from "react"; 146 | import { useGreeter } from "./greeter"; 147 | 148 | export default function Greeter() { 149 | const greeter = useGreeter(); 150 | return

{greeter.greet()}

; 151 | } 152 | ``` 153 | 154 | This not only makes it super easy for components to consume services, but once I started using it, I realized I also preferred providing service mocks in tests explicitly vs. using Jest's 155 | [module mocking](https://jestjs.io/docs/en/mock-functions#mocking-modules). I found that explicitly specifying the services my components rely upon made me less likely to 156 | mock out implementation details and ensure I drew clear boundaries around separation of concern. 157 | 158 | > I also prefer to encapsulate services as classes (call me old-school I guess?), and found Jest's ES6 class mocking to be a bit difficult. This of course is just my personal opinion :sweat_smile: 159 | 160 | However, once I started doing this with multiple services, e.g. `FooService`, `BarService`, `BazService`, I started to get into this slippery slope where not only was I writing a ton of boilerplate code for every service, but my code started looking more diagonal vs. vertical when declaring services. 161 | 162 | ```jsx 163 | import FooService, { FooContext } from "./fooService"; 164 | import BarService, { BarContext } from "./barService"; 165 | import BazService, { BazContext } from "./bazService"; 166 | 167 | const foo = new Foo(); 168 | const bar = new Bar(); 169 | const baz = new Baz(); 170 | export default function App() { 171 | 172 | 173 | {/* ... */} 174 | 175 | ; 176 | } 177 | ``` 178 | 179 | I wanted a way to generalize the concept of providing services via contexts and hooks in an easy and intuitive manner, and took inspiration from [Angular's dependency injection system](https://angular.io/guide/dependency-injection) to do so (but without the complexity that true DI comes with). This turned out to work well for my use cases, and hence `react-service-container` was born. 180 | 181 | With `react-service-container`, the above becomes: 182 | 183 | ```jsx 184 | import FooService from "./fooService"; 185 | import BarService from "./barService"; 186 | import BazService from "./bazService"; 187 | import { ServiceContainer } from "react-service-container"; 188 | 189 | export default function App() { 190 | 191 | {/* ^_^ */} 192 | ; 193 | } 194 | ``` 195 | 196 | Not to mention no more `Context` / hook definition boilerplate in your services. 197 | 198 | ![MUCH BETTA!](https://i.imgur.com/DgcnE9v.gif) 199 | 200 | ## Installation 201 | 202 | ``` 203 | npm i -S react-service-container 204 | ``` 205 | 206 | ### UMD Builds 207 | 208 | UMD builds can be found in the npm package's `umd/` folder, containing both development (`react-service-container.js`) 209 | and production (`react-service-container.min.js`) builds. Source maps are included in the folder. 210 | 211 | If you'd like to include react-service-container using a ` 216 | ``` 217 | 218 | ## Usage 219 | 220 | ### Providing services 221 | 222 | In order to use `react-service-container`, you must create a top-level `ServiceContainer` component, and pass it 223 | a list of **providers** via its `providers` prop that tell `react-service-container` what services are available, 224 | and how to constructor them. 225 | 226 | ```jsx 227 | import Greeter from "./greeter"; 228 | import ApiClient from "./apiClient"; 229 | 230 | ReactDOM.render( 231 | 240 | 241 | 242 | ); 243 | ``` 244 | 245 | You can then use the `useService()` hook within your components in order to make use of a service. 246 | 247 | **NOTE**: Each service is only instantiated _once_, the first time `useService()` is called. 248 | 249 | #### The Provider API 250 | 251 | > If you're familiar with [Angular's DI providers](https://angular.io/guide/dependency-injection-providers), you're familiar with ours. The API is pretty much the same. 252 | 253 | Providers come in two forms: 254 | 255 | - An object with a `provide` key whose value is the **service token** you wish to use to represent the service provided, and an additional key the options of which are described below. 256 | - A `Function` object you pass as a shorthand for `{provide: Function, useClass: Function}` 257 | 258 | The providers you can use are listed below in the code example: 259 | 260 | ```js 261 | class MyService {} 262 | 263 | const providers = [ 264 | MyService, // Class shorthand 265 | { provide: MyService, useClass: MyService }, // Equivalent to the above 266 | { provide: MyService, useValue: new MyService() }, // Provide a concrete value to be used 267 | { provide: MyService, useFactory: () => new MyService() }, // Provide a factory function, useful for configuring the service 268 | ]; 269 | ``` 270 | 271 | You can also alias dependencies via `useExisting` 272 | 273 | ```js 274 | class NewService {} 275 | 276 | const providers = [MyService, { provide: NewService, useExisting: MyService }]; 277 | ``` 278 | 279 | This is useful for gradually deprecating APIs. 280 | 281 | See the tests in this repo for example of using each. 282 | 283 | ### Using Hierarchal Containers 284 | 285 | `react-service-container` fully supports hierarchal `` components. When `` 286 | components are nested within one another, `useService()` calls act exactly like variable lookups: starting from the inner-most to the outer-most service container, the first service token found will be used. 287 | 288 | Using hierarchal service containers not only lets you keep different parts of your codebase cleanly separated, but 289 | it allows for [lazy loading](https://reactjs.org/docs/code-splitting.html) of services at the time in which you need it. 290 | 291 | Say you have an app which shows a list of TODOs, as well as a settings page. You lazily load the page since users tend to navigate to either one page or the other, most likely TODOs. Chances are that the services 292 | for the TODOs page might be different than those used for the settings page. However, some, such as getting 293 | information about the current user, might be shared across the entire application. Using hierarchal service containers 294 | allows the top-level application to contain shared modules, while lazily loaded feature modules can configure their services at load time. 295 | 296 | ```jsx 297 | // src/todos/index.js 298 | 299 | import React from "react"; 300 | import { ServiceContainer } from "react-service-container"; 301 | import TodosService from "./todosService"; 302 | 303 | export default function Todos() { 304 | return ( 305 | 306 | {/* Render TODOs */} 307 | 308 | ); 309 | } 310 | ``` 311 | 312 | ```jsx 313 | // src/settings/index.js 314 | 315 | import React from "react"; 316 | import { ServiceContainer } from "react-service-container"; 317 | import SettingsService from "./settingsService"; 318 | 319 | export default function Settings() { 320 | return ( 321 | 322 | {/* Render settings */} 323 | 324 | ); 325 | } 326 | ``` 327 | 328 | ```js 329 | // src/App.js 330 | 331 | import React, { Suspense, lazy } from "react"; 332 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 333 | import { ServiceContainer } from "react-service-container"; 334 | 335 | // Common dependency 336 | import UserService from "./userService"; 337 | // Loading indicator 338 | import Loading from "./components/Loading"; 339 | 340 | // Lazily loaded components 341 | const Todos = lazy(() => import("./todos")); 342 | const Settings = lazy(() => import("./settings")); 343 | 344 | export default function App() { 345 | return ( 346 | 347 | 348 | }> 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | ); 360 | } 361 | ``` 362 | 363 | Now any components/hooks under `Todos` or `Settings` can call `useService(UserService)`, but only components/hooks under `Todos` can 364 | call `useService(TodosService)` and same for `Settings` and `useService(SettingsService)`; 365 | 366 | ### Wrapping non-class dependencies 367 | 368 | Provide is _simple_. `provide` can be any JS object. Theoretically `{provide: 'hello!', useValue: {...}}` would work. 369 | If you want to inject something that's not a class, try using [Symbols](http://mdn.io/Symbol). 370 | 371 | ```jsx 372 | /** config.js */ 373 | 374 | export const config = { 375 | ..., 376 | }; 377 | 378 | // NOTE: A string would work fine as well, if you wanted to be simpler. My preference 379 | // is to go for Symbols since they're completely unambiguous. 380 | export const CONFIG = Symbol.for("config"); 381 | 382 | /** App.js */ 383 | 384 | import {CONFIG, config} from "./config"; 385 | 386 | function App() { 387 | return {...} 388 | } 389 | ``` 390 | 391 | ### Usage within class components 392 | 393 | Service containers can be easily used without hooks in class components as well. Simply set 394 | the component's `contextType` property to `ServiceContainerContext` and use `this.context.get()` 395 | inside the render method or anywhere else that's needed. 396 | 397 | ```jsx 398 | /* Greeting.js */ 399 | 400 | import React from "react"; 401 | import { ServiceContainerContext } from "react-service-container"; 402 | import Greeter from "./greeter"; 403 | 404 | class MyComponent extends React.Component { 405 | static contextType = ServiceContainerContext; 406 | 407 | render() { 408 | const greeter = this.context.get(Greeter); 409 | return

{greeter.greet()}

; 410 | } 411 | } 412 | ``` 413 | 414 | ### Usage with TypeScript 415 | 416 | `react-service-container` is written in TypeScript, and comes with first-class support for it. When using `Function` 417 | objects, such as class constructors, with `useService()`, it is properly typed as an instance of that constructor. 418 | 419 | Let's take the example from the introduction and rewrite it in TypeScript. 420 | 421 | ```ts 422 | // greeter.ts 423 | 424 | export default class Greeter { 425 | greet(): string { 426 | return "👋 Hello there!"; 427 | } 428 | } 429 | ``` 430 | 431 | ```tsx 432 | // App.tsx 433 | import React from "react"; 434 | import { ServiceContainer } from "react-service-container"; 435 | import Greeter from "./greeter"; 436 | import Greeting from "./Greeting"; 437 | 438 | export default function App() { 439 | return ( 440 | 441 | 442 | 443 | ); 444 | } 445 | ``` 446 | 447 | ```tsx 448 | // Greeting.tsx 449 | import React from "react"; 450 | import { useService } from "react-service-container"; 451 | import Greeter from "./greeter"; 452 | 453 | export default function Greeting() { 454 | const greeter = useService(Greeter); 455 | return

{greeter.greet()}

; 456 | } 457 | ``` 458 | 459 | In the above component, `greeter` is correctly typed to `Greeter`, ensuring type correctness and consistency. 460 | 461 | #### Non-class services 462 | 463 | What about the `config` example earlier? 464 | 465 | ```tsx 466 | /** config.ts */ 467 | 468 | export interface Config {/* ... */} 469 | 470 | export const config: Config = { 471 | ..., 472 | }; 473 | 474 | export const CONFIG = Symbol.for("config"); 475 | 476 | /** App.tsx */ 477 | 478 | import {CONFIG, config} from "./config"; 479 | 480 | function App() { 481 | return {...} 482 | } 483 | ``` 484 | 485 | Here's how we might use that in a component: 486 | 487 | ```tsx 488 | // Component.tsx 489 | import { useService } from "react-service-container"; 490 | import { CONFIG } from "./config"; 491 | 492 | export default function Component() { 493 | const config = useService(CONFIG); 494 | // render component 495 | } 496 | ``` 497 | 498 | Here, `config` is typed as `any`. This is because based on the given Symbol, TypeScript does not _statically_ know 499 | what type the value associated with that symbol is; the symbol could represent any type. 500 | 501 | However, because it's cast to `any`, we can easily typecast the result 502 | 503 | ```tsx 504 | import { useService } from "react-service-container"; 505 | import { CONFIG, Config } from "./config"; 506 | 507 | export default function Component() { 508 | const config = useService(CONFIG); 509 | // render component 510 | } 511 | const config = useService(CONFIG) as Config; 512 | ``` 513 | 514 | This is still less than ideal, since the TypeCasting is ugly and can be repetitive. Here is my preferred approach: 515 | 516 | ```tsx 517 | // config.ts 518 | import {useService} from 'react-service-container'; 519 | 520 | export interface Config {/* ... */} 521 | 522 | export const config: Config = { 523 | ..., 524 | }; 525 | 526 | const configToken = Symbol.for("config"); 527 | 528 | export const CONFIG_PROVIDER = { 529 | provide: configToken, 530 | useValue: config 531 | }; 532 | 533 | export const useConfig = () => useService(configToken) as Config; 534 | ``` 535 | 536 | By providing a custom `useConfig` hook, and defining the provider within the component, it dramatically reduces the 537 | error surface and repetition of doing manual type-checking, and allows you to abstract away the service token for the config itself. 538 | 539 | ```tsx 540 | // App.tsx 541 | import {ServiceContainer} from 'react-service-container'; 542 | import {CONFIG_PROVIDER} from './config'; 543 | 544 | function App() { 545 | return {...} 546 | } 547 | 548 | // Component.tsx 549 | 550 | import {useConfig} from './config'; 551 | 552 | export default function Component() { 553 | const config = useConfig(); 554 | // render component using config, which is now correctly typed. 555 | } 556 | ``` 557 | 558 | # License 559 | 560 | MIT 561 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - [x] Typescript 2 | - [x] Tidy up README 3 | - [x] TypeScript usage / tips 4 | - [x] Rewrite each section 5 | - [x] CI Setup (linting / testing / etc) 6 | - [x] Distribution and publishing 7 | - Source maps, and minification 8 | - [-] Examples (P2) 9 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Still a work in progress. Coming soon! 2 | -------------------------------------------------------------------------------- /examples/hierarchal-lazy-loading/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/hierarchal-lazy-loading/index.jsx: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | 3 | import React, { useState } from "react"; 4 | import ReactDOM from "react-dom"; 5 | import { ServiceContainer, useService } from "./lib"; 6 | import GithubDAO from "./lib/githubDAO"; 7 | 8 | ReactDOM.render(, document.getElementById("root")); 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | function Repos() { 19 | const [loading, setLoading] = useState(false); 20 | const [error, setError] = useState(null); 21 | const [repos, setRepos] = useState(null); 22 | const githubDAO = useService(GithubDAO); 23 | 24 | const loadRepos = async () => { 25 | setLoading(true); 26 | try { 27 | const loadedRepos = await githubDAO.listRepos(); 28 | setRepos(loadedRepos); 29 | setError(null); 30 | } catch (err) { 31 | setError(err); 32 | } finally { 33 | setLoading(false); 34 | } 35 | }; 36 | 37 | return ( 38 |
39 | 40 | {(() => { 41 | if (loading) { 42 | return

Loading...

; 43 | } 44 | 45 | if (error) { 46 | return

Error: {error}

; 47 | } 48 | 49 | return ( 50 |
    51 | {repos 52 | ? repos.map((r) => ( 53 |
  • 54 | {r.name} 55 |
  • 56 | )) 57 | : null} 58 |
59 | ); 60 | })()} 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/hierarchal-lazy-loading/lib/githubDAO.js: -------------------------------------------------------------------------------- 1 | export default class GithubDAO { 2 | async listRepos() { 3 | const res = await fetch("https://api.github.com/repositories"); 4 | const data = await res.json(); 5 | return data.map((repo) => ({ 6 | id: repo.id, 7 | name: repo.full_name, 8 | link: repo.html_url, 9 | })); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-service-container", 3 | "version": "0.2.6", 4 | "description": "A simple, robust, idiomatic service locator library for React applications", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/traviskaufman/react-service-container.git" 8 | }, 9 | "main": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "scripts": { 12 | "dev": "parcel index.html", 13 | "pretest": "npm run build", 14 | "test": "jest --coverage --config jest.config.js", 15 | "test:watch": "jest --watch --config jest.config.js", 16 | "build:lib": "tsc", 17 | "build:dist": "webpack --config webpack.config.js && webpack --config webpack.config.js --env production", 18 | "build": "npm run build:lib && npm run build:dist", 19 | "prepublishOnly": "npm run build" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "dependency-injection", 24 | "service-container", 25 | "provider", 26 | "di", 27 | "service-locator" 28 | ], 29 | "author": "Travis Kaufman ", 30 | "license": "MIT", 31 | "peerDependencies": { 32 | "react": "^16.13.1" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^26.0.3", 36 | "@types/react": "^16.9.41", 37 | "@types/react-test-renderer": "^16.9.2", 38 | "jest": "^26.1.0", 39 | "react": "^16.13.1", 40 | "react-dom": "^16.13.1", 41 | "react-test-renderer": "^16.13.1", 42 | "regenerator-runtime": "^0.13.5", 43 | "rollup": "^2.21.0", 44 | "ts-jest": "^26.1.1", 45 | "ts-loader": "^8.0.0", 46 | "tslib": "^2.0.0", 47 | "typescript": "^3.9.5", 48 | "webpack": "^5.75.0", 49 | "webpack-cli": "^5.0.1", 50 | "webpack-merge": "^5.0.9" 51 | }, 52 | "browserslist": "> 0.25%, not dead", 53 | "jest": { 54 | "coverageThreshold": { 55 | "global": { 56 | "branches": 100, 57 | "functions": 100, 58 | "lines": 100, 59 | "statements": 100 60 | } 61 | }, 62 | "globals": { 63 | "ts-jest": { 64 | "tsConfig": "tsconfig.test.json" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | const UNINSTANTIATED = Symbol.for("uninstantiated"); 4 | 5 | export type ServiceFor = T extends new (...args: any[]) => infer R ? R : any; 6 | 7 | export interface UseValueProvider { 8 | provide: T; 9 | useValue: ServiceFor; 10 | } 11 | 12 | export interface UseClassProvider { 13 | provide: T; 14 | useClass: new (...args: any) => ServiceFor; 15 | } 16 | 17 | export interface UseFactoryProvider { 18 | provide: T; 19 | useFactory(): ServiceFor; 20 | } 21 | 22 | export interface UseExistingProvider { 23 | provide: T; 24 | useExisting: R; 25 | } 26 | 27 | export type Provider = 28 | | (new (...args: any[]) => any) 29 | | UseValueProvider 30 | | UseClassProvider 31 | | UseFactoryProvider 32 | | UseExistingProvider; 33 | 34 | export type Providers = Array>; 35 | 36 | export class ServiceContainerRegistry { 37 | private providers = new Map(); 38 | 39 | constructor( 40 | private readonly parent: ServiceContainerRegistryReadonlyProxy | null 41 | ) { 42 | this.parent = parent; 43 | this.providers = new Map(); 44 | } 45 | 46 | get>(serviceToken: T): R { 47 | if (this.providers.has(serviceToken)) { 48 | const initFn = this.providers.get(serviceToken) as () => R; 49 | try { 50 | return initFn(); 51 | } catch (err) { 52 | console.error( 53 | `[react-service-container] Provider for ${String( 54 | serviceToken 55 | )} threw an error. ` + 56 | "Please check the error output below for more information." 57 | ); 58 | throw err; 59 | } 60 | } 61 | 62 | if (this.parent != null) { 63 | // This will recursively call each parent registry, until the base environment is hit in 64 | // which case the base case is thrown below. 65 | return this.parent.get(serviceToken); 66 | } 67 | 68 | const errorMsg = 69 | `[react-service-container] Could not find provider for token ${String( 70 | serviceToken 71 | )}. ` + 72 | 'Think of this as a "missing variable" error. Ensure that in one of your parent ' + 73 | "service containers, you have configured your providers array to provide a value for this type."; 74 | throw new Error(errorMsg); 75 | } 76 | 77 | add(provider: Provider) { 78 | if (!("provide" in provider)) { 79 | const errorMsg = 80 | `[react-service-container] Missing "provide" key in object with key(s): ${stringifyKeys( 81 | provider 82 | )}. ` + 83 | 'Each provider must specify a "provide" key as well as one of the correct use* values.'; 84 | throw new Error(errorMsg); 85 | } 86 | 87 | let instance: ServiceFor = UNINSTANTIATED as any; 88 | const initFn = () => { 89 | if (instance !== UNINSTANTIATED) { 90 | return instance; 91 | } 92 | 93 | let init; 94 | switch (true) { 95 | case "useValue" in provider: 96 | const value = (provider as UseValueProvider).useValue; 97 | init = () => value; 98 | break; 99 | case "useClass" in provider: 100 | const Ctor = (provider as UseClassProvider).useClass; 101 | init = () => new Ctor(); 102 | break; 103 | case "useFactory" in provider: 104 | init = (provider as UseFactoryProvider).useFactory; 105 | break; 106 | case "useExisting" in provider: 107 | const resolvedAlias = (provider as UseExistingProvider) 108 | .useExisting; 109 | init = () => { 110 | try { 111 | return this.get(resolvedAlias); 112 | } catch (_) { 113 | const errorMessage = 114 | `[react-service-container] Failed alias lookup for useExisting provider ${String( 115 | provider 116 | )}. ` + 117 | "It looks like you passed a token to `useExisting` that was not registered as a provider. " + 118 | "Ensure that the token given is registered *before* the alias is referenced. " + 119 | "If the value reference by the alias is provided within the same providers array as the alias, " + 120 | "ensure that it comes before the alias in the providers array."; 121 | throw new Error(errorMessage); 122 | } 123 | }; 124 | break; 125 | default: 126 | const errorMsg = 127 | `[create-service-container] Provider missing proper use* value in key(s): ${stringifyKeys( 128 | provider, 129 | (k) => k !== "provide" 130 | )}. ` + 131 | 'Possible values are: ["useValue", "useClass", "useFactory", "useExisting"]'; 132 | throw new Error(errorMsg); 133 | } 134 | 135 | instance = init(); 136 | return instance; 137 | }; 138 | 139 | this.providers.set((provider as any).provide, initFn); 140 | } 141 | } 142 | 143 | export type ServiceContainerRegistryReadonlyProxy = Pick< 144 | ServiceContainerRegistry, 145 | "get" 146 | >; 147 | 148 | export const ServiceContainerContext = React.createContext( 149 | null 150 | ); 151 | 152 | export type ServiceContainerProps = React.PropsWithChildren<{ 153 | providers: Providers; 154 | }>; 155 | 156 | export function ServiceContainer({ 157 | providers, 158 | children, 159 | }: ServiceContainerProps) { 160 | const parent = useContext(ServiceContainerContext); 161 | const registry = buildRegistry(providers, parent); 162 | 163 | return ( 164 | 165 | {children} 166 | 167 | ); 168 | } 169 | 170 | export function useService>(serviceToken: T): R { 171 | const container = useContext(ServiceContainerContext); 172 | if (!container) { 173 | const errorMsg = 174 | "[react-service-container] Could not find service container context. It looks like you may have used the useService() hook " + 175 | "in a component that is not a child of a .... Take a look at your component tree " + 176 | "and ensure that somewhere in the hierarchy before this component is rendered, there is a " + 177 | "available"; 178 | throw new Error(errorMsg); 179 | } 180 | return container.get(serviceToken); 181 | } 182 | 183 | function readonlyProxy( 184 | registry: ServiceContainerRegistry 185 | ): ServiceContainerRegistryReadonlyProxy { 186 | const proxy = { 187 | get(serviceToken: any): ServiceFor { 188 | return registry.get(serviceToken); 189 | }, 190 | }; 191 | return proxy; 192 | } 193 | 194 | function buildRegistry( 195 | providers: Providers, 196 | parent: ServiceContainerRegistryReadonlyProxy | null 197 | ) { 198 | const registry = new ServiceContainerRegistry(parent); 199 | addProviders(registry, providers); 200 | return registry; 201 | } 202 | 203 | function addProviders( 204 | registry: ServiceContainerRegistry, 205 | providers: Providers 206 | ) { 207 | normalize(providers).forEach((provider) => { 208 | registry.add(provider); 209 | }); 210 | } 211 | 212 | function normalize(providers: Providers): Providers { 213 | return providers.map((provider) => { 214 | const assumeClassShorthand = typeof provider === "function"; 215 | if (assumeClassShorthand) { 216 | return { 217 | provide: provider, 218 | useClass: provider as UseClassProvider["useClass"], 219 | }; 220 | } 221 | return provider; 222 | }); 223 | } 224 | 225 | function stringifyKeys(obj: {}, filter = (k: string) => true) { 226 | return Object.keys(obj) 227 | .filter(filter) 228 | .map((k) => `"${k}"`) 229 | .join(", "); 230 | } 231 | -------------------------------------------------------------------------------- /test/__snapshots__/react-service-container.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Injection Tokens 1`] = ` 4 |

5 | Hello, world! 6 |

7 | `; 8 | 9 | exports[`useExisting with injection tokens 1`] = ` 10 |

11 | value 1 12 |

13 | `; 14 | -------------------------------------------------------------------------------- /test/react-service-container.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import { 4 | Provider, 5 | ServiceContainer, 6 | ServiceContainerContext, 7 | useService, 8 | } from "../src"; 9 | 10 | class Dep { 11 | fn() { 12 | return "Output!"; 13 | } 14 | } 15 | 16 | const Component = () => { 17 | const dep = useService(Dep); 18 | return

{dep.fn()}

; 19 | }; 20 | 21 | let mock: jest.Mock; 22 | beforeEach(() => { 23 | mock = jest.fn(); 24 | jest.spyOn(console, "error").mockImplementation(); 25 | jest.spyOn(console, "warn").mockImplementation(); 26 | }); 27 | 28 | afterEach(() => { 29 | (console.error as jest.Mock).mockRestore(); 30 | (console.warn as jest.Mock).mockRestore(); 31 | }); 32 | 33 | test("useValue", () => { 34 | renderWithProviders([{ provide: Dep, useValue: { fn: mock } }]); 35 | expect(mock).toHaveBeenCalled(); 36 | }); 37 | 38 | test("useClass", () => { 39 | class MockClass { 40 | fn() { 41 | mock(); 42 | } 43 | } 44 | 45 | renderWithProviders([{ provide: Dep, useClass: MockClass }]); 46 | expect(mock).toHaveBeenCalled(); 47 | }); 48 | 49 | test("useFactory", () => { 50 | const factory = () => ({ 51 | fn: mock, 52 | }); 53 | 54 | renderWithProviders([{ provide: Dep, useFactory: factory }]); 55 | expect(mock).toHaveBeenCalled(); 56 | }); 57 | 58 | test("useExisting", () => { 59 | class Existing { 60 | fn() { 61 | mock(); 62 | } 63 | } 64 | 65 | renderWithProviders([ 66 | { provide: Existing, useClass: Existing }, 67 | { provide: Dep, useExisting: Existing }, 68 | ]); 69 | expect(mock).toHaveBeenCalled(); 70 | }); 71 | 72 | test("Class shorthand", () => { 73 | class MockDep { 74 | fn() { 75 | return mock(); 76 | } 77 | } 78 | 79 | const Component = () => { 80 | const mockDep = useService(MockDep); 81 | return

{mockDep.fn()}

; 82 | }; 83 | 84 | renderer.create( 85 | 86 | 87 | 88 | ); 89 | expect(mock).toHaveBeenCalled(); 90 | }); 91 | 92 | test("Injection Tokens", () => { 93 | interface Config { 94 | message: string; 95 | } 96 | 97 | const CONFIG = Symbol.for("config"); 98 | const config: Config = { message: "Hello, world!" }; 99 | 100 | const useConfig = () => useService(CONFIG) as Config; 101 | 102 | const Component = () => { 103 | const { message } = useConfig(); 104 | return

{message}

; 105 | }; 106 | const component = renderer.create( 107 | 108 | 109 | 110 | ); 111 | expect(component.toJSON()).toMatchSnapshot(); 112 | }); 113 | 114 | test("useExisting with injection tokens", () => { 115 | const V1 = Symbol.for("v1"); 116 | const VALIAS = Symbol.for("vAlias"); 117 | const v1 = "value 1"; 118 | 119 | const useV1 = () => useService(VALIAS) as string; 120 | 121 | const Component = () => { 122 | const v1 = useV1(); 123 | return

{v1}

; 124 | }; 125 | 126 | const component = renderer.create( 127 | 133 | 134 | 135 | ); 136 | expect(component.toJSON()).toMatchSnapshot(); 137 | }); 138 | 139 | test("Hierarchal injection", () => { 140 | const parentMock = jest.fn(); 141 | const childMock = jest.fn(); 142 | const overrideMock = jest.fn(); 143 | const mockToBeOverriden = jest.fn(); 144 | 145 | class ParentDep { 146 | fn() { 147 | parentMock(); 148 | } 149 | } 150 | 151 | class Child { 152 | fn() { 153 | childMock(); 154 | } 155 | } 156 | 157 | class DepToOverride { 158 | fn() { 159 | mockToBeOverriden(); 160 | } 161 | } 162 | 163 | class Override { 164 | fn() { 165 | overrideMock(); 166 | } 167 | } 168 | 169 | const Component = () => { 170 | const parent = useService(ParentDep); 171 | const child = useService(Child); 172 | const override = useService(DepToOverride); 173 | 174 | return ( 175 |

176 | {parent.fn()} 177 | {child.fn()} 178 | {override.fn()} 179 |

180 | ); 181 | }; 182 | 183 | renderer.create( 184 | 185 | 188 | 189 | 190 | 191 | ); 192 | 193 | expect(parentMock).toHaveBeenCalled(); 194 | expect(childMock).toHaveBeenCalled(); 195 | expect(overrideMock).toHaveBeenCalled(); 196 | expect(mockToBeOverriden).not.toHaveBeenCalled(); 197 | }); 198 | 199 | test("useExisting hierarchal override", () => { 200 | const parentMock = jest.fn(); 201 | const childMock = jest.fn(); 202 | 203 | class ParentDep { 204 | fn() { 205 | return parentMock(); 206 | } 207 | } 208 | 209 | class Child { 210 | fn() { 211 | return childMock(); 212 | } 213 | } 214 | 215 | const Component = () => { 216 | const dep = useService(Child); 217 | return

{dep.fn()}

; 218 | }; 219 | 220 | renderer.create( 221 | 222 | 225 | 226 | 227 | 228 | ); 229 | expect(parentMock).toHaveBeenCalled(); 230 | expect(childMock).not.toHaveBeenCalled(); 231 | }); 232 | 233 | test("Class components", () => { 234 | let mock = jest.fn(); 235 | 236 | class Dep { 237 | fn() { 238 | return mock(); 239 | } 240 | } 241 | 242 | class Component extends React.Component { 243 | static contextType = ServiceContainerContext; 244 | 245 | render() { 246 | const dep = this.context.get(Dep); 247 | return

{dep.fn()}

; 248 | } 249 | } 250 | 251 | renderer.create( 252 | 253 | 254 | 255 | ); 256 | expect(mock).toHaveBeenCalled(); 257 | }); 258 | 259 | test("Descriptive error message when useService cannot find context", () => { 260 | const Component = () => { 261 | const dep = useService(Dep); 262 | return

{dep.fn()}

; 263 | }; 264 | 265 | expect(() => { 266 | renderer.create(); 267 | }).toThrow( 268 | "Could not find service container context. It looks like you may have used the useService() hook " + 269 | "in a component that is not a child of a .... Take a look at your component tree " + 270 | "and ensure that somewhere in the hierarchy before this component is rendered, there is a " + 271 | "available" 272 | ); 273 | }); 274 | 275 | test("Descriptive error when missing provider", () => { 276 | expect(() => { 277 | renderer.create( 278 | 279 | 280 | 281 | ); 282 | }).toThrow('Think of this as a "missing variable" error'); 283 | }); 284 | 285 | test("Descriptive error when provider function throws", () => { 286 | const err = new Error("Provider error"); 287 | expect(() => { 288 | renderer.create( 289 | { 294 | throw err; 295 | }, 296 | }, 297 | ]} 298 | > 299 | 300 | 301 | ); 302 | }).toThrow(err); 303 | expect(console.error).toHaveBeenCalledWith( 304 | `[react-service-container] Provider for ${Dep} threw an error. Please check the error output below for more information.` 305 | ); 306 | }); 307 | 308 | test("Descriptive error message when useExisting lookup fails", () => { 309 | const ALIAS = Symbol.for("alias"); 310 | const Component = () => { 311 | const dep = useService(ALIAS); 312 | return

{dep.fn()}

; 313 | }; 314 | 315 | expect(() => { 316 | renderer.create( 317 | 318 | 319 | 320 | ); 321 | }).toThrow( 322 | "It looks like you passed a token to `useExisting` that was not registered as a provider. " + 323 | "Ensure that the token given is registered *before* the alias is referenced. If the value reference by " + 324 | "the alias is provided within the same providers array as the alias, ensure that it comes before the alias " + 325 | "in the providers array." 326 | ); 327 | }); 328 | 329 | test("Descriptive error when provider is malformed", () => { 330 | // Missing provide 331 | const noProvideKey = { foo: "bar" }; 332 | expect(() => { 333 | renderer.create( 334 | 335 | 336 | 337 | ); 338 | }).toThrow( 339 | `[react-service-container] Missing "provide" key in object with key(s): "foo". Each provider must specify a "provide" key as well as one of the correct use* values.` 340 | ); 341 | 342 | // Missing correct value to go with provide 343 | const wrongUseKey = { provide: Dep, useCls: Dep }; 344 | expect(() => { 345 | renderer.create( 346 | 347 | 348 | 349 | ); 350 | }).toThrow( 351 | '[create-service-container] Provider missing proper use* value in key(s): "useCls". ' + 352 | 'Possible values are: ["useValue", "useClass", "useFactory", "useExisting"]' 353 | ); 354 | }); 355 | 356 | test("only instantiates services once", () => { 357 | const mockFactory = jest.fn().mockImplementation(() => new Dep()); 358 | renderer.create( 359 | 360 | 361 | 362 | 363 | ); 364 | expect(mockFactory).toHaveBeenCalledTimes(1); 365 | }); 366 | 367 | function renderWithProviders(providers: Provider[]) { 368 | return renderer.create( 369 | 370 | 371 | 372 | ); 373 | } 374 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "lib" /* Redirect output structure to the directory. */, 18 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "exclude": ["test/", "lib/", "umd/"] 70 | } 71 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDirs": ["src/", "test/"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fileName = require("./package.json").name; 3 | const { merge } = require("webpack-merge"); 4 | 5 | const common = { 6 | entry: "./src/index.tsx", 7 | devtool: "source-map", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: [ 13 | { 14 | loader: "ts-loader", 15 | options: { 16 | compilerOptions: { 17 | declaration: false, 18 | }, 19 | }, 20 | }, 21 | ], 22 | exclude: /node_modules/, 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: [".tsx", ".ts", ".js"], 28 | }, 29 | output: { 30 | path: path.resolve(__dirname, "umd"), 31 | library: "ReactServiceContainer", 32 | libraryTarget: "umd", 33 | }, 34 | externals: { 35 | react: { 36 | commonjs: "react", 37 | commonjs2: "react", 38 | amd: "react", 39 | root: "React", 40 | }, 41 | }, 42 | }; 43 | 44 | const dev = { 45 | mode: "development", 46 | output: { 47 | filename: `${fileName}.js`, 48 | }, 49 | }; 50 | 51 | const prod = { 52 | mode: "production", 53 | output: { 54 | filename: `${fileName}.min.js`, 55 | }, 56 | }; 57 | 58 | module.exports = (env) => merge(common, env === "production" ? prod : dev); 59 | --------------------------------------------------------------------------------