3 React Three Fiber Mistakes I'll Never Make Again
Overview
I recently rebuilt Pascal’s 3D editor from scratch. In this video, I explain the three architecture mistakes that forced that decision.
These were not beginner errors. They initially felt clean and scalable. But as the editor grew, performance degraded, complexity increased, and simple features became harder to reason about. It became clear that some foundational patterns were wrong.
In this devlog, I break down three major issues: mixing React re renders with the Three.js frameloop, overusing Zustand reactivity, and placing logic inside components instead of systems. I also explain the patterns that replaced them and why they scale better.
Pascal is now open source, and the codebase is far healthier than before.
Mistake 1: Mixing React with the Three.js Frameloop
We originally stored all scene nodes inside Zustand and updated the store whenever something changed. Moving an object meant updating its position in the store, which triggered a React re render.
This works for static edits. It breaks during real time interactions.
Dragging an object caused repeated store writes. Each write triggered re renders across components, history updates, and recalculations. The system became noisy and inefficient.
React reconciliation is not designed for high frequency 3D updates. Three.js already has a frameloop built for that.
The fix was separating concerns. The store updates only when the interaction is committed. During dragging, the mesh moves directly through a scene registry that maps node IDs to mesh references.
React stays declarative. Three.js handles real time updates imperatively. Same feature, dramatically better performance and clarity.
Mistake 2: Making Everything Reactive
Zustand makes reactivity easy, which makes it easy to overuse.
Not all data should be reactive.
In Pascal, slabs could push walls upward. That vertical offset is a visual side effect, not structural state. It does not belong in undo history or UI updates.
We initially stored dirty nodes using setters. That created new object references, triggered subscriptions, and added complexity to persistence and history logic.
The better solution was simple. Mutate when reactivity is unnecessary.
We directly mutated a Set of dirty node IDs. No re render. No history pollution. No unnecessary object creation. The system reads that Set inside the frameloop and clears it.
Zustand becomes an organizational layer, not an automatic reactivity engine. That distinction is critical in performance sensitive 3D applications.
Mistake 3: Putting Logic Inside Components
React Three Fiber makes colocating logic inside components easy. That convenience can become a trap.
We initially implemented wall mitering logic inside each wall component. It felt logical, but every wall update triggered multiple component re renders. Geometry recalculations were tied to React’s lifecycle instead of the frameloop.
This caused cascading updates and unnecessary rebuilds.
The solution was introducing systems.
A system runs inside useFrame. Instead of each wall recalculating itself, a centralized system checks dirty walls each frame. If needed, it updates the geometry directly through the mesh registry and marks it clean.
Logic shifts from component driven updates to loop driven updates. Instead of many re renders, there is one predictable system pass per frame. This aligns with real time engine design.
The Result
The rewrite was about drawing a clear boundary between React’s declarative UI layer and Three.js’ real time rendering layer.
The architecture is now easier to reason about, performance is significantly better, and new features scale without fighting the framework.
If you are building complex React Three Fiber applications, understanding where reactivity should stop and where imperative systems should begin is one of the most important architectural decisions you will make.
Resources/Tech Stack
#threejs #js #zustand
Need help with this tutorial? Join our Discord community!