Building the State Management Library
@superlucky84|September 14, 2025 (1m ago)361 views

- Table of Contents
- Overview
- Signal Pattern
- Lens
- Using Lens
- Combining Signal and Lens Patterns
- Registering Subscription Functions
- Bound References
- Unbound References
- Bound Outer References
- Canceling Subscriptions
- Conclusion
Overview
As a hobby, I built a library using concepts I had been curious about and had been sharing what I learned along the way.
"> There are already other series available: Building My Own Custom React , Creating My Own SSR Framework.
This time, I’m sharing the story of creating a state management library (state-ref)
To implement the state management library, I needed to understand a few concepts: the Signal pattern
and Lens
, which is used in functional programming.
Signal Pattern
One of the main motivations for creating state-ref
was that I wanted to experiment with the Signal pattern and build something practical using it.
When I first set out to build it, I knew what I wanted to implement, but I didn’t realize that the pattern matched the Signal pattern. I understood the concept, but not the terminology.
I just thought it was a convenient form of the Observer pattern, and it wasn’t until long after I had finished that I learned this pattern is actually called the Signal pattern.
The Signal pattern is a method where a subscription relationship is automatically formed the moment a user tries to access a specific value. The collected callback functions are executed whenever that value is updated. In other words, it’s a pattern that naturally embeds ‘subscription’ within the execution flow, without explicitly specifying the usual ‘subscribe → execute’ process.
The code below is a simplified excerpt of the core implementation of the Signal pattern in the preact/signal
library. It is implemented using JavaScript’s property accessors.
When a specific value is accessed via a property accessor, the addDependency function registers the corresponding subscription callbacks.
When the value is modified, access occurs through the property setter (set
), which triggers any subscription functions bound to that value, e.g., node._target._notify();
.
Object.defineProperty(Signal.prototype, "value", {
get(this: Signal) {
const node = addDependency(this);
...
return this._value;
},
set(this: Signal, value) {
this._value = value;
...
node._target._notify(); // Execute subscription functions
...
},
});
In preact/signal
, you can register subscription functions using the effect
function.
In the example below, the subscription function calculates the number of elements in a matrix by multiplying rowCount
and columnCount
.
When a specific property of the matrix object is accessed inside the subscription function, it is automatically added to the dependency list. Later, if the value changes, the function defined within effect is executed.
effect(() => {
const matrixCount = matrix.rowCount * matrix.columCount;
console.log(matrixCount);
});
Simple, right? In the frontend field, this concept is already widely known and familiar. Additionally, libraries such as Vue, MobX, and SolidJS are also known to utilize the signal pattern.
Actually, this is not a new concept; I only recently realized that it has been widely used for quite some time.
Lens
When I decided to create a state management library, the part I struggled with the most was how to track values.
For example, if you are using a state object created with Zustand
in a specific component, you can subscribe to the exact value you want by specifying it precisely using dot notation, as shown below.
export default function Board() {
// Accessing nested values in a state object
const squares = useGameStore((state) => state.game.board.squares);
...
}
I wanted to actually implement a feature that could precisely subscribe to specific values within nested objects, like in the example above. I also had a small wish to peek at how other libraries tackled this problem.
However, contrary to my plans, I didn’t have much free time back then due to childcare and other responsibilities, so I couldn’t check or test other code. As a result, whenever I got a brief moment, the best I could do was mentally simulate the code and imagine, ‘This is how I could implement it.’
What came to my mind during this process was the concept of a Lens
.
Frontend developers often need to maintain data purity when manipulating state. For example, when using libraries like React-Redux
, they often rely on immer
to handle such scenarios.
Lens serves a similar purpose as immer
, but it is designed to be more declarative and easier to work with, in line with the philosophy of functional programming.
In any case, the Lens
pattern is extremely convenient when you want to track specific values within deeply nested objects.
Using Lens
Lens
is a concept that originated in the functional programming language Haskell, but in JavaScript, many developers have implemented and used it in various ways.
While creating state-ref, I also implemented Lens in a way that was convenient for me to work with. Below is an example of how to use it.
const targetObject1 = { a: { b: { c: { k: 1, j: 2, w: 3 } } } };
const targetObject2 = { a: { b: { c: { k: 1, j: 11, w: 111 } } } };
const lensInstance = lens().chain("a").chain("b").chain("c").chain("j");
// Accessing values in a nested object
console.log(lensInstance.get(targetObject1)); // return 2
console.log(lensInstance.get(targetObject2)); // return 11
// Modifying values in a nested object using copy-on-write
lensInstance.set(7)(targetObject2);
As shown in the code above, using the chain
method allows you to simply express the intention of "which value I want to access." The actual object from which the value is retrieved is determined at the moment get()
is called.
Using this pattern, even if the object reference changes, you can always retrieve the latest value as long as you know the "property path" stored in the Lens instance.
When subscribing to a deeply nested value, if you store the lensInstance
object that records the reference path along with the value at the time of subscription, you can call lensInstance.get()
when the value changes to check whether it has actually been updated.
The process proceeds as follows:
- Store a pair consisting of the value at the time of subscription and the corresponding
Lens
instance. - When a change in the object is detected via the observer pattern, compare the stored original value with the updated value (the latest value at the changed location can be retrieved using the
Lens
). - If the values differ, consider it a change and execute the subscriber functions registered via the signal pattern.
Copy-on-Write
When modifying a value using set, it operates in a copy-on-write manner.
Copy-on-write is a technique frequently used in functional programming that allows changes to be handled while maintaining data immutability.
When updating a specific value in an object, the original object itself is not modified; only the parts that need to change are copied into a new object, while the rest continues to reference the existing object. This allows the program to easily determine whether an object has changed using simple reference comparison, ensuring both performance and stability.
By leveraging this feature of Lens
, it becomes straightforward to track whether a specific part of a subscribed object’s state has changed.
Combining Signal and Lens Patterns
The state-ref
I created is, simply put, an object that combines the characteristics of the signal pattern and the Lens
pattern.
What makes it unique, however, is that values can only be read or set through .value
.
For example, to directly use the value of state.a.b
in a specific object, you need to append .value
.
console.log(state.a.b.value);
And when you need to set a new value to state.a.b
, you assign it using .value =
as shown in the example below.
state.a.b.value = 3;
The process of accessing and assigning values is abstracted through .value
.
This approach is inspired by the way preact/signal
works, enforcing that all reads and writes go through .value
, which makes data access clear and keeps both implementation and usage intuitive and simple.
To combine the characteristics of the signal pattern and the Lens pattern, we utilized Proxy
Here is a simplified example of the core state-ref
code that combines the signal pattern with the Lens
pattern using Proxy.
export function makeProxy(value, lensInstance, subscribeCallback) {
return new Proxy(
...
{
/**
* When accessing a value or reference from an object
*/
get(_: T, prop: keyof T) {
/**
* When accessing via .value
*/
if (prop === 'value') {
/**
* Collect subscription callbacks
*/
collector(
lensInstance.get(rootValue),
() => lensInstance.get(rootValue),
subscribeCallback,
);
return value;
}
// Using dot notation allows the Lens to be continuously chained.
const chainedLensInstance = lensInstance.chain(prop);
const propertyValue: any = lens.get(rootValue);
return makeProxy(propertyValue, chainedLensInstance, subscribeCallback);
},
/**
* When modifying a value or updating a reference in an object
*/
set(_, prop: string | symbol, value) {
if (prop !== 'value') {
throw new Error('Can only be assigned to a "value".');
} else if (prop === 'value' && value !== lensValue.get(rootValue)) {
const newTree = lensValue.set(value)(rootValue);
rootValue.root = newTree.root;
// Execute collected subscription callbacks
// First argument (storeRenderList): list of subscription callbacks (referenced from makeProxy's outer scope)
// Second argument (rootProxy): root proxy of the store (referenced from makeProxy's outer scope)"
runner(storeRenderList, rootProxy);
}
return true;
},
}
);
}
Accessing and Consuming Values
In this code, when you access a value using .value—for example, console.log(state.a.b.value);
—the proxy's get handler is triggered.
When get
is executed, the subscribeCallback
, the current value at that location lensValue.get(rootValue)
, and a function to retrieve the value at a future change () => lensValue.get(rootValue)
are stored via the collector function, so they can be executed at the time of change.
Updating Values
When a value is changed, like state.a.b.value = 3;
, the proxy’s set
is triggered.
When set
is called, it compares the previously collected value from get
with the newly assigned value. If the two values differ, the Lens instance’s set
updates the original object using a copy-on-write approach.
Finally, the subscribeCallbacks
collected at the get stage are executed to reflect the changes.
Registering Subscription Functions
Like the effect
in preact/signal
mentioned earlier, state-ref
provides a watch
function to register subscription callbacks.
Below is a simplified implementation of the watch
function for illustration purposes.
function watch(subscribeCallback) {
const lensRootInstance = lens();
const rootProxy = makeProxy({
rootValue,
lensRootInstance,
subscribeCallback,
});
// Execute once initially to collect subscriptions
subscribeCallback(rootProxy, true);
return rootProxy;
}
The watch
function takes a callback that should be executed whenever the store changes.
Using the previously introduced makeProxy
function, it creates a rootProxy
object.
This proxy object simultaneously serves as a Lens
for accessing nested properties and as a mechanism to subscribe to and react to changes in the original object.
Bound References
When you register a subscription function via watch
, it is executed once initially to collect dependencies.
At this time, the rootProxy
is passed as an argument, like subscribeCallback(rootProxy, true)
. The second argument indicates whether this is the first run for dependency collection.
It’s easier to understand if you imagine defining and using a subscription function as shown in the example below.
const matrixSubscribeCallback = (innerRef, isFirst) => {
const matrixCount = innerRef.rowCount.value * innerRef.columCount.value;
console.log(matrixCount);
};
// References bound to a specific subscription function
const outerRef = watch(matrixSubscribeCallback);
// References not bound to any subscription function
const anotherRef = watch();
In the code above, the first argument of matrixSubscribeCallback
, innerRef
is the root proxy object of the store. The outerRef
returned by watch is the same reference as innerRef
.
These innerRef
and outerRef
are connected to matrixSubscribeCallback.
Therefore, when you access values from innerRef
or outerRef
(e.g., console.log(ref.rowCount.value);
), the corresponding subscription information is registered via the collector
function. Subsequently, whenever the rowCount
value changes, the connected subscription function is executed.
In the matrixSubscribeCallback
example, the subscription function runs whenever rowCount
or columnCount
changes.
The second parameter,
isFirst
, returnstrue
on the function’s initial execution andfalse
thereafter, allowing you to distinguish between a run triggered by actual changes and one for dependency collection.
Unbound References
On the other hand, anotherRef
is not connected to any subscription function. This is because, although watch
was executed to obtain the reference, no subscription function was passed as an argument.
Therefore, accessing values through anotherRef
will not trigger any subscription function.
As shown in the example below, even if anotherRef
is directly referenced within the matrixSubscribeCallback
subscription function, it is not collected by the collector
. In other words, anotherRef.etcCount
is not linked to any callback function, so changes to its value will not cause matrixSubscribeCallback
to run.
// References not bound to any subscription function
const anotherRef = watch();
const matrixSubscribeCallback = (innerRef, isFirst) => {
const matrixCount =
innerRef.rowCount.value *
innerRef.columCount.value *
anotherRef.etcCount.value;
console.log(matrixCount);
};
// References bound to a specific subscription function
const outerRef = watch(matrixSubscribeCallback);
You can leverage this characteristic to use certain values without having them collected by subscriptions. In other words, it allows users to clearly distinguish between cases where a value is tracked and cases where it is not.
The underlying principle is that when a proxy object is created, the subscription function injected into watch
gets linked to the collected dependency information through the collector
.
return new Proxy(
...
{
get(_: T, prop: keyof T) {
if (prop === 'value') {
/**
* Collecting subscription callbacks
*/
collector(
lensInstance.get(rootValue),
() => lensInstance.get(rootValue),
subscribeCallback,
);
return value;
}
}
...
}
...
);
Bound Outer References
Besides the innerRef
reference object used inside the subscription function, as seen in the previous example, the outerRef
returned by watch
allows access outside the subscription callback. This design makes it easier to integrate with components in a UI library.
To illustrate, I’ll use my hobby project, the component-based UI library lithent, as an example.
const Component = mount((renew) => {
let count = 1;
const change = () => {
count += 1;
renew();
};
return () => <button onClick={change}>{count}</button>;
});
mount
is a function that creates a component, and it provides a renew
function as the first argument to the function it consumes.
The renew
function requests an update for the component. In the example below, it increments the count
value by 1 and then re-renders the component.
Note that, in general, directly calling a function like
lithent
’srenew
can be considered an anti-pattern, as it breaks the reactive declarative paradigm where state changes are automatically propagated. Nevertheless, we adopted manual updates as the primary interface to keep state management as simple as possible using native closures, without tying it to a specific pattern. Moreover, since the library was designed for light and convenient use by its creator, personal needs and practical considerations are naturally reflected.
If you want to share store values using state-ref
instead of the component’s internal count
state, you can do so as follows.
const watch = createStore(1); // watch 생성
const Component = mount((renew) => {
count countRef = watch(renew);
const change = () => {
countRef.value += 1;
};
return () => <button onClick={change}>{count.value}</button>;
});
By returning a proxy reference externally via watch
, you can easily collect the subscription points outside of the subscription function, making it useful in various scenarios.
Using outerRef
, you can effortlessly connect components with state.
Building on this feature, you can also connect state easily in React
and Preact
using simple snippets:
It is designed to operate non-invasively without modifying the UI library, using only the basic
useState
functionality and a simple subscription pattern. If needed, snippets based onuseSyncExternalStore
can also be easily customized.
Canceling Subscriptions
There are two ways to implement subscription cancellation.
Cancellation Using AbortSignal
The first method leverages AbortSignal.
While this feature is commonly used to cancel fetch
requests, it can also be applied to custom event handling.
To cancel a subscription, we return an AbortSignal
from the subscription function, which allows the cancellation to be handled accordingly.
Below is a concrete example of its usage.
const abortController = new AbortController();
watch((stateRef) => {
console.log(
"Changed John's Second House Color",
stateRef.john.house[1].color.value
);
return abortController.signal;
});
abortController.abort(); // run abort
To cancel a subscription, an AbortController
is created, and the subscription function is set up to return it when executed.
The following code is a simplified example of a function that runs alongside the first execution for collecting subscription values:
// Executes when the subscription function runs for the first time to collect subscriptions
function subscribeFirstRunner(subscribeCallback, storeRenderList) {
const result = subscribeCallback(rootProxy, true);
if (result instanceof AbortSignal) {
result.addEventListener("abort", () => {
storeRenderList.delete(subscribeCallback);
});
}
}
If the value returned by subscribeCallback
is an AbortSignal
, an abort
event listener is bound to this signal
object.
When abortController.abort();
is called, the registered listener executes and removes the subscription function from the collected subscription list.
Canceling by Returning false
While using AbortSignal
provides a reliable and useful way to cancel subscriptions, it can be somewhat cumbersome.
As an alternative, you can cancel a subscription easily when certain conditions are met. In the example below, the subscription is canceled by checking a condition and returning false
.
watch((stateRef) => {
const color = stateRef.john.house[1].color.value;
console.log("Changed John's Second House Color", color);
return color === "red" ? false : true;
});
When a value changes, the runner
in the example below is executed (called from the setter
in the earlier makeProxy
example).
If the subscription callback returns exactly false
after execution, the corresponding subscription function is removed from the collected subscription list.
function runner(storeRenderList, rootProxy) {
...
runableRenewList.forEach(subscribeCallback => {
if (subscribeCallback(rootProxy, false) === false) {
storeRenderList.delete(run);
}
});
...
}
Conclusion
My motivation was partly pure curiosity, but in reality, I created this state management library to solve a specific problem.
The company I work for has a traditional structure where multiple files are loaded to build a page. The preact/signal
library I had used before couldn’t share state across files when built separately, because each file would run in a different runtime context.
To allow all files to freely reference the same state, I created state-ref
.
I hope this library can be a small help for those, like me, who want lightweight and flexible state management in legacy environments. Even if you don’t use the code as-is, if you enjoyed reading this article, I believe the concepts and code are simple enough to implement or adapt on your own.
If you found it even a little enjoyable, please give a star to the state-ref repository. A single click brings me great joy.