State management library virtual DOM evaluation and comparison
Highlighted feature: As part of this study I created (and improved upon) a React implementation of Qwik's useStore hook. This creates a signal proxy for any nested object or array (or objects with nested arrays!). See the useStore/nested tab for further information and demonstration.
Technical note: This page is using parallel routes - each of the "tabs" below is a separate route. Click each item and observe the change in the address bar.
This page has been included to compare the efficiency of state updates on virtual DOM updates in a variety of leading state management solutions with a detailed look at Signals. Signals are one of the most recent additions to the state management ecosystem: introduced in Solid JS, adopted (and improved) by Qwik JS, with other frameworks following suit. There is currently no native implementation for React but the Preact extension integrates well, and is used and demonstrated here.
Signals in a nutshell: Signals update UI directly completely bipassing the DOM for the most efficient and reactive UI updates possible. Updating a signal value will update its value in a parent UI without re-rendering its descendent node tree. For complex node trees this is a huge performance gain.
Conclusion of this study: None of the legacy state management solutions tested offer truly reactive state updates and only Signals can update state directly without updating the virtual DOM. e.g. nanostores, recoil, zustand and redux (by default) all update the virtual dom and descendent child nodes when state is changed in a parent, though each does so with varying degrees of effiency. However it should be noted that Redux integrates well with signals which can be used for efficient updates where necessary in any application.
Reactive objects and arrays
This route exists to test a React implementaion of Qwik's useStore hooks for proxying reactive nested objects. This is the entire useStore implementation.
Beneath the code are some working examples of reactifying deeply nested objects, objects with nested arrays, and bare arrays.
At the time of writing the latter example extends beyond Qwik functionality.
import { signal, Signal } from "@preact/signals-react";
import { useRef } from "react";
type NestedObject = {
[key: string]: any;
};
export type SignalsObject<T> = {
[K in keyof T]: T[K] extends Array<infer U>
? Signal<U>[]
: T[K] extends object
? SignalsObject<T[K]>
: Signal<T[K]>;
};
const convertToSignals = <T extends NestedObject>(obj: T): SignalsObject<T> => {
if (Array.isArray(obj)) {
return obj.map((item) =>
typeof item === "object" && item !== null ? convertToSignals(item) : signal(item)
) as SignalsObject<T>;
}
const result: any = {};
for (const key in obj) {
if (Array.isArray(obj[key])) {
result[key] = obj[key].map((item: any) =>
typeof item === "object" && item !== null ? convertToSignals(item) : signal(item)
);
} else if (typeof obj[key] === "object" && obj[key] !== null) {
result[key] = convertToSignals(obj[key]);
} else {
result[key] = signal(obj[key]);
}
}
return result;
};
export const useStore = <T extends NestedObject>(initialObject: T): SignalsObject<T> => {
const objectRef = useRef<SignalsObject<T> | null>(null);
if (!objectRef.current) {
objectRef.current = convertToSignals(initialObject);
}
return objectRef.current;
};
export default useStore;
Reactive Object with nested array
ObjectGet
ID: test-id
Values:
Value 1: 1
Value 2: 2
Value 3: 3
ObjectSet
Reactive deeply nested objects
ObjectGet
First name: Alice
Last name: Smith
Street: 123 Main St
City: Wonderland
ObjectSet
Reactive array
ArrayGet
Item 1: spain
Item 2: italy
Item 3: france