import { type ComputedRef, computed } from '../src/computed' import { isReactive, reactive, shallowReactive, toRaw } from '../src/reactive' import { isRef, ref } from '../src/ref' import { effect } from '../src/effect' describe('reactivity/reactive/Array', () => { test('should make Array reactive', () => { const original = [{ foo: 1 }] const observed = reactive(original) expect(observed).not.toBe(original) expect(isReactive(observed)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReactive(observed[0])).toBe(true) // get expect(observed[0].foo).toBe(1) // has expect(0 in observed).toBe(true) // ownKeys expect(Object.keys(observed)).toEqual(['0']) }) test('cloned reactive Array should point to observed values', () => { const original = [{ foo: 1 }] const observed = reactive(original) const clone = observed.slice() expect(isReactive(clone[0])).toBe(true) expect(clone[0]).not.toBe(original[0]) expect(clone[0]).toBe(observed[0]) }) test('observed value should proxy mutations to original (Array)', () => { const original: any[] = [{ foo: 1 }, { bar: 2 }] const observed = reactive(original) // set const value = { baz: 3 } const reactiveValue = reactive(value) observed[0] = value expect(observed[0]).toBe(reactiveValue) expect(original[0]).toBe(value) // delete delete observed[0] expect(observed[0]).toBeUndefined() expect(original[0]).toBeUndefined() // mutating methods observed.push(value) expect(observed[2]).toBe(reactiveValue) expect(original[2]).toBe(value) }) test('Array identity methods should work with raw values', () => { const raw = {} const arr = reactive([{}, {}]) arr.push(raw) expect(arr.indexOf(raw)).toBe(2) expect(arr.indexOf(raw, 3)).toBe(-1) expect(arr.includes(raw)).toBe(true) expect(arr.includes(raw, 3)).toBe(false) expect(arr.lastIndexOf(raw)).toBe(2) expect(arr.lastIndexOf(raw, 1)).toBe(-1) // should work also for the observed version const observed = arr[2] expect(arr.indexOf(observed)).toBe(2) expect(arr.indexOf(observed, 3)).toBe(-1) expect(arr.includes(observed)).toBe(true) expect(arr.includes(observed, 3)).toBe(false) expect(arr.lastIndexOf(observed)).toBe(2) expect(arr.lastIndexOf(observed, 1)).toBe(-1) }) test('Array identity methods should work if raw value contains reactive objects', () => { const raw = [] const obj = reactive({}) raw.push(obj) const arr = reactive(raw) expect(arr.includes(obj)).toBe(true) }) test('Array identity methods should be reactive', () => { const obj = {} const arr = reactive([obj, {}]) let index: number = -1 effect(() => { index = arr.indexOf(obj) }) expect(index).toBe(0) arr.reverse() expect(index).toBe(1) }) // only non-existent reactive will try to search by using its raw value describe('Array identity methods should not be called more than necessary', () => { const identityMethods = ['includes', 'indexOf', 'lastIndexOf'] as const function instrumentArr(rawTarget: any[]) { const mutableTarget = rawTarget as Record< (typeof identityMethods)[number], any > identityMethods.forEach(key => { const spy = vi.fn(rawTarget[key] as any) mutableTarget[key] = spy }) } function searchValue(target: any[], ...args: unknown[]) { return identityMethods.map(key => (target[key] as any)(...args)) } function unInstrumentArr(rawTarget: any[]) { identityMethods.forEach(key => { ;(rawTarget[key] as any).mockClear() // relink to prototype method rawTarget[key] = Array.prototype[key] as any }) } function expectHaveBeenCalledTimes(rawTarget: any[], times: number) { identityMethods.forEach(key => { expect(rawTarget[key]).toHaveBeenCalledTimes(times) }) } test('should be called once with a non-existent raw value', () => { const reactiveArr = reactive([]) instrumentArr(toRaw(reactiveArr)) const searchResult = searchValue(reactiveArr, {}) expectHaveBeenCalledTimes(toRaw(reactiveArr), 1) expect(searchResult).toStrictEqual([false, -1, -1]) unInstrumentArr(toRaw(reactiveArr)) }) test('should be called once with an existent reactive value', () => { const existReactiveValue = reactive({}) const reactiveArr = reactive([existReactiveValue, existReactiveValue]) instrumentArr(toRaw(reactiveArr)) const searchResult = searchValue(reactiveArr, existReactiveValue) expectHaveBeenCalledTimes(toRaw(reactiveArr), 1) expect(searchResult).toStrictEqual([true, 0, 1]) unInstrumentArr(toRaw(reactiveArr)) }) test('should be called twice with a non-existent reactive value', () => { const reactiveArr = reactive([]) instrumentArr(toRaw(reactiveArr)) const searchResult = searchValue(reactiveArr, reactive({})) expectHaveBeenCalledTimes(toRaw(reactiveArr), 2) expect(searchResult).toStrictEqual([false, -1, -1]) unInstrumentArr(toRaw(reactiveArr)) }) test('should be called twice with a non-existent reactive value, but the raw value exists', () => { const existRaw = {} const reactiveArr = reactive([existRaw, existRaw]) instrumentArr(toRaw(reactiveArr)) const searchResult = searchValue(reactiveArr, reactive(existRaw)) expectHaveBeenCalledTimes(toRaw(reactiveArr), 2) expect(searchResult).toStrictEqual([true, 0, 1]) unInstrumentArr(toRaw(reactiveArr)) }) }) test('delete on Array should not trigger length dependency', () => { const arr = reactive([1, 2, 3]) const fn = vi.fn() effect(() => { fn(arr.length) }) expect(fn).toHaveBeenCalledTimes(1) delete arr[1] expect(fn).toHaveBeenCalledTimes(1) }) test('should track hasOwnProperty call with index', () => { const original = [1, 2, 3] const observed = reactive(original) let dummy effect(() => { dummy = observed.hasOwnProperty(0) }) expect(dummy).toBe(true) delete observed[0] expect(dummy).toBe(false) }) test('shift on Array should trigger dependency once', () => { const arr = reactive([1, 2, 3]) const fn = vi.fn() effect(() => { for (let i = 0; i < arr.length; i++) { arr[i] } fn() }) expect(fn).toHaveBeenCalledTimes(1) arr.shift() expect(fn).toHaveBeenCalledTimes(2) }) //#6018 test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => { const arr = ref([1]) const fn1 = vi.fn() const fn2 = vi.fn() effect(() => { fn1() if (arr.value.length > 0) { arr.value.slice() fn2() } }) expect(fn1).toHaveBeenCalledTimes(1) expect(fn2).toHaveBeenCalledTimes(1) arr.value.splice(0) expect(fn1).toHaveBeenCalledTimes(2) expect(fn2).toHaveBeenCalledTimes(1) }) test('add existing index on Array should not trigger length dependency', () => { const array = new Array(3) const observed = reactive(array) const fn = vi.fn() effect(() => { fn(observed.length) }) expect(fn).toHaveBeenCalledTimes(1) observed[1] = 1 expect(fn).toHaveBeenCalledTimes(1) }) test('add non-integer prop on Array should not trigger length dependency', () => { const array: any[] & { x?: string } = new Array(3) const observed = reactive(array) const fn = vi.fn() effect(() => { fn(observed.length) }) expect(fn).toHaveBeenCalledTimes(1) observed.x = 'x' expect(fn).toHaveBeenCalledTimes(1) observed[-1] = 'x' expect(fn).toHaveBeenCalledTimes(1) observed[NaN] = 'x' expect(fn).toHaveBeenCalledTimes(1) }) // #2427 test('track length on for ... in iteration', () => { const array = reactive([1]) let length = '' effect(() => { length = '' for (const key in array) { length += key } }) expect(length).toBe('0') array.push(1) expect(length).toBe('01') }) // #9742 test('mutation on user proxy of reactive Array', () => { const array = reactive([]) const proxy = new Proxy(array, {}) proxy.push(1) expect(array).toHaveLength(1) expect(proxy).toHaveLength(1) }) describe('Array methods w/ refs', () => { let original: any[] beforeEach(() => { original = reactive([1, ref(2)]) }) // read + copy test('read only copy methods', () => { const raw = original.concat([3, ref(4)]) expect(isRef(raw[1])).toBe(true) expect(isRef(raw[3])).toBe(true) }) // read + write test('read + write mutating methods', () => { const res = original.copyWithin(0, 1, 2) const raw = toRaw(res) expect(isRef(raw[0])).toBe(true) expect(isRef(raw[1])).toBe(true) }) test('read + identity', () => { const ref = original[1] expect(ref).toBe(toRaw(original)[1]) expect(original.indexOf(ref)).toBe(1) }) }) describe('Array subclasses', () => { class SubArray extends Array { lastPushed: undefined | T lastSearched: undefined | T push(item: T) { this.lastPushed = item return super.push(item) } indexOf(searchElement: T, fromIndex?: number | undefined): number { this.lastSearched = searchElement return super.indexOf(searchElement, fromIndex) } } test('calls correct mutation method on Array subclass', () => { const subArray = new SubArray(4, 5, 6) const observed = reactive(subArray) subArray.push(7) expect(subArray.lastPushed).toBe(7) observed.push(9) expect(observed.lastPushed).toBe(9) }) test('calls correct identity-sensitive method on Array subclass', () => { const subArray = new SubArray(4, 5, 6) const observed = reactive(subArray) let index index = subArray.indexOf(4) expect(index).toBe(0) expect(subArray.lastSearched).toBe(4) index = observed.indexOf(6) expect(index).toBe(2) expect(observed.lastSearched).toBe(6) }) }) describe('Optimized array methods:', () => { test('iterator', () => { const shallow = shallowReactive([1, 2, 3, 4]) let result = computed(() => { let sum = 0 for (let x of shallow) { sum += x ** 2 } return sum }) expect(result.value).toBe(30) shallow[2] = 0 expect(result.value).toBe(21) const deep = reactive([{ val: 1 }, { val: 2 }]) result = computed(() => { let sum = 0 for (let x of deep) { sum += x.val ** 2 } return sum }) expect(result.value).toBe(5) deep[1].val = 3 expect(result.value).toBe(10) }) test('concat', () => { const a1 = shallowReactive([1, { val: 2 }]) const a2 = reactive([{ val: 3 }]) const a3 = [4, 5] let result = computed(() => a1.concat(a2, a3, 6, { val: 7 })) expect(result.value).toStrictEqual([ 1, { val: 2 }, { val: 3 }, 4, 5, 6, { val: 7 }, ]) expect(isReactive(result.value[1])).toBe(false) expect(isReactive(result.value[2])).toBe(true) expect(isReactive(result.value[6])).toBe(false) a1.shift() expect(result.value).toStrictEqual([ { val: 2 }, { val: 3 }, 4, 5, 6, { val: 7 }, ]) a2.pop() expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }]) a3.pop() expect(result.value).toStrictEqual([{ val: 2 }, 4, 5, 6, { val: 7 }]) }) test('entries', () => { const shallow = shallowReactive([0, 1]) const result1 = computed(() => Array.from(shallow.entries())) expect(result1.value).toStrictEqual([ [0, 0], [1, 1], ]) shallow[1] = 10 expect(result1.value).toStrictEqual([ [0, 0], [1, 10], ]) const deep = reactive([{ val: 0 }, { val: 1 }]) const result2 = computed(() => Array.from(deep.entries())) expect(result2.value).toStrictEqual([ [0, { val: 0 }], [1, { val: 1 }], ]) expect(isReactive(result2.value[0][1])).toBe(true) deep.pop() expect(Array.from(result2.value)).toStrictEqual([[0, { val: 0 }]]) }) test('every', () => { const shallow = shallowReactive([1, 2, 5]) let result = computed(() => shallow.every(x => x < 5)) expect(result.value).toBe(false) shallow.pop() expect(result.value).toBe(true) const deep = reactive([{ val: 1 }, { val: 5 }]) result = computed(() => deep.every(x => x.val < 5)) expect(result.value).toBe(false) deep[1].val = 2 expect(result.value).toBe(true) }) test('filter', () => { const shallow = shallowReactive([1, 2, 3, 4]) const result1 = computed(() => shallow.filter(x => x < 3)) expect(result1.value).toStrictEqual([1, 2]) shallow[2] = 0 expect(result1.value).toStrictEqual([1, 2, 0]) const deep = reactive([{ val: 1 }, { val: 2 }]) const result2 = computed(() => deep.filter(x => x.val < 2)) expect(result2.value).toStrictEqual([{ val: 1 }]) expect(isReactive(result2.value[0])).toBe(true) deep[1].val = 0 expect(result2.value).toStrictEqual([{ val: 1 }, { val: 0 }]) }) test('find and co.', () => { const shallow = shallowReactive([{ val: 1 }, { val: 2 }]) let find = computed(() => shallow.find(x => x.val === 2)) // @ts-expect-error tests are not limited to es2016 let findLast = computed(() => shallow.findLast(x => x.val === 2)) let findIndex = computed(() => shallow.findIndex(x => x.val === 2)) let findLastIndex = computed(() => // @ts-expect-error tests are not limited to es2016 shallow.findLastIndex(x => x.val === 2), ) expect(find.value).toBe(shallow[1]) expect(isReactive(find.value)).toBe(false) expect(findLast.value).toBe(shallow[1]) expect(isReactive(findLast.value)).toBe(false) expect(findIndex.value).toBe(1) expect(findLastIndex.value).toBe(1) shallow[1].val = 0 expect(find.value).toBe(shallow[1]) expect(findLast.value).toBe(shallow[1]) expect(findIndex.value).toBe(1) expect(findLastIndex.value).toBe(1) shallow.pop() expect(find.value).toBe(undefined) expect(findLast.value).toBe(undefined) expect(findIndex.value).toBe(-1) expect(findLastIndex.value).toBe(-1) const deep = reactive([{ val: 1 }, { val: 2 }]) find = computed(() => deep.find(x => x.val === 2)) // @ts-expect-error tests are not limited to es2016 findLast = computed(() => deep.findLast(x => x.val === 2)) findIndex = computed(() => deep.findIndex(x => x.val === 2)) // @ts-expect-error tests are not limited to es2016 findLastIndex = computed(() => deep.findLastIndex(x => x.val === 2)) expect(find.value).toBe(deep[1]) expect(isReactive(find.value)).toBe(true) expect(findLast.value).toBe(deep[1]) expect(isReactive(findLast.value)).toBe(true) expect(findIndex.value).toBe(1) expect(findLastIndex.value).toBe(1) deep[1].val = 0 expect(find.value).toBe(undefined) expect(findLast.value).toBe(undefined) expect(findIndex.value).toBe(-1) expect(findLastIndex.value).toBe(-1) }) test('forEach', () => { const shallow = shallowReactive([1, 2, 3, 4]) let result = computed(() => { let sum = 0 shallow.forEach(x => (sum += x ** 2)) return sum }) expect(result.value).toBe(30) shallow[2] = 0 expect(result.value).toBe(21) const deep = reactive([{ val: 1 }, { val: 2 }]) result = computed(() => { let sum = 0 deep.forEach(x => (sum += x.val ** 2)) return sum }) expect(result.value).toBe(5) deep[1].val = 3 expect(result.value).toBe(10) }) test('join', () => { function toString(this: { val: number }) { return this.val } const shallow = shallowReactive([ { val: 1, toString }, { val: 2, toString }, ]) let result = computed(() => shallow.join('+')) expect(result.value).toBe('1+2') shallow[1].val = 23 expect(result.value).toBe('1+2') shallow.pop() expect(result.value).toBe('1') const deep = reactive([ { val: 1, toString }, { val: 2, toString }, ]) result = computed(() => deep.join()) expect(result.value).toBe('1,2') deep[1].val = 23 expect(result.value).toBe('1,23') }) test('map', () => { const shallow = shallowReactive([1, 2, 3, 4]) let result = computed(() => shallow.map(x => x ** 2)) expect(result.value).toStrictEqual([1, 4, 9, 16]) shallow[2] = 0 expect(result.value).toStrictEqual([1, 4, 0, 16]) const deep = reactive([{ val: 1 }, { val: 2 }]) result = computed(() => deep.map(x => x.val ** 2)) expect(result.value).toStrictEqual([1, 4]) deep[1].val = 3 expect(result.value).toStrictEqual([1, 9]) }) test('reduce left and right', () => { function toString(this: any) { return this.val + '-' } const shallow = shallowReactive([ { val: 1, toString }, { val: 2, toString }, ] as any[]) expect(shallow.reduce((acc, x) => acc + '' + x.val, undefined)).toBe( 'undefined12', ) let left = computed(() => shallow.reduce((acc, x) => acc + '' + x.val)) let right = computed(() => shallow.reduceRight((acc, x) => acc + '' + x.val), ) expect(left.value).toBe('1-2') expect(right.value).toBe('2-1') shallow[1].val = 23 expect(left.value).toBe('1-2') expect(right.value).toBe('2-1') shallow.pop() expect(left.value).toBe(shallow[0]) expect(right.value).toBe(shallow[0]) let deep = reactive([{ val: 1 }, { val: 2 }]) left = computed(() => deep.reduce((acc, x) => acc + x.val, '0')) right = computed(() => deep.reduceRight((acc, x) => acc + x.val, '3')) expect(left.value).toBe('012') expect(right.value).toBe('321') deep[1].val = 23 expect(left.value).toBe('0123') expect(right.value).toBe('3231') deep = reactive([{ val: 1 }, { val: 2 }]) const maxBy = (prev: any, cur: any) => { expect(isReactive(prev)).toBe(true) expect(isReactive(cur)).toBe(true) return prev.val > cur.val ? prev : cur } left = computed(() => deep.reduce(maxBy)) right = computed(() => deep.reduceRight(maxBy)) expect(left.value).toMatchObject({ val: 2 }) expect(right.value).toMatchObject({ val: 2 }) deep[0].val = 23 expect(left.value).toMatchObject({ val: 23 }) expect(right.value).toMatchObject({ val: 23 }) deep[1].val = 24 expect(left.value).toMatchObject({ val: 24 }) expect(right.value).toMatchObject({ val: 24 }) }) test('reduce left and right with single deep reactive element and no initial value', () => { const deep = reactive([{ val: 1 }]) const left = computed(() => deep.reduce(prev => prev)) const right = computed(() => deep.reduceRight(prev => prev)) expect(isReactive(left.value)).toBe(true) expect(isReactive(right.value)).toBe(true) expect(left.value).toMatchObject({ val: 1 }) expect(right.value).toMatchObject({ val: 1 }) deep[0].val = 2 expect(left.value).toMatchObject({ val: 2 }) expect(right.value).toMatchObject({ val: 2 }) }) test('some', () => { const shallow = shallowReactive([1, 2, 5]) let result = computed(() => shallow.some(x => x > 4)) expect(result.value).toBe(true) shallow.pop() expect(result.value).toBe(false) const deep = reactive([{ val: 1 }, { val: 5 }]) result = computed(() => deep.some(x => x.val > 4)) expect(result.value).toBe(true) deep[1].val = 2 expect(result.value).toBe(false) }) // Node 20+ // @ts-expect-error tests are not limited to es2016 test.skipIf(!Array.prototype.toReversed)('toReversed', () => { const array = reactive([1, { val: 2 }]) const result = computed(() => (array as any).toReversed()) expect(result.value).toStrictEqual([{ val: 2 }, 1]) expect(isReactive(result.value[0])).toBe(true) array.splice(1, 1, 2) expect(result.value).toStrictEqual([2, 1]) }) // Node 20+ // @ts-expect-error tests are not limited to es2016 test.skipIf(!Array.prototype.toSorted)('toSorted', () => { // No comparer // @ts-expect-error expect(shallowReactive([2, 1, 3]).toSorted()).toStrictEqual([1, 2, 3]) const shallow = shallowReactive([{ val: 2 }, { val: 1 }, { val: 3 }]) let result: ComputedRef<{ val: number }[]> // @ts-expect-error result = computed(() => shallow.toSorted((a, b) => a.val - b.val)) expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3]) expect(isReactive(result.value[0])).toBe(false) shallow[0].val = 4 expect(result.value.map(x => x.val)).toStrictEqual([1, 4, 3]) shallow.pop() expect(result.value.map(x => x.val)).toStrictEqual([1, 4]) const deep = reactive([{ val: 2 }, { val: 1 }, { val: 3 }]) // @ts-expect-error result = computed(() => deep.toSorted((a, b) => a.val - b.val)) expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3]) expect(isReactive(result.value[0])).toBe(true) deep[0].val = 4 expect(result.value.map(x => x.val)).toStrictEqual([1, 3, 4]) }) // Node 20+ // @ts-expect-error tests are not limited to es2016 test.skipIf(!Array.prototype.toSpliced)('toSpliced', () => { const array = reactive([1, 2, 3]) // @ts-expect-error const result = computed(() => array.toSpliced(1, 1, -2)) expect(result.value).toStrictEqual([1, -2, 3]) array[0] = 0 expect(result.value).toStrictEqual([0, -2, 3]) }) test('values', () => { const shallow = shallowReactive([{ val: 1 }, { val: 2 }]) const result = computed(() => Array.from(shallow.values())) expect(result.value).toStrictEqual([{ val: 1 }, { val: 2 }]) expect(isReactive(result.value[0])).toBe(false) shallow.pop() expect(result.value).toStrictEqual([{ val: 1 }]) const deep = reactive([{ val: 1 }, { val: 2 }]) const firstItem = Array.from(deep.values())[0] expect(isReactive(firstItem)).toBe(true) }) test('extend methods', () => { class Collection extends Array { // @ts-expect-error every(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.every(obj => obj.id === foo) } // @ts-expect-error filter(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.filter(obj => obj.id === foo) } // @ts-expect-error find(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.find(obj => obj.id === foo) } // @ts-expect-error findIndex(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.findIndex(obj => obj.id === bar) } findLast(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') // @ts-expect-error our code is limited to es2016 but user code is not return super.findLast(obj => obj.id === bar) } findLastIndex(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.findIndex(obj => obj.id === bar) } // @ts-expect-error forEach(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') } // @ts-expect-error map(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.map(obj => obj.value) } // @ts-expect-error some(foo: any, bar: any, baz: any) { expect(foo).toBe('foo') expect(bar).toBe('bar') expect(baz).toBe('baz') return super.some(obj => obj.id === baz) } } const state = reactive({ things: new Collection(), }) const foo = { id: 'foo', value: '1' } const bar = { id: 'bar', value: '2' } const baz = { id: 'baz', value: '3' } state.things.push(foo) state.things.push(bar) state.things.push(baz) expect(state.things.every('foo', 'bar', 'baz')).toBe(false) expect(state.things.filter('foo', 'bar', 'baz')).toEqual([foo]) const _foo = state.things.find('foo', 'bar', 'baz') expect(isReactive(_foo)).toBe(true) expect(foo).toStrictEqual(_foo) expect(state.things.findIndex('foo', 'bar', 'baz')).toBe(1) const _bar = state.things.findLast('foo', 'bar', 'baz') expect(isReactive(_bar)).toBe(true) expect(bar).toStrictEqual(_bar) expect(state.things.findLastIndex('foo', 'bar', 'baz')).toBe(1) expect(state.things.forEach('foo', 'bar', 'baz')).toBeUndefined() expect(state.things.map('foo', 'bar', 'baz')).toEqual(['1', '2', '3']) expect(state.things.some('foo', 'bar', 'baz')).toBe(true) { class Collection extends Array { find(matcher: any) { return super.find(matcher) } } const state = reactive({ // @ts-expect-error things: new Collection({ foo: '' }), }) const bar = computed(() => { return state.things.find((obj: any) => obj.foo === 'bar') }) bar.value state.things[0].foo = 'bar' expect(bar.value).toEqual({ foo: 'bar' }) } }) }) })