diff --git a/README.md b/README.md index 7b77dc2..908ae78 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,6 @@ When creating a source cell, you have fine-grained control over its behavior: ```javascript 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 }); ``` diff --git a/library/classes.js b/library/classes.js index ff44eb4..67bc56a 100644 --- a/library/classes.js +++ b/library/classes.js @@ -36,8 +36,6 @@ * @typedef {object} CellOptions * @property {boolean} [immutable] * Whether the cell should be immutable. If set to true, the cell will not allow updates and will throw an error if the value is changed. - * @property {boolean} [deep] - * Whether the cell should watch for changes deep into the given value. By default the cell only reacts to changes at the top level. * @property {(oldValue: T, newValue: T) => boolean} [equals] * A function that determines whether two values are equal. If not provided, the default equality function will be used. */ @@ -226,27 +224,6 @@ function throwAnyErrors() { } } -const mutativeMapMethods = new Set(['set', 'delete', 'clear']); -const mutativeSetMethods = new Set(['add', 'delete', 'clear']); -const mutativeArrayMethods = new Set([ - 'push', - 'pop', - 'shift', - 'unshift', - 'splice', - 'sort', - 'reverse', -]); -const mutativeDateMethods = new Set([ - 'setDate', - 'setMonth', - 'setFullYear', - 'setHours', - 'setMinutes', - 'setSeconds', - 'setMilliseconds', -]); - /** @template T */ class Effect { /** @@ -936,8 +913,6 @@ export class DerivedCell extends Cell { * @extends {Cell} * A cell whose value can be directly modified. * Source cells are the primary way to introduce reactivity. - * They can hold any value type and will automatically handle proxying of objects - * to enable deep reactivity when needed. * * @example * ```typescript @@ -975,7 +950,7 @@ export class SourceCell extends Cell { * @returns {T} The value of the Cell. */ get() { - return this.#proxy(this.revalued); + return this.revalued; } /** @@ -999,71 +974,6 @@ export class SourceCell extends Cell { UPDATE_BUFFER.push(this); if (!IS_UPDATING) triggerUpdate(); } - - /** - * Proxies the provided value deeply, allowing it to be observed and updated. - * @template T - * @param {T} value - The value to be proxied. - * @returns {T} - The proxied value. - */ - #proxy(value) { - if (typeof value !== 'object' || value === null) return value; - return new Proxy(value, { - get: (target, prop) => { - this.revalued; - if (this.options?.deep) { - // @ts-expect-error: Direct access is faster than Reflection here. - return this.#proxy(target[prop]); - } - - if (typeof prop === 'string') { - const isMutativeMethod = - (target instanceof Map && mutativeMapMethods.has(prop)) || - (target instanceof Set && mutativeSetMethods.has(prop)) || - (target instanceof Date && mutativeDateMethods.has(prop)) || - ((ArrayBuffer.isView(target) || Array.isArray(target)) && - mutativeArrayMethods.has(prop)); - - if (isMutativeMethod) { - // @ts-expect-error: Direct access is faster than Reflection here. - return (...args) => { - // @ts-expect-error: Direct access is faster than Reflection here. - const result = target[prop](...args); - UPDATE_BUFFER.push(this); - this[IsScheduled] = true; - if (!IS_UPDATING) triggerUpdate(); - return result; - }; - } - } - - // @ts-expect-error: Direct access is faster than Reflection here. - let value = target[prop]; - - if (typeof value === 'function') { - value = value.bind(target); - } - - return value; - }, - set: (target, prop, value) => { - // @ts-expect-error: dynamic object access. - const formerValue = target[prop]; - const isEqual = deepEqual(formerValue, value); - if (!isEqual) { - // @ts-expect-error: dynamic object access. - target[prop] = value; - UPDATE_BUFFER.push(this); - this[IsScheduled] = true; - if (!IS_UPDATING) { - triggerUpdate(); - } - } - - return true; - }, - }); - } } /** diff --git a/tests/index.test.js b/tests/index.test.js index 83d2d08..7b68300 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -19,13 +19,10 @@ describe('Cells', () => { }); test('Cell should ignore updates for deeply equal values', () => { - const cell = Cell.source( - { - a: 1, - b: { c: 2, d: 3 }, - }, - { deep: true }, - ); + const cell = Cell.source({ + a: 1, + b: { c: 2, d: 3 }, + }); const callback = vi.fn(); cell.listen(callback); @@ -40,12 +37,6 @@ describe('Cells', () => { b: { c: 2, d: 4 }, }); expect(callback).toHaveBeenCalledTimes(1); - - cell.get().b.c = 2; - expect(callback).toHaveBeenCalledTimes(1); - - cell.get().b.c = 67; - expect(callback).toHaveBeenCalledTimes(2); }); test('Creates a reactive Cell with null or undefined', () => { @@ -747,156 +738,6 @@ describe('Derived cells', () => { }); }); -describe('Nested cells', () => { - test('Cell of object type should be reactive', () => { - const cell = Cell.source({ a: 1 }); - const cell2 = Cell.source({ b: 10 }); - - const derived = Cell.derived(() => cell.get().a + cell2.get().b); - - cell.get().a = 2; // Deep reactivity test - expect(derived.get()).toBe(12); - - cell2.get().b = 20; // Deep reactivity test - expect(derived.get()).toBe(22); - }); - - test('Derived cell of object type should run callback when value changes', () => { - const cell = Cell.source({ a: 'hello', b: 1, c: true, d: null }); - const callback = vi.fn(); - - const derived = Cell.derived(() => { - callback(); - return cell.get().a; - }); - expect(derived.get()).toBe('hello'); - expect(callback).toHaveBeenCalledTimes(1); - - cell.set({ a: 'world', b: 2, c: false, d: null }); - expect(callback).toHaveBeenCalledTimes(2); - }); - - test('Cell of map type should be able to read entries', () => { - const cell = Cell.source(new Map()); - cell.get().set('a', 1); - cell.get().set('b', 2); - - const array = Cell.derived(() => Array.from(cell.get().entries())); - - cell.get().set('c', 3); - - expect(array.get()).toEqual([ - ['a', 1], - ['b', 2], - ['c', 3], - ]); - }); - - test('Cell of array type should be reactive', () => { - const cell = Cell.source([1, 2, 3]); - - const sum = Cell.derived(() => cell.get().reduce((a, b) => a + b, 0)); - expect(sum.get()).toBe(6); - - cell.get()[0] = 3; // Deep reactivity test - expect(sum.get()).toBe(8); - - cell.get().push(4); // Deep reactivity test - expect(sum.get()).toBe(12); - - cell.get().pop(); // Deep reactivity test - expect(sum.get()).toBe(8); - }); - - test('Cell of nested array type should be reactive', () => { - /** @type {SourceCell<[number, [number, number], number]>} */ - const cell = Cell.source([1, [2, 3], 4], { deep: true }); - const d1 = Cell.derived(() => cell.get()[1][1] + 2); - const d2 = Cell.derived(() => cell.get()[1][0] + d1.get()); - - expect(d1.get()).toBe(5); - expect(d2.get()).toBe(7); - cell.get()[1][1] = 5; // Deep reactivity test - - expect(d1.get()).toBe(7); - expect(d2.get()).toBe(9); - }); - - test('Cells of maps should be reactive', () => { - const cell = Cell.source(new Map()); - const derived = Cell.derived(() => cell.get().get('a')); - - expect(derived.get()).toBe(undefined); - - cell.get().set('a', 1); // Deep reactivity test - expect(derived.get()).toBe(1); - - cell.get().set('a', 2); // Deep reactivity test - expect(derived.get()).toBe(2); - }); - - test('Cells of sets should be reactive', () => { - const cell = Cell.source(new Set()); - const derived = Cell.derived(() => cell.get().has(1)); - const size = Cell.derived(() => cell.get().size); - - expect(derived.get()).toBe(false); - expect(size.get()).toBe(0); - - cell.get().add(1); // Deep reactivity test - expect(derived.get()).toBe(true); - expect(size.get()).toBe(1); - - cell.get().add(2); // Deep reactivity test - expect(derived.get()).toBe(true); - expect(size.get()).toBe(2); - }); - - test('Cells of dates should be reactive', () => { - const cell = Cell.source(new Date()); - const callback = vi.fn(); - cell.listen(callback); - - cell.set(new Date(2022, 1, 1)); - expect(callback).toHaveBeenCalledTimes(1); - cell.set(new Date(2022, 1, 1)); - expect(callback).toHaveBeenCalledTimes(1); - - cell.get().setMonth(2); - expect(callback).toHaveBeenCalledTimes(2); - }); - - test('Cell should handle built-in operators on objects', () => { - const cell = Cell.source({ a: 1, b: 2 }); - const derived = Cell.derived(() => cell.get().a + cell.get().b); - - cell.get().a += 2; - expect(derived.get()).toBe(5); - - cell.get().b += 2; - expect(derived.get()).toBe(7); - - cell.get().a++; - expect(derived.get()).toBe(8); - - cell.get().b--; - expect(derived.get()).toBe(7); - }); - - test('Cell should handle built-in operators on arrays', () => { - const cell = Cell.source([1, 2, 3]); - const derived = Cell.derived(() => { - return cell.get().map((x) => x + 5); - }); - - expect(derived.get()).toEqual([6, 7, 8]); - - cell.get()[0]++; - - expect(derived.get()).toEqual([7, 7, 8]); - }); -}); - describe('Batched effects', () => { test('Batched effects should run only once', () => { const callback = vi.fn(); @@ -3609,25 +3450,6 @@ describe('Effect options', () => { }); describe('Cell options', () => { - test('Cells should be deeply proxied if specified', () => { - const cell = Cell.source({ a: 1, b: { c: 5 } }, { deep: true }); - const callback = vi.fn(); - cell.listen(callback); - cell.get().b.c = 2; // Deep reactivity test - expect(callback).toHaveBeenCalledTimes(1); - }); - - test('Cells should be shallowly proxied by default', () => { - const cell = Cell.source({ a: 1, b: { c: 5 } }); - const callback = vi.fn(); - cell.listen(callback); - cell.get().a = 2; // Deep reactivity test - expect(callback).toHaveBeenCalledTimes(1); - - cell.get().b.c = 90; // Should not trigger update - expect(callback).toHaveBeenCalledTimes(1); - }); - test('Immutable cells should not allow updates', () => { const cell = Cell.source(1, { immutable: true }); expect(() => {