When working with RequireJS, you’re likely to run across two modules that need to reference each other. When you create a circular reference using the standard RequireJS define statement on both sides, the module that’s loaded last fails and is thus undefined. This is confusing since the syntax looks valid, but the declared dependency is ultimately undefined. Here’s a quick example of a circular dependency to clarify:
Imagine you have a “user” module that references a “cart” module. The cart module needs a reference to the user module to determine where the user lives in order to calculate taxes.
A separate module called “user” needs a reference to “cart” in order to show a cart icon with the number of items next to it. User.js
So in this example, the cart and user modules have circular references. Thankfully, there’s a variety of ways you can handle circular references when working with RequireJS.
1. Use the Inline Require Syntax
The simplest way to handle circular references is to update one of the two modules to use the following syntax instead of the traditional dependency array approach:
var cart = require("cart");
So given the above example, you could update the cart.js module to use the inline require syntax like this:
This approach is outlined in RequireJS’ documentation. The advantage is it’s simple and obvious. But that’s where the benefits end.
This approach hinders automated unit testing. You can’t test modules that use this pattern in isolation because the inline require syntax assumes the module has already been loaded by another module. Remember, to use this syntax the module must have already been loaded elsewhere. If it hasn’t already been loaded by another module, then RequireJS will throw an error. This leads to the other downside of this approach: It assumes that modules are loaded in a certain order. This syntax is effectively saying “Before this module can be loaded, the module that I’m referencing inline must have already been loaded.” Of course, this dependency isn’t clear until you understand the implications of this syntax. Due to these downsides, this simple approach should be avoided.
2. Utilize the Publish-Subcribe Pattern
When two modules need to communicate and interact with one another, the publish-subscribe (aka pub/sub) pattern a simple and popular approach. This pattern allows two modules to interact in a loosely coupled manner by publishing and subscribing to events. When an event occurs in module a, module b is notified and can thus respond or stay in sync as desired.
The advantage of this approach is loose coupling. Two modules can interact without having any direct references to each other. This means you can test each module in isolation. However, this loose coupling comes with a cost: Changes in one module can ripple throughout the system in ways that are hard to predict. Since there’s no explicit connection between the two (or more) modules that are interacting with one another, it can be difficult to understand how and why the system responds when a given piece of data changes.
3. Pass Data into the Constructor
Often a circular reference is introduced so that some specific data in module A is available in module B. The easiest way to resolve this issue is to use approach #1 outlined above, but as we already discussed, that approach should be avoided. A superior approach is to pass the data module B desires into module B’s constructor or initialization function. This scenario assumes that module A calls module B and requires some data from module A. In this approach module A calls an initialization function on module B to pass in the necessary data. This approach honors established object-oriented principles by assuring the constructor clearly conveys the necessary parameters for instantiating the module.
4. Unify Closely Related Modules
Often a circular reference is simply a sign of a design problem. Before attempting the other approaches, consider whether the two interacting modules should be unified. The obvious advantage to this approach is it resolves a fundamental design flaw rather than masking the issue using the other approaches outlined in this post. Of course, you have to consider carefully whether unifying truly produces a superior design. Is it practical to interact with a single larger module? Does it still have a clear single responsibility? Does unifying the two modules impact the UI design? These are questions to consider before implementing this approach.
5. Pass a Function Reference into the Constructor
If module B simply needs to be able to call a method on module A, then module A can pass a reference to the desired function into the constructor of Module B. This scenario assumes module A initializes module B, so the downside is it assumes a coupling between the two modules. Any modules that initialize module B would need access to the data in module A. Thus, this approach is typically only practical when there’s a single caller to module B.
6. Move Logic / Data to One Side
In some cases the data or method that necessitates the circular reference can simply be moved to the other side. For example, consider whether the responsibilities in module A that depend upon logic in module B could simply be moved to module B. Or vice versa. Sometimes a circular reference simply stems from placing the logic in the wrong spot.
7. Create a Business Object / Service
Creating separate business objects or services is a useful way to eliminate circular dependencies as well. The two modules that rely upon the same methods can now both reference the business object instead. This eliminates the circular reference and creates a clean, simple business object that can typically be easily tested in isolation. A common scenario for this approach is when two viewmodels interact with one another. The common logic can be refactored to a business object so that they both rely on the business object instead.
How do you handle circular dependencies? Chime in via the comments below.