Releasing Layoutit Grid as open source has been transformational for our company. The reception that opening the project had outside and inside the company was heartwarming. We are now encouraging and supporting ourselves to contribute and participate more in open source projects. In this post, first published in patak.dev I share a few lessons learned while working to upstream a few ideas we got while developing undo support for Layoutit Grid to vueuse’s useRefHistory.
useRefHistory lets you watch a ref and keep track of its history, providing undo and redo functionality. It uses
flush 'pre' by default to watch the source ref. Its semantics are aligned with the standard
watch, creating history points that bundle all the changes done to the source value during the same “tick”. The use of
flush 'sync' is discouraged in the Vue docs. Being able to buffers invalidated effects is important for performance, but also to avoid breaking invariants when generating spurious history points in updates that require several operations. One big caveat for example is that when deeply sync watching an array, a single splice call generates up to three triggers.
Let’s look at a simplified
useUndo composable that only allows undoing of ref changes to understand how we were able to do it and how
ignorableWatch was distilled so we can use the same technique when building other composables. The main difficulty to implement ref history is how to update the source when undoing without triggering the internal watch and re-adding this state to the history. If we only need to support sync flushing, it can be implemented in a straight forward way using a guard.
This technique doesn’t work with flush pre and post because the sync change to the source in the current “tick” triggers the watch but it is flushed together with other effects after the tick is over. So we need a way to flag certain updates to the source and ignore them after the watch is called. We can achieve this using a double watch.
The syncCount is incremented in sync with every change to the source ref value using a watch with
flush 'sync'. This lets us know how many times the ref has been modified in this “tick”. When calling
ignoreUpdates, all sync changes to the source in the updater function will be counted and added to the
ignoreCount. When the second watch flushes at the end of the tick, we know if there were more changes done to the source than the ones that were ignored so we can filter out the triggered effect if all the changes in the “tick” were marked to be ignored.
ignorableWatch follows this idea supporting all flush modes. It is implemented using the double watch scheme for flush
'post', a sync watch allows us to count changes and ignore the triggered effect if all operations were flagged to be ignored in the current “tick”. For
'sync', a single watch with a guard is used to implement
ignorePrevAsyncUpdates is a no-op provided so users can write generic code that does not depend on the
There is a related utility in vueuse called pausableWatch that exposes two methods
resume() allowing to ignore the watch while it is paused.
ignorableWatch is different from it because in
pausableWatch effects are ignored if the watch is paused at flush time.
When we use
ignorableWatch, the effects are ignored when they are triggered.
Now that we have
ignorableWatch in our tool belt, we can build a generic version of the
useUndo composable that supports all flushing modes. We leave
_undo untouched so we can focus on how to modify the watched source without triggering when undoing.
You can check the implementation of
useRefHistory to see how
ignorableWatch is used in the lib. In it, other useful features to deal with ref history are provided.
I learned about the importance of
flush 'pre' while developing undo support for Layoutit Grid. I was later able to upstream some of this experience to vueuse, and
ignorableWatch was spawned organically as part of this process. This is a good example of how working in Apps using Vue Composition API facilitates the discovery and sharing of reusable pieces.