On the Plan and Create team we were using CommonJS as our module system until very recently when we ran into a circular dependency issue with our React components. For one of our new features we had a component that needed to reference a modal and that modal in turn needed to reference a new instance of the original component. This circular relationship caused a problem and webpack could not resolve the dependencies correctly. Our solution was to upgrade our modules to use ES6 import/exports. This enabled us to reuse the react components and avoid circular dependencies while moving us closer to ES standards. We upgraded as much as we could without affecting other teams.
What is a module?
A module is a reusable block of code that with data and implementation details for a specific functionality that is exposed as a public API to be loaded and used by other modules. The concept of a module stems from the modular programming paradigm which says that software should be composed of separate components that are responsible for specific functions that are linked together to form a complete program.
Why are modules useful?
Modules allow programmers to:
- Abstract code: hide implementation details from the user, so they only have knowledge on what the object does, not how it’s done
- Encapsulate code: hiding attributes in programming so they can only be accessed via methods of their current class
- Reuse code: avoid repetitiveness in code by abstracting out methods and classes
- Manage dependencies
ES5 Module System – CommonJSES5 was not designed with modules, so developers introduced patterns to simulate modular design. CommonJS modules were designed with server-side development in mind, so the API is synchronous. Modules are loaded at the moment they are needed in the order they are required inside of the file. Each file is a unique module with two objects, require and module.exports, used to define dependencies and modules.
Exports or module.exports is used to export module contents as public elements and a module identifier (location path of the module).
Require is used by modules to import the exports of other modules. Every time you use require(‘example-module’) you get the same instance of that module ensuring the modules are a singleton and state is synchronized throughout the application.
ES6 Module SystemES6 introduces a standard module system based on CommonJS. In ES6 the module system operates differently to the mechanism above. CommonJS assumes that you will either use an entire module, or not use it at all whereas ES6 modules assumes that a module exports one or more entities and another module will use any number of those entities exported.
The two core concepts of the ES6 module system are exporting and importing. Each file represents a single module which can export any number of entities as well as import any other entities.
Variable and functions that are declared in a module are scoped to that module, so only entities that are exported from the module are public to other modules and the rest remain private to the module. This can be leveraged for abstraction and to explicitly make elements publicly available.
The import directive is used to bring in modules to the current file. A module can import any number of other modules and refer to none, some, or all of the objects. Any object that is referred to must be specified in the import statement.
- Because import and export are static, static analyzers can build a tree of dependencies.
- Modules can be synchronously and asynchronously loaded
At this time not all browsers implement ES6 module loading. The workaround is to use transpilers such as Babel to convert code to an ES5 module format.
Difficulties/ChangesUpdating to use ES6 modules in our code base led to some problems in our test suite, causing majority of the tests to fail. We soon realized that this is because in our current test suite we were using the JS Rewire library to mock modules in tests which does not support the new ES6 module syntax causing everything to explode.
Rewire is important because it provides an easy way to perform dependency injection by adding getter and setter methods to modules us to modify the behavior of imported modules in order to test the component that imports those modules in isolation.
Luckily there is a an alternative to the JS Rewire library, babel-plugin-rewire, that works with Babel’s ES6 module by adding methods to modules allowing us to mock data and test components in isolation. To make this change you must include the babel-plugin-rewire in your package.json file in the dependencies section amongst other changes.
Example using the JS Rewire Library: *Note the CommonJS require syntax
Example using babel-plugin-rewire: * Note the ES6 import syntax – you can import named exports which only import the methods, not the entire library.
About the Author
Sonalee is a Co-op Front-End Developer on the Plan and Create team at Hootsuite. She is a Bachelor of Science in Computer Science and a minor in Commerce at the University of British Columbia. In her spare time, Sonalee enjoys hiking, snowboarding and foosball.