Understanding React.js (Part 1) - Module System and Injection

During the process of re-implementing React.js in Typescript (code at GitHub), I get a chance to look deeper into the source code of React. At this stage, I am focusing on an old version of React (v15+), which essentially uses the stack reconciler. The newer version of React uses fiber reconciler, which will be covered in later posts.

Haste Module System

When I first read the source code, I was very confused about how modules are imported because each file did not use relative imports but rather absolute imports (e.g., var ReactUpdates = require("ReactUpdates")). Such type of path is typically for packages installed under node_modules/, but I could not find them there. It turns out that the magic lies in the “Haste” module system.

For React V15, the “Haste” module system from Facebook was used. Each source file contains a license header in which it uses @providesModule ModuleName to declare a unique module. No matter how deep those files/modules locate, they will be eventually copied into a single flat directory called lib/ with their unique filenames. Consequently, all require("ModuleName") will be auto converted into require("./ModuleName"). In other words, prepending all require() paths with ./.

Side note #1: In newer version of React (V16+), the Haste module system is replaced by ES6 import/export. The whole React architecture is split into a set of packages, which is following the pattern called monorepo.

Side note #2: Since the ModuleName is the same as the file name and unique, for VS Code users, it is easy to open a module by ctrl-p and type the module name.

Injection

React uses Dependency Injection extensively. The package react has only about 2k LoC, which defines core base classes, such as Component, PureComponent, and ReactElement. The methods of Component such as setState are delegated to this.updater which is later injected by a specific renderer. There are many renderers, including react-dom for browser, react-native for mobile phone, and react-test-renderer which converts the virtual DOM into a string mockup for testing.

Different renderers share a core package called react-reconciler, which is responsible for mounting/updating/unmounting components. Inside react-reconciler, Dependency Injection is also used. Each renderer injects their own implementation of HostComponent into react-reconciler. For example, react-dom injects ReactDOMComponent as HostComponent. Also, it injects ReactUpdateQueue as the updater of Commponent through ReactCompositeComponent.mountComponent.

All default injections are defined in a module called ReactDefaultInjection. For example, the BatchingStrategy and ReconcileTransaction are injected into ReactUpdates to handle batched updates within a transaction. Moreover, EventListener and EventPluginHub are injected to handle events.

Summary

Once I understand how the “Haste” system works, I can see its advantage that the import path can be kept really concise. Combining with VS code ctrl-p, it is efficient to locate each module.

Injection is definitely a good way to make React more abstract and customizable. However, this abstraction also makes it harder to locate the exact implementation of abstract instances. Fortunately, react-dom uses the ReactDefaultInjection, which specifies many key injections and saves some effort in locating their implementations.