I have been working on the development of online documents since last December and encountered the following problems in my work:

The first is how to deal with the differences between platforms.

Our product needs to be compatible with multiple platforms, and the same function needs to call different APIS on different platforms. In order to deal with these platform differences, we wrote a lot of code like this:

if (isMobile) { // ... } else if (isDesktop) {//... } else {//... Browser logic}Copy the code

This is not only difficult to maintain, but because of the inability to tree Shake, code that runs only on platform B is shipped to users’ devices on platform A, increasing package sizes unnecessarily.

One alternative to Hacky is to eliminate branches that won’t be executed through Uglify, but that still doesn’t solve the problem of low maintainability.

The second problem is how to reuse code across multiple products.

Our project has two sub-products, documents and tables, which have both similarities and differences in UI and logic. For example, two product title bar drop-down boxes behave the same, but the menu items in the drop-down boxes are different. For example, a document has a menu item set on the page, but a table does not. For example, two products have the same logic for authentication and networking, but different approaches to the document model.

The third problem is the age-old problem of front-end development, which is how to do state management and logic reuse elegantly.

At present, the community has proposed many solutions to this problem:

  • Mixin is a popular solution in the Vue community. However, it seems that mixin is not a good solution. It can cause problems such as implicit dependencies and naming conflicts. Mixins Considered Harmful Mixins
  • HOC, which was previously recommended by React, is also less than ideal, leading to excessive tag nesting and also naming conflicts.
  • Hooks, now the dominant solution in the React community, solve mixin and HOC issues, but have limitations, such as being only functional components, causing redundant repeat rendering, etc.

Of course, there is no silver bullet to solve this problem perfectly, but we still need to explore new models tailored to the circumstances and needs of our projects.

The fourth issue is code organization.

As products get more complex, code volumes naturally increase, and projects decay — lots of copy-and-paste code, unclear module boundaries, long files and methods, and so on — maintenance costs skyrocket.

To sum up, we need a mechanism that:

  • Separate platform-dependent code and call it to business code with a unified interface;
  • Reuse as much of the same UI and logic as possible, and can easily handle inconsistent parts;
  • Provide a new way of state management and logic reuse;
  • Organize code to decouple modules as much as possible to improve code maintainability.

In my search for a solution, I drew a lot of inspiration from vscode and Angular projects, both of which use dependency injection (DI).

Angular#

The Angular framework itself and applications developed using Angular are based on dependency injection:

  • Dependency injection can be used for configuration management and logical reuse. Logically related states and methods are divided into classes called Services, which can be injected into components or other services. Components can subscribe to the state in a service (combined with RxJS) so that when the state in a service changes, the component rerenders in response; When the business logic needs to be executed, the component can call the methods of the Service.
  • A component can access properties and methods of its parent component or other directives on the same element through dependency injection.
  • The HTTP interceptor and route authentication interfaces provided by the framework are also based on dependency injection.

So what exactly is dependency injection that shines in vscode and Angular, and why does it solve the four problems mentioned at the beginning of this article?

Dependency injection #

In software engineering, dependency injection is an implementation of inversion of control to solve dependency design patterns. A dependency refers to an object that can be exploited (that is, the service provider). Dependency injection is the passing of dependencies to dependent objects that will be used (that is, clients). The service is part of the state that will become the client. Passing the service to the client, rather than allowing the client to create or find the service, is the basic requirement of this design pattern.

That definition comes from Wikipedia, which is known for its reticence. Let’s put it in a simpler way:

Dependency injection is not constructing what you want (that is, dependencies), but declaring what you want and letting someone else construct it. When this construction process occurs in its own construction phase, it is called dependency injection.

If you dig deeper, dependency injection is related to concepts like dependency inversion and inversion of control. You can refer tothisZhihu replied.

In a dependency injection system, there are three main roles:

  • Dependencies: Any can be consumers – mainly class (in the frame of the front end to add components), the use of things, these things may mean class, value, function, component, etc., the dependencies will usually have a logo to distinguish FuYiHe other dependencies, this identifier may be interface, class, may also be a certain data structure. Dependencies bound to an identifier should have the same interface so that consumers can use them indiscriminately (without perception).
  • Providers: Or injectors, they instantiate and provide dependencies based on the needs of consumers.
  • Consumers: They get dependencies from providers through identifiers and then use them. One consumer may also be a dependency on another.

Now that you have a general understanding of the dependency injection pattern, let’s look at how dependency injection solves the problems mentioned at the beginning of this article.

How to implement code reuse #

The idea of solving the second problem is actually the same as that of solving the first one. We just need to abstract the different parts into dependencies and let the rest of the code depend on them.

How to solve state management #

Dependency injection can manage share state. The state shared by multiple components can be extracted into dependencies and combined with publish-subscribe mode to realize intuitive single data flow. Putting methods that change states into dependencies gives you an intuitive idea of how those states will change; It is also easy to combine RxJS to manage more complex data flows. This solution

  • Compared to mixin schemes:

    • Its dependencies are explicit;
    • Does not cause naming conflicts.
  • Compared to HOC:

    • No wrapper hell issues due to multiple nesting;
    • It is easy to track the location and changes of state.
  • Compared to Hooks:

    • “Memorize”;
    • Use classes to store state ratiossetStateThe API is more intuitive to the human mind.
  • The implementation of state management is scoped. If you have many similar modules on the interface (such as Trello’s Kanban), dependency injection mode allows you to easily manage the state of each module to ensure that they do not share some state incorrectly.

[How to solve code organization problems]

The concept of “dependencies” in the dependency injection model forces developers to think about which code is logically related and should be placed in the same class to decouple functional modules. It also forces developers to think about what is UI code and what is business code, keeping UI and business separate. And because of the dependency injection system, the instantiation of a class (and even destruction process) is done by dependency injection framework, so developers only need to care about what function should be divided into modules, module dependencies between how, without the instantiation class one by one, thus reduce the code and the mental burden. Finally, because dependency injection takes the responsibility of constructing its own dependencies away from the class, unit testing is easy.

To make it easy to use dependency injection in React, I implemented a lightweight dependency injection library and a set of React Bindings during the refactoring process.

It has the following features:

  • Non-invasive: Unlike Angular, which is all DI based, WEDi is completely opt-in, and you can decide when and where to use DI.
  • Easy to use: no new concepts are introduced.
  • Both React class and functional components are supported.
  • Support for hierarchical dependency injection
  • Supports injection of class, value (instance), and factory function dependencies.
  • Delayed instantiation is supported.
  • Good type support is provided based on TypeScript.

[Used in functional components]

When you need to provide dependencies, just call useCollection to generate a collection, and then shove it into the Provider component. The Provider’s children can access them.

function FunctionProvider() { const collection = useCollection([FileService]) return ( <Provider Collection ={collection}> {/* Children can access the dependencies in the collection */} </Provider>)}Copy the code
function FunctionConsumer() { const fileService = useDependency(FileService); Return (/* From here you can call properties and methods on FileService */); }Copy the code

Ensure that the function component’sProviderDependencies are not rebuilt during rerendering, so you don’t lose the state saved in the dependencies.

[Optional dependency]

UseDependency can be declared optional by passing the second argument true to it, and TypeScript will infer that the return value may be null. Wedi throws an error if the dependency is not declared optional and cannot be obtained.

function FunctionConsumer() {
  const nullable: NullableService | null = useDependency(NullableService, true)
  const required: NullableService = useDependency(NullableService) // Error!
}
Copy the code

[Used in class components]

@Provide([ FileService, IPlatformService, { useClass: MobilePlatformService }); ] ) class ClassComponent extends Component { static contextType = InjectionContext; @Inject(IPlatformService) platformService! : IPlatformService; @Inject(NullableService, true) nullableService? : NullableService; }Copy the code

When these dependencies are used, the component’s default context is set to InjectionContext, and the dependency can be retrieved through the Inject decorator. Also, you can pass true to Inject to declare dependencies optional.

Support for a variety of dependencies, including classes, values, instances, and factory functions.

[class]

There are two ways to declare a class as a dependency, either by passing the class itself, or by using the useClass API in conjunction with the identifier.

Const classDepItems = [IPlatformService, {useClass: MobilePlatformService}]Copy the code

[Value, instance]

Use useValue to inject values or instances.

const valueDepItem = [IConfig, { useValue: '2020' }]
Copy the code

[Factory function]

Use useFactory to inject the factory method.

const factorDepItem = [ IUserService, { useFactory: (http: IHTTPService): IUserService => new TimeSerialUserService(HTTP, TIME), deps: [IHTTPService]}]Copy the code

You can even inject components:

const IDropdown = createIdentifier<any>('dropdown') const IConfig = createIdentifier<any>('config') const WebDropdown = function () { const dep = useDependency(IConfig) return <div>WeDropdown, {dep}</div> } @Provide([ [IDropdown, { useValue: WebDropdown }], [IConfig, { useValue: 'wedi' }] ]) class Header extends Component { static contextType = InjectionContext @Inject(IDropdown) private dropdown:  any render() { const Dropdown = this.dropdown return <Dropdown></Dropdown> // WeDropdown, wedi } }Copy the code

[combining RxJS]

class CounterService implements Disposable { counter$ = interval(1000).pipe( startWith(0), Scan ((acc) => ACC + 1)) // If there is dispose, wedI will call it when the component is destroyed, here you can do some clean up work: void { this.counter$.complete() } } function App() { const collection = useCollection([CounterService]) return ( <Provide collection={collection}> <Display /> </Provide> ) } function Display() { const counter = useDependency(CounterService) const count = useDependencyValue(counter.counter$) return <div>{count}</div> // 0, 1, 2, 3... }Copy the code