Scalable signals for TypeScript by tldraw.
Today we are excited to announce the release of tldraw’s core reactive state management system as an open source project under the MIT license.
Signia is an original library for working with fine-grained reactive values (called “signals”) using a new lazy reactivity model based on logical clocks. It was born from a need to work efficiently with large derived immutable collections that change frequently.
In addition to the core library, which is framework-agnostic, we are also releasing bindings for React.
You can find the code on GitHub at github.com/tldraw/signia.
The documentation is available at signia.tldraw.dev.
What’s special in Signia
Signia’s API resembles similar libraries such as Jotai, @preact/signals and Recoil. Its main differentiating features are:
Incremental derivations that can save work while re-computing derived values.
Transactions with built-in support for rollbacks.
Signals that scale
tldraw is a collaborative digital whiteboard built with React. It has an unusually active client state, with lots of in-memory data that changes often and much of which is derived from other data.
We spent several months building tldraw’s new version using a popular reactive state framework. However, we quickly ran into two big problems that made it impossible for us to scale to the number of shapes and multiplayer users that we knew browsers could handle:
Computed values were being recomputed from scratch every time one of their dependency values changed. In tldraw, we have several large derived collections, such as which shapes to display and in which order, that can change on every frame and which are too expensive to recompute from scratch each time.
Computed values were only cached when they were being observed. In tldraw, many complex derived values are only used while processing user inputs, which is not considered a reactive ‘observing’ context. As a result, these values would be thrown away and recomputed on every frame unless we created mock listeners for them, which would leave us in danger of creating memory leaks.
Both issues were fundamental limitations of the framework’s reactivity model. Rather than create a potentially brittle solution by working outside of the framework, we decided the best way forward was to implement our own solution that used a different approach to reactivity.
Clocks and Diffs
Signia’s reactivity system is based on a single integer, a global logical clock, which is incremented any time a root state value is updated. Comparing clock values allows Signia to always cache derived values, regardless of whether they are being actively observed.
Knowing when a value last changed also allows our signals to emit a description of how they recently changed in addition to their actual current value. These change descriptions, or diffs, allow us to incrementally recompute a derived value. For example: if you are filtering a list you would only need to apply the filter predicate to new or updated items rather than every item in the list.
Signia represents only one of a family of projects created for tldraw to power its client-side state and multiplayer synchronization. While we prepare for tldraw’s own open source release, our plan is to release more of these projects that take advantage of Signia’s unique abilities.
In the meantime, we invite the developer community to explore Signia and consider its implications for more general-purpose utilities, for example by using Immer with Signia to create incrementally-computed immutable collections.
If you’re interested in learning more or have questions and comments, come join us on tldraw’s discord server. Good luck!