The Ghost in the Machine: Disappearing State and a Deep Dive

Ever had one of those days where your app just… breaks? Not with a clear error message, but with an insidious, almost magical kind of failure that makes you question your sanity? That was my reality recently while working on a React Native project. What started as a seemingly innocuous crash in our internal `@reown/appkit-react-native` package quickly spiraled into a deep dive into the inner workings of Valtio, our state management library, and the surprisingly complex world of Node.js module resolution.
The problem wasn’t immediately obvious. Our app crashed, and while debugging, I noticed something profoundly weird with Valtio. The `subscribe()` function, responsible for reacting to state changes, seemed to be working perfectly. Yet, the core `proxy()` function, which creates the observable state, wasn’t behaving as expected. It was a classic “what you see is not what you get” scenario, and it felt like I was chasing ghosts in the machine.
The Ghost in the Machine: Disappearing State and a Deep Dive
My initial hunch pointed to Valtio’s internal mechanisms. I decided to get my hands dirty, something developers often dread but find invaluable: I went directly into the library code. Specifically, I poked around in `node_modules/valtio/vanilla.js`. My goal was simple: print the value of `proxyStateMap.get(proxyObject)` in both the `proxy()` and `subscribe()` functions.
The results were jarring. Inside `proxy()`, the `proxyStateMap` correctly returned the proxy object, as expected. ✅ But when `subscribe()` tried to find the *same* proxy object, it returned `undefined`. ❌ Wait, what? It was like putting your keys in one drawer and confidently searching for them in an entirely different, empty drawer. The `proxyObject` was clearly there, but `subscribe` couldn’t find it. This could only mean one thing: they were looking at completely different `WeakMap` instances, belonging to different Valtio module instances.
Debugging Library Code: A Necessary Evil (and a Pro Tip)
Before I go further, a quick sidebar on debugging `node_modules` code. It’s often seen as a last resort, but honestly, it can be a lifesaver. When you’re dealing with truly bizarre library behavior, adding `console.log` statements directly into the library’s source allows you to peek at its internal state and trace the execution flow in ways that public APIs simply don’t permit. You get to see exactly what the library is thinking and doing at each step.
However, there’s a common pitfall: module caching. I spent far too long scratching my head, wondering why my carefully placed logs weren’t appearing. Then it hit me: Node.js, and by extension, your package manager, caches modules. To ensure your changes in `node_modules` are actually picked up, you need to clear that cache. For `pnpm`, a quick `pnpm start –reset-cache` (or simply deleting `node_modules` and `pnpm-lock.yaml` and reinstalling) will often do the trick. It’s a small step that saves hours of frustration.
Going Back in Time: The `.npmrc` Revelation
With the `WeakMap` discrepancy confirmed, I was at a crossroads. The code had worked before, so what had changed? This is where the developer’s most underrated superpower comes into play: git history. I retraced my steps, commit by commit, until I found a point where everything was working flawlessly. Then, I performed the digital equivalent of an archaeological dig, comparing the working state to the broken one.
And there it was. A single, seemingly innocent line change in my `.npmrc` file:
+ node-linker=hoisted
Bingo. That one line. That single configuration change was the culprit, the hidden lever that had silently triggered this cascade of module instance chaos. It was a stark reminder that sometimes the smallest changes in your build configuration can have the most profound and unexpected impacts on your runtime behavior.
The Root Cause: A Multi-Instance Problem
Adding `node-linker=hoisted` tells pnpm to adopt a flat `node_modules` structure, much like npm or Yarn classic. While often desirable for simplifying dependency trees, in this specific case, it broke the sharing mechanism for Valtio. Here’s what my `node_modules` structure looked like:
node_modules/valtio(version 1.13.2)node_modules/@reown/appkit-core-react-native/node_modules/valtio(version 2.1.8)node_modules/@reown/appkit-react-native/node_modules/valtio(version 2.1.8)
Can you spot the problem? We had *three* different installations of Valtio! This wasn’t happening by accident. A few factors contributed:
- Version Mismatch: The root `node_modules` had Valtio 1.13.2 (likely a transitive dependency from some other package), while our `@reown` packages explicitly needed 2.1.8.
- No Virtual Store: With `node-linker=hoisted`, pnpm doesn’t use its default `.pnpm` virtual store. The virtual store is designed to create a single, de-duplicated, and symlinked dependency graph that inherently prevents these kinds of multi-instance issues.
- Pnpm’s Prudence: To avoid potential conflicts from differing versions, pnpm, even with hoisting enabled, chose to keep these different Valtio instances separate and nested.
The consequence? Each of these Valtio instances came with its own, completely separate `proxyStateMap` WeakMap. When our code called `proxy()` and stored an object in one instance’s `proxyStateMap`, the subsequent call to `subscribe()` was using a *different* Valtio instance, which, of course, had its own empty `proxyStateMap`. The proxy object was literally in the wrong “drawer” for the searching function.
The Fix: Unifying Dependencies and Broader Lessons
The solution was thankfully straightforward once the root cause was identified: force all Valtio dependencies across the entire project to use a single, consistent version. I achieved this by adding a `pnpm.overrides` section to my root `package.json`:
{ "pnpm": { "overrides": { "valtio": "2.1.8" } }
}
After a quick `pnpm install`, the magic happened. This override unified all Valtio versions to 2.1.8, allowing pnpm to successfully hoist it to the root `node_modules` directory. The nested instances vanished, and crucially, all packages in our workspace were now sharing the *same* Valtio module instance and, therefore, the *same* `proxyStateMap`. The app sprang back to life, the ghosts banished.
Alternatively, I could have simply removed `node-linker=hoisted` to revert to pnpm’s default virtual store behavior, which cleverly handles these scenarios through symlinking. However, my specific project had other compelling reasons for requiring a hoisted structure, so the `pnpm.overrides` approach was the perfect fit.
Beyond Valtio: The Singleton Trap
This saga highlighted some critical takeaways that extend far beyond Valtio itself. This isn’t just a “Valtio thing”; it’s a common pitfall for any library that relies on module-level state, internal `WeakMap`s, or singleton-like patterns. If a library maintains internal state that needs to be globally accessible or shared across its consumers, having multiple instances of that library can lead to incredibly confusing and hard-to-diagnose bugs.
Understanding your package manager’s dependency resolution strategy – whether it’s flat hoisting, a virtual store, or something else – is paramount. Version mismatches are often the silent killers here, as they prevent proper hoisting and can inadvertently create these multi-instance scenarios. Tools like `overrides`, `resolutions`, or `peerDependencies` become essential for maintaining a healthy and predictable dependency graph.
Happy Debugging (and a Little Less Mystery)
What began as a perplexing crash ultimately became a valuable lesson in the intricacies of modern JavaScript development. My key takeaways are simple, yet powerful: always check your git history first; don’t shy away from adding `console.log` statements directly into `node_modules` (just remember to clear your cache!); understand that multiple package instances can wreak havoc on libraries expecting singleton behavior; and finally, actively manage your dependencies to avoid version mismatches. By internalizing these lessons, we can approach our next debugging adventure with more confidence, a clearer roadmap, and hopefully, fewer disappearing keys.




