The Hoisting Madness in Monorepos (Reshaped)
2020-04-04 4 min
I know, I know—the title might seem a bit dramatic, but it served its purpose: grabbing your attention. When hoisted dependency issues arise in a monorepo, it may not reach full-blown madness, but it sure can be a pesky headache.
Recently, a bug was introduced in one of the project I collaborate with: some forms simply stopped working. The culprit? The formik object was undefined—something it had never done before. What’s even stranger is that not all forms failed—just some. It felt random, but as usual, that “last change” was behind it.
The quick fix was to revert to a previous commit where things worked. But—and here’s the twist—even forms that worked before the merge broke afterward. It turned out the Formik library had been updated unintentionally.
Working in a Monorepo
Our project uses a monorepo structure, with each package declaring its own dependencies. Yarn’s workspaces feature manages the shared dependencies across packages. Essentially, workspaces allow packages to share modules with the same name and version through a common node_modules folder located in the project root.
In recent commits, a package—call it X—introduced a dependency on Formik version ^2.1.2. Other packages used version ^1.5.8, and another one (Y) relied on version ^2.0.5. This introduced conflicting versions across the monorepo.
Here’s how it played out:
- X and Y are web apps.
- We also have shared UI components (UI), which rely on Formik
1.5.8. - After merging, Formik
2.1.2was hoisted (shared) to the rootnode_modules. Because UI hadn’t declared Formik as a dependency, it ended up using the wrong version.
Understanding Node Module Resolution & Hoisting
Node resolves modules by searching upward from the current file’s directory to parent directories until it finds the module in a node_modules folder—or throws an error.
Yarn’s workspace hoisting takes advantage of this: shared dependencies get hoisted to the root. If UI didn’t explicitly declare Formik, the resolver finds whichever version got hoisted there—regardless of compatibility
The Missing Dependency Issue
Since UI didn’t declare Formik, it ended up resolving Formik from the root, which was now 2.1.2 instead of its intended 1.5.8.
Even after adding Formik 1.5.8 to UI, Yarn hoisted it to the root again. It seemed like nothing changed—but the key win was that now each package explicitly declared its required version
A Persistent Issue: Conflicting Contexts
However, a new error surfaced:
ERROR in ... FormikActions
Module .../node_modules/formik/dist" has no exported member 'FormikActions'.
This happened because two incompatible Formik versions were being loaded in the same environment—leading to namespace and context conflicts.
The Right Fix: Peer Dependencies
When deciding if a library should include third-party dependencies internally—or rely on its host—consider its purpose. If it’s a plugin or companion to another library (like UI), it shouldn’t directly ship that dependency. Instead, it should:
Add it as a devDependency (for testing or development).
"devDependencies": {
"formik": "^1.5.8"
}
This prevents bundlers from including it unless needed.
Declare it as a peerDependency (to require the host project to supply it).
"peerDependencies": {
"formik": "^1.5.8"
}
This way, Yarn raises a warning if the host doesn’t provide a compatible version
Running yarn why formik
Yarn’s why command lets you see which versions of a dependency are hoisted—and why:
$ yarn why formik
=> Found "formik@1.5.8"
info Has been hoisted to "formik"
info Reasons this module exists
- "workspace-aggregator-ec62dbed-443f-486a-b6f7-896c9473a3a4" depends on it
- Hoisted from "_project_#@monorepo#X#formik"
- Hoisted from "_project_#@monorepo#Z#formik"
info Disk size without dependencies: "1.15MB"
info Disk size with unique dependencies: "8.99MB"
info Disk size with transitive dependencies: "22.58MB"
info Number of shared dependencies: 23
=> Found "@monorepo/Y#formik@2.1.2"
info This module exists because "_project_#@monorepo#Y" depends on it.
info Disk size without dependencies: "960KB"
info Disk size with unique dependencies: "8.8MB"
info Disk size with transitive dependencies: "8.93MB"
info Number of shared dependencies: 12
✨ Done in 2.67s.
Output reveals both the hoisted 1.5.8 (used by X and Z) and the isolated 2.1.2 (used by Y), along with their origins.
Summary & Takeaways
- Understand hoisting behavior in Yarn workspaces.
- Never rely on implicit dependencies—declare them explicitly.
- Use devDependencies for tooling/testing.
- Use peerDependencies for plugins or companion libraries to ensure version compatibility.
- Use
yarn why <dependency>to inspect hoisting and dependency distribution.
With these practices, you can avoid unexpected dependency conflicts and keep your monorepo tidy and stable.