Repository: pmndrs/valtio (ts)

Issue:
Assigning directly to an array by index does not change array length
## Expected behavior

When assigning directly to an array position, the array length should be updated, a la the native JavaScript array behavior. Any components depending on the array (for example, iterating over its items) should be re-rendered.

**Example of index assignment with a native Array**

```
const arr = [];
arr[0] = 'foo';
arr.length; // 1
arr[6] = 'bar';
arr.length; // 7
```

## Observed behavior

When assigning to an array position on a proxy store, the length is not updated, and dependent components do not re-render. However, an update notification is sent to `subscribe`.

Reproduction: https://codesandbox.io/s/valtio-array-length-bug-kl8d2?file=/src/App.js

Uncomment line 44 to "fix" the problem manually by assigning the correct array length. This triggers the component re-render, too.

Observe also that the `subscribe` callback, when it logs the current proxy state, shows the new third item present but the length is still `2`.


Pull request description:
fix: array length for direct assignment
Fixes #7 : array length not updating on index assignment.

It appears that creating an empty array using `Object.create` from the prototype of the original doesn't create a proper array, just an Object which inherits from the Array prototype. Real arrays have special behaviors.

See also https://stackoverflow.com/questions/22022058/new-array-vs-object-createarray-prototype

The added tests check for the special indexing properties of an Array: a simple case of adding a sequential item, and an edge case of adding a non-contiguous one.

Patch:
diff --git a/src/index.ts b/src/index.ts
index 8aa75b5..bcb9da5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -37,7 +37,10 @@ const createProxy = <T extends object>(initialObject: T = {} as T): T => {
       listeners.forEach((listener) => listener(nextVersion))
     }
   }
-  const proxy = new Proxy(Object.create(initialObject.constructor.prototype), {
+  const emptyCopy = Array.isArray(initialObject)
+    ? []
+    : Object.create(initialObject.constructor.prototype)
+  const proxy = new Proxy(emptyCopy, {
     get(target, prop, receiver) {
       if (prop === MUTABLE_SOURCE) {
         if (!mutableSource) {


Test patch:
diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx
index 36f192c..9f8f068 100644
--- a/tests/basic.test.tsx
+++ b/tests/basic.test.tsx
@@ -139,6 +139,44 @@ it('array in object', async () => {
   await findByText('counts: 0,1,2,3')
 })
 
+it('array length after direct assignment', async () => {
+  const obj = proxy({ counts: [0, 1, 2] })
+
+  const Counter: React.FC = () => {
+    const snapshot = useProxy(obj)
+    return (
+      <>
+        <div>counts: {snapshot.counts.join(',')}</div>
+        <div>length: {snapshot.counts.length}</div>
+        <button
+          onClick={() => (obj.counts[obj.counts.length] = obj.counts.length)}>
+          increment
+        </button>
+        <button
+          onClick={() =>
+            (obj.counts[obj.counts.length + 5] = obj.counts.length + 5)
+          }>
+          jump
+        </button>
+      </>
+    )
+  }
+
+  const { getByText, findByText } = render(
+    <StrictMode>
+      <Counter />
+    </StrictMode>
+  )
+
+  await findByText('counts: 0,1,2')
+
+  fireEvent.click(getByText('increment'))
+  await findByText('counts: 0,1,2,3')
+
+  fireEvent.click(getByText('jump'))
+  await findByText('counts: 0,1,2,3,,,,,,9')
+})
+
 it('circular object', async () => {
   const obj = proxy<any>({ object: {} })
   obj.object = obj