Reactive Collections: Automatic Reactivity for Arrays, Sets, and Maps
I'm excited to announce a powerful enhancement to use-less-react that dramatically simplifies state management when working with collections. Starting with version 0.10.1, makeReactiveProperties now supports automatic reactivity for Arrays, Sets, and Maps.
This means you can mutate collections using their native methodsβpush(), add(), set()βand React components will automatically re-render. No manual notify() calls, no cloning, no immutability gymnastics. Just natural, intuitive JavaScript.
The Problem: Managing Collection State in Reactβ
Working with collections in React has traditionally been verbose and error-prone. Consider a simple todo list:
class TodoStore extends PubSub {
todos: Todo[] = [];
constructor() {
super();
this.makeReactiveProperties('todos');
}
addTodo(text: string) {
// Before v0.10.1: you had to clone the array
this.todos = [...this.todos, { id: Date.now(), text, completed: false }];
// Or manually notify after mutation
// this.todos.push({ id: Date.now(), text, completed: false });
// this.notify('todos');
}
}
This approach has several drawbacks:
- Verbose: Every mutation requires either array cloning or manual notifications
- Error-Prone: It's easy to forget the
notify()call or spread operator - Unnatural: Developers must abandon native array methods they know and love
- Performance: Unnecessary allocations when cloning large arrays
The Solution: Reactive Proxiesβ
With v0.10.1, collections wrapped by makeReactiveProperties become reactive proxies that automatically detect mutations and trigger React re-renders.
Reactive Arraysβ
Arrays now automatically notify on all mutating operations:
class TodoStore extends PubSub {
todos: Todo[] = [];
constructor() {
super();
this.makeReactiveProperties('todos');
}
addTodo(text: string) {
// β¨ Just use native push() - automatic notification!
this.todos.push({
id: Date.now(),
text,
completed: false
});
}
toggleTodo(id: number) {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) {
// β¨ Index assignment also notifies automatically
this.todos[index] = {
...this.todos[index],
completed: !this.todos[index].completed
};
}
}
removeTodo(id: number) {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) {
// β¨ splice() automatically notifies
this.todos.splice(index, 1);
}
}
}
Supported mutating methods:
- Index assignments:
arr[0] = value push(),pop(),shift(),unshift()splice(),sort(),reverse()fill(),copyWithin()
Non-mutating methods like map(), filter(), find(), slice() don't trigger notifications (as expected).
Reactive Setsβ
Sets become reactive proxies that notify on mutations:
class TagManager extends PubSub {
tags: Set<string> = new Set();
constructor() {
super();
this.makeReactiveProperties('tags');
}
addTag(tag: string) {
// β¨ Automatically notifies
this.tags.add(tag.toLowerCase());
}
removeTag(tag: string) {
// β¨ Automatically notifies
this.tags.delete(tag.toLowerCase());
}
clearTags() {
// β¨ Automatically notifies
this.tags.clear();
}
}
Supported mutating methods:
add(value)delete(value)clear()
Reactive Mapsβ
Maps also get automatic reactivity:
class ConfigStore extends PubSub {
settings: Map<string, string> = new Map();
constructor() {
super();
this.makeReactiveProperties('settings');
}
setSetting(key: string, value: string) {
// β¨ Automatically notifies
this.settings.set(key, value);
}
removeSetting(key: string) {
// β¨ Automatically notifies
this.settings.delete(key);
}
}
Supported mutating methods:
set(key, value)delete(key)clear()
Mix and Match: Collections + Primitivesβ
You can combine reactive collections with primitive reactive properties seamlessly:
class AppState extends PubSub {
items: string[] = [];
tags: Set<string> = new Set();
metadata: Map<string, unknown> = new Map();
count = 0;
constructor() {
super();
// All properties become reactive
this.makeReactiveProperties('items', 'tags', 'metadata', 'count');
}
increment() {
this.count++; // Notifies
}
addItem(item: string) {
this.items.push(item); // Notifies
}
addTag(tag: string) {
this.tags.add(tag); // Notifies
}
setMeta(key: string, value: unknown) {
this.metadata.set(key, value); // Notifies
}
}
Batch Notifications for Bulk Operationsβ
As always, when performing multiple mutations, use batchNotifications() to trigger only a single React re-render:
class TodoStore extends PubSub {
todos: Todo[] = [];
tags: Set<string> = new Set();
constructor() {
super();
this.makeReactiveProperties('todos', 'tags');
}
async bulkAdd(items: string[]) {
await this.batchNotifications(async () => {
items.forEach(text => {
// Multiple mutations...
this.todos.push({ id: Date.now(), text, completed: false });
// Extract hashtags
const tags = text.match(/#\w+/g) || [];
tags.forEach(tag => this.tags.add(tag));
});
// Only ONE notification sent to React!
});
}
}
See full specs of batchNotifications here
Important: WeakMap and WeakSet are not supportedβ
Due to their non-enumerable nature, WeakMap and WeakSet cannot be made reactive. If you attempt to make them reactive, an error will be thrown:
class Cache extends PubSub {
cache: WeakMap<object, string> = new WeakMap();
constructor() {
super();
// β Throws error
this.makeReactiveProperties('cache');
}
}
Solution: Use Map or Set instead for reactive collections.
Important: map, forEach and all non-mutating methods won't automatically call notifyβ
All non-mutating methods like map, forEach, reduce etc. won't automatically call notify. Why? Because they aren't inherently mutators. We cannot make assumptions on how they're used.
So if you need to use one of these methods, please call notify manually, i.e.
class SomeClass extends PubSub {
someArray: Array<{ isChecked: boolean }> = [];
someMethod() {
this.someArray.forEach(item => {
if (someCondition) {
item.isChecked = true;
this.notify("someArray"); // call notify manually
}
})
}
}
Using Reactive Collections in React Componentsβ
Consuming reactive collections in components with useReactiveInstance is easy, but you have to pay attention to a subtle detail. If the reactive collection is a Set, for instance, you cannot simply write:
function TodoList() {
const { state, instance } = useReactiveInstance(
() => new MyClass(),
(instance) => ({
someSet: instance.someSet,
}),
['someSet']
);
}
Why? Because the reference of the Set is stable, so no changes will be detected on it. You will have to convert it to an immutable object or array, in order for useReactiveInstance to actually detect changes.
For instance, if someSet: Set<string>Β you could transform it in an array of strings:
function TodoList() {
const { state, instance } = useReactiveInstance(
() => new MyClass(),
(instance) => ({
someSet: Array.from(instance.someSet),
}),
['someSet']
);
}
Now, once notified, useReactiveInstance will detect that the new snapshot is different from the previous, and the reactive value will be updated correctly!
TLDR; you cannot return stable instance references in the getSnapshot function of useReactiveInstance, so please transform reactive collections accordingly.
The Benefitsβ
β
Zero Boilerplate - No manual notify() calls for collections
β
Intuitive API - Use native array/set/map methods as usual
β
Type-Safe - Full TypeScript support and inference
β
Performance - Only mutating operations trigger notifications
β
Batching Support - Multiple mutations can be batched efficiently
β
Developer Experience - Write code that feels natural and idiomatic
Migration from Previous Versionsβ
If you're already using makeReactiveProperties with collections, your code will continue to work. The enhancement is backward compatible:
Before v0.10.1:
addTodo(text: string) {
this.todos = [...this.todos, { id: Date.now(), text, completed: false }];
// Still works, but now you can use push() instead!
}
After v0.10.1:
addTodo(text: string) {
// Simpler and more intuitive
this.todos.push({ id: Date.now(), text, completed: false });
}
Final Noteβ
Reactive collections are available starting from v0.10.1.
For full API details and advanced usage, check out the makeReactiveProperties documentation.
With reactive collections, use-less-react takes another step toward making state management in React applications simple, predictable, and enjoyable!
