Cells is a powerful yet lightweight library for reactive state management in JavaScript applications. It offers an intuitive API that simplifies the complexities of managing and propagating state changes throughout your application.
- Simple API: Easy to learn and use, even for developers new to reactive programming.
- Lightweight: No external dependencies, keeping your project lean.
- Flexible: Works seamlessly with any JavaScript framework or vanilla JS.
- Type-safe: Built with TypeScript, providing excellent type inference and checking.
- Performant: Optimized for efficiency, with features like batched updates to minimize unnecessary computations.
Get started with Cells in your project:
npm install @adbl/cellsOr if you prefer Yarn:
yarn add @adbl/cellsSource cells are the building blocks of your reactive state. They hold values that can change over time, automatically notifying dependents when updates occur.
import { Cell } from '@adbl/cells';
const count = Cell.source(0);
console.log(count.get()); // Output: 0
count.set(5);
console.log(count.get()); // Output: 5Derived cells allow you to create computed values based on other cells. They update automatically when their dependencies change, ensuring your derived state is always in sync.
const count = Cell.source(0);
const doubledCount = Cell.derived(() => count.get() * 2);
console.log(doubledCount.get()); // Output: 0
count.set(5);
console.log(doubledCount.get()); // Output: 10Easily set up listeners to react to changes in cell values, allowing you to create side effects or update your UI in response to state changes.
const count = Cell.source(0);
count.listen((newValue) => {
console.log(`Count changed to: ${newValue}`);
});
count.set(3); // Output: "Count changed to: 3"
count.set(7); // Output: "Count changed to: 7"When you need to perform multiple updates but only want to trigger effects once, you can use batch updates to optimize performance:
const cell1 = Cell.source(0);
const cell2 = Cell.source(0);
const callback = () => {
console.log('Update occurred');
};
cell1.listen(callback);
cell2.listen(callback);
Cell.batch(() => {
cell1.set(1);
cell2.set(2);
});
// Output: "Update occurred" (only once)Cells provides utilities for handling asynchronous operations, making it easy to manage loading states, data, and errors:
const fetchUser = Cell.async(async (userId) => {
const response = await fetch(`https://api.example.com/users/${userId}`);
return response.json();
});
const { pending, data, error, run } = fetchUser;
pending.listen((isPending) => {
console.log(isPending ? 'Loading...' : 'Done!');
});
data.listen((userData) => {
if (userData) {
console.log('User data:', userData);
}
});
run(123); // Triggers the async operationWhen you call run() multiple times on the same async cell, any previous ongoing async operations are automatically aborted. This prevents race conditions and ensures that only the result of the latest run() call is applied.
Additionally, the getter function receives an AbortSignal via this.signal, which you can use to cancel long-running operations or check for abortion:
const fetchUser = Cell.async(async function (userId) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
signal: this.signal, // Pass the abort signal to fetch
});
return response.json();
});
const { data, run } = fetchUser;
// Start first request
run(123);
// Start second request - this aborts the first
run(456);
// Only the result of the second request (user 456) will be appliedCells offers utility functions to work with nested cell structures, making it easier to handle complex state shapes:
const nestedCell = Cell.source(Cell.source(5));
const flattenedValue = Cell.flatten(nestedCell);
console.log(flattenedValue); // Output: 5
const arrayOfCells = [Cell.source(1), Cell.source(2), Cell.source(3)];
const flattenedArray = Cell.flattenArray(arrayOfCells);
console.log(flattenedArray); // Output: [1, 2, 3]
const objectWithCells = { a: Cell.source(1), b: Cell.source(2) };
const flattenedObject = Cell.flattenObject(objectWithCells);
console.log(flattenedObject); // Output: { a: 1, b: 2 }For more complex objects, you can provide custom equality functions to determine when a cell's value has truly changed:
const userCell = Cell.source(
{ name: 'Alice', age: 30 },
{
equals: (a, b) => a.name === b.name && a.age === b.age,
}
);To aid in debugging, you can name your effects, making it easier to track and manage them:
const count = Cell.source(0);
count.listen((value) => console.log(`Count is now: ${value}`), {
name: 'countLogger',
});
console.log(count.isListeningTo('countLogger')); // Output: true
count.stopListeningTo('countLogger');When creating a source cell, you have fine-grained control over its behavior:
const cell = Cell.source(initialValue, {
immutable: boolean, // If true, the cell will not allow updates
deep: boolean, // By default, the cell only reacts to changes at the top level of objects. Setting deep to true will proxy the cell to all nested properties and trigger updates when they change as well.
equals: (oldValue, newValue) => boolean, // Custom equality function
});When setting up listeners or effects, you can customize their behavior:
cell.listen(callback, {
once: boolean, // If true, the effect will only run once
signal: AbortSignal, // An AbortSignal to cancel the effect
name: string, // A name for the effect (useful for debugging)
priority: number, // The priority of the effect (higher priority effects run first)
});By default, Cells uses WeakRef and Garbage Collection to manage memory. This is easy to use but can lead to "ghost computations", where listeners and derived cells keep running for a short time after they are no longer needed.
For high-performance scenarios, you can use a LocalContext to group dependencies and kill them synchronously.
const ctx = Cell.context();
const source = Cell.source(1);
Cell.runWithContext(ctx, () => {
// This listener is now bound to 'ctx' (Strong Reference)
source.listen((val) => console.log(val));
});
source.set(2); // Logs: 2
// Synchronously remove all listeners created in that block
ctx.destroy();
source.set(3); // Nothing happens