[Vue] Arrays and v-model on it's items with a simple wrapper
Tags: vue, typescript
Published: 2023-07-15 12:36:30

# Arrays and v-model in Vue 3

Vue’s template compiler deals with arrays with `v-for` directive, with assisting `:key` for better performance (and not only!). While everything is easy for basic displaying. We encourage the first problem when we try to use `v-model` inside:

```html
<div v-for="(item, idx) in items" :key="idx">
  <input v-model="item" />
</div>
```

And here we go… While `items` is of type `Ref<string[]>`, the `item` here is just `string` , it won’t even compile, because `v-model` tries to mount a handler for the event `"update:modelValue"`, which tries to assign the new value to `item`, which is basically a local variable in template renderer. If we use just `:model-value="item"`, the template compiler will generate the code:

```jsx
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items.value, (item, idx) => {
  return (_openBlock(), _createElementBlock("div", { key: idx }, [
    _createElementVNode("input", { "model-value": item }, null, 8 /* PROPS */, _hoisted_1)
  ]))
}), 128 /* KEYED_FRAGMENT */))
```

As you see, `item` here is literally a local variable, an argument of a function, to which you shouldn’t assign anything (it wouldn’t have any actual effect).

It’s not a problem if the array is an array of objects and we are mutating its fields, the objects here will keep the reactivity and Vue will allow mutating them without a problem. The problem will start, when using a different component there that will return a new object, instead of mutating the original one, for example, a picker for category, tags (that aren’t stored as just plain text).

## Simple solution: use the array instance

Vue 3’s reactivity is awesome and works flawlessly with arrays, we can literally just mutate an item in an array, so we can go that way: instead of using the `item` instance, we can use the index from the `v-for` (`idx`) and source array:

```html
<div v-for="(item, idx) in items" :key="idx">
  <input v-model="items[idx]" />
</div>
```

And now everything works as expected! The generated code looks like this:

```jsx
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items.value, (item, idx) => {
  return (_openBlock(), _createElementBlock("div", { key: idx }, [
    _withDirectives(_createElementVNode("input", {
      "onUpdate:modelValue": $event => ((items.value[idx]) = $event)
    }, null, 8 /* PROPS */, _hoisted_1), [
      [_vModelText, items.value[idx]]
    ])
  ]))
}), 128 /* KEYED_FRAGMENT */))
```

We see that we use `idx` item of `items` array for both setting the `model-value` prop and to set in `"update:modelValue"`. The `item` from the `v-for` is not used at all.

This is a simple solution that will work in most simple cases.

## What if the case isn’t simple?

The solution above assumes that we are working on an existing array, I mean… `ref`. What if it would be a `computed`? Maybe we want to filter a list first?

```jsx
const items = ref(['foo', 'bar']);
const search = ref('');
const filteredList = computed(() => {
  if (search.value.length == 0) return items.value;
  return items.value.filter(item => item.includes(search.value));
});
```

We can’t just replace the `items` with `filteredList` in `v-for` and keep the `items[idx]` in the `input` tag, because the given `idx` won’t be the index of the item in the original array!

Again, if we mutate the items field, it wouldn’t be any issue here. The returned array will still contain reactive objects.

Sure, we could check if the element should be displayed in the template:

```tsx
<div 
  v-for="(item, idx) in items" 
  :key="idx" 
  v-if="search.includes(item)"
>
  <input v-model="items[idx]" />
</div>
```

But this solution is… just wrong, against Vue’s style guide. We need to remember that `v-for` is above the `v-if` directive, meaning that we can hide that way every item separately. But then, it will make the checks with every component rerender, even if nothing in the array changes!

## Store the list of indices

One of the solutions is to provide an array of indexes instead:

```tsx
const filteredIdx = computed(() => {
  if (search.value.length == 0) {
    return items.value.map((_, idx) => idx);
  }
  return items.value
    .map((item, idx) => item.includes(search.value) ? idx : null)
    .filter(i => i != null)
});
```

Then we can write the template:

```tsx
<div v-for="idx in filteredIdx" :key="idx">
  <input v-model="items[idx]" />
</div>
```

This solution also lets you display the items in a different order than the original array, before mapping the items to indices, sort the array (copying the array first by `[...items.value].sort` or by using the new `toSorted` method!).

Rather a clean solution, but if we would like to use the filtered array in a child component, we would have to provide both data: array and filtered list.

## Wrap the array, but it’s not as simple as you think it is

Anyway, we need to keep the original index here, so… we better construct a type for a wrapped arrays… but how?

We could simply wrap it into an object:

```tsx
type WrappedArray<T> = {
  array: Ref<T[]>,
  indices: Ref<number[]>;
};
```

and use it providing both as reactive values, but no, don’t do this. We better do a reactive wrapper for an array

### Reactive array proxy

We could write a proxy object that still acts like a normal array, but can handle mutations on original array (or update it whole, by creating new array instance).  We will have array of types:

```tsx
type ProxyArrayItem<T> = { value: T, idx: number};
```

The `value` field here will be reactive, we could do `item.value = newItem` and it should work, replacing the item on the specific index in original array. We will allow to filter the items and sort them, so we will have the function header:

```tsx
export function toArrayProxy<T>(opt: {
  array: Ref<T[]>;
  sort?: (a: T, b: T) => number;
  filter?: (a: T) => boolean;
}): ComputedRef<ProxyArrayItem<T>[]> {
  // return proxyArray
}
```

Next, we need an inner function that will return indices of items to show:

```tsx
function indices(): number[] {
  const { filter, sort } = opt;
  let arr = opt.array.value.map((item, idx) => {
   return { value: item, idx }
  });
  if (filter) arr = arr.filter((i) => filter(i.value));
  if (sort) arr = arr.sort((a, b) => sort(a.value, b.value));
  return arr.map(i => i.idx);
}
```

A hidden bonus here: the `filter` and `sort` functions can use reactive data and if `indices` is used in a `computed`, it will be recalculated when dependency will update.

And then, we need to create the array that will wrap it:

```tsx
const proxyArray = computed(() => {
  return indices().map(c => /* what? */);
}));
```

And yeah, what should we put there? Basically returning the `{ value: opt.array.value[c], idx: c }` won’t provide reactivity for `value`. Here we have a few possible approaches. Computed’s `value` returns exactly what it’s getter returns, so here we return a raw array of objects. We want the field `value` (the array’s item’s) to point at the original array, with both getter and setter. 

### Just use another `computed` inside!…?

Easiest approach: 

```tsx
const proxyArray = computed(() => {
  return indices().map(idx => computed({
    get: () => opt.array.value[idx],
    set: (newValue) => opt.array.value[idx] = newValue,
  }));
});
```

But then we won’t know, what is the real index in the original array, if we need it ever.

Also, we have another reactive element here… Well, `n` reactive elements, each array item is separate reactive object.

Take a look at what we actually need here… Getter should return `opt.array.value[idx]`, where `idx` is returned from `indices`. Setter should… depends on the approach:

- just set `opt.array.value[idx] = newValue`
- call setter given in options, or assign to `opt.array.value = newArray`, where `newArray` can be either done by any method of replacing an item in array:
    - copy array and replace item
    - use `.splice(idx, 1, newValue)`
    - use new method `.with(idx, newValue)`

We could easily provide just those setters and getters or… do it like it Vue does, but without using another level of reactivity! Here is where a simple object with a getter and setter comes in:

```tsx
const proxyArray = computed(() => {
  return indices().map(idx => ({
    idx,
    get value () {
      return opt.array.value[idx];
    },
    set value(newValue) {
      opt.array.value[idx] = newValue;
    },
  }));
});
```

So, basically, what happens here? Let see:

```html
<div v-for="(item, i) in proxyArray" :key="i">
	<ItemEditor v-model="item.value"/>
</div>
```

What happens here?

```jsx
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_unref(proxy), (item, i) => {
    return (_openBlock(), _createElementBlock("div", { key: i }, [
      _createVNode(ItemEditor, {
        modelValue: item.value,
        "onUpdate:modelValue": ($event) => ((item.value) = $event)
      }, null, 8 /* PROPS */, ["modelValue", "onUpdate:modelValue"])
    ]))
  }), 128 /* KEYED_FRAGMENT */))
```

Let’s see, then `modelValue: item.value` actually calls the getter, which returns `opt.array.value[idx]`, which means we depend on given `opt.array` (the component will rerender when we change the array) and on the array items `opt.array.value[idx]`.

On the other side, `onUpdate:modelValue` makes a simple assignation to `.value`, which calls the setter, which does `opt.array.value[idx] = newValue`. Everything we need. Of course, as I mentioned, we can change the way it’s assigned.

This setter will cause the update of the component it’s changing because the array items themselves were reactive before.

![How it works](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4nwlvlhx0jy9u72urbfz.png)

Logically, `value` acts exactly like array’s item. Underneath it’s done by getter and setter and using a cached item, so getter doesn’t hit the array each time we try to get the item. If the item in array will change, there will be new instance of `ArrayProxyItem` created anyway, caused by the dependency created while mapping items in function `indices`. If `T` is an object, it will remain reactive and will be the same instance as in the array.

## Further development

We got it done easily, but why not make a step forward? We could easily make some common jobs easier with this solution, like removing items! Just add a method to returned items:

```tsx
const proxyArray = computed(() => {
  return indices().map(idx => ({
    idx,
    get value () {
      return opt.array.value[idx];
    },
    set value(newValue) {
      opt.array.value[idx] = newValue;
    },
    delete: () => opt.array.value.splice(idx, 1),
  }));
});
```

And we can use it directly in the template:

```html
<div v-for="(item, i) in proxyArray" :key="i">
	<ItemEditor v-model="item.value" @remove="item.delete"/>
</div>
```

## Further optimizations

JavaScript is a nice runtime environment, but sadly, it encourages developers to make the code slow… Sure, “*no premature optimization*”, but seriously… Don’t set up the traps in places you will regret later! If you are building something low-level, keep it fast and optimized, so you don’t have to care that much about optimization on high-level code (your components). Many web pages are using way too much resources, don’t join them!

I won’t talk about how the conventional for loop (`for (const item of items)`) is much faster than the `.forEach`. Look at the upper code… What happens here? While it looks clean, underneath the shiny shell of JavaScript it’s a hell… 3 closures!

Here comes the hated Object-Oriented Programming… Classes! Okay, it’s not so “object-oriented”, because we are going to use just one single class, but still.

We had previously declared the type `ProxyArrayItem<T>` and… forgot about it. Now let’s change the type into an actual class:

```tsx
class ArrayProxyItem<T> {
  constructor(private array: Ref<T[]>, public idx: number) {}
  get value() {
    return this.array.value[this.idx];
  }
  set value(newValue) {
    this.array.value[this.idx] = newValue;
  }
  delete() {
    this.array.value.splice(this.idx, 1);
  }
}
```

Not the we will have 3 class methods instead of 3 closures per every(!) item of array, while having still the same access interface.

The only difference is just creating the proxy array:

```tsx
const proxyArray = computed(() => {
  return indices().map(idx => new ArrayProxyItem(opt.array, idx));
});
```

A silly micro benchmark (without Vue’s part) that tests 1000 array elements says that this method is over **20 times faster** when creating. But micro benchmarks are often just a curiosity that has little significance. 

Sure, it might be lost in the all the work our JS app has to do, but… why waste time and memory, when it can be saved? Especially if it’s something that could be done without that layer of abstraction, we are doing it only to write code easier.

Results for different approaches:

- raw print: 17 MiB heap, 25ms render (and total)
- computed: 48 MiB heap, 10.1ms creation, 20.0ms render, 30.1ms total
- closures: 36 MiB heap, 13.7ms creation, 23,6ms render, 37.3ms total
- classes: 24 MiB heap, 7.8ms creation, 24ms render, 31.8ms total

Well, the worst is the approach using closures. Twice that long as using classes or computers. Computeds don’t provide the functionality we need, so it’s a different problem. There are no significant differences in rendering, the benchmark wasn’t done on clean system, so we need to apply a big measurement error.

For heap usage, those are peeks I found with `setInterval` each 1ms. I’ve measured it multiple times, and results were always similar: with classes the peek never was as high as the others. Don’t trust those measures too much, those measures are not trustworthy, just the overall observation is that using classes had the lowest footprint memory. Computeds and closures were getting higher. I tried using profiler, but it also doesn’t find the actual peeks, and the observations were similar. I know, those tests were done with vite server, without devtools opened, but still the differences are noticable. Also note, that for raw print, there was used just one array, instead of 100.

Somehow rendering is fastest for computeds…  It’s probably because it caches the item. Let’s do that in out class:

```tsx
class ArrayProxyItem<T> {
  _cache: T;
  constructor(private array: Ref<T[]>, public idx: number) {
    this._cache = array.value[idx];
  }
  get value() {
    return this._cache;
  }
  set value(newValue) {
   this.array.value[this.idx] = newValue;
  }
  delete() {
    this.array.value.splice(this.idx, 1);
  }
}
```

And we’ve managed to get down a few ms for objects for type, but it gets worse for primitives. It’s hard to find the best solution. The `_cache` will still be reactive, if it ever change (the item instance, either object as whole or if it’s primitive) the `proxyArray` computed should create a new proxy for that item anyway.

There might be a much bigger performance penalty when mutating data using nested computeds, but it’s a deeper problem, much harder to measure

## Final code

```tsx
export class ArrayProxyItem<T> {
  _cache: T;
  constructor(private array: Ref<T[]>, public idx: number) {
    this._cache = array.value[idx];
  }
  get value() {
    return this._cache;
  }
  set value(newValue) {
    this.array.value[this.idx] = newValue;
  }
  delete() {
    this.array.value.splice(this.idx, 1);
  }
}

// export type ProxyArrayItem<T> = { value: T, idx: number};
export function toArrayProxy<T>(opt: {
  array: Ref<T[]>;
  sort?: (a: T, b: T) => number;
  filter?: (a: T) => boolean;
}) {

  function indices(): number[] {
    const { filter, sort } = opt;
    let arr = opt.array.value.map((item, idx) => {
      return { value: item, idx };
    });
    if (filter) arr = arr.filter((i) => filter(i.value));
    if (sort) arr = [...arr].sort((a, b) => sort(a.value, b.value));
    return arr.map((i) => i.idx);
  
  const proxyArray = computed(() => {
    return indices().map((idx) => new ArrayProxyItem(opt.array, idx));
  });

  return proxyArray;
}
```

After using the cache, it works more like that:

![How it works with local cache reference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfidz5nzft0eaxod3r9z.png)

The constructor copies the item (reference to it, if it's object) to local cache, so the original array doesn’t have to be used anymore, when we are reading the value. If you use `ArrayProxyItem`, the dependency will be installed only on the `T` object (if it’s reactive! A primitive won’t be reactive here, but it’s not any problem here), not on the array. If the item’s field change, everything will work fine on both `ArrayProxyItem`'s value and directly on the source array. If the item will change, there will be another array of `ArrayProxyItem` created, so the cached reference isn’t any problem. Also again, if the new array will contain the same references, components won’t have to trigger updating the view.

I know it sounds complicated but… Thanks to the reactivity in Vue it just works.

## Implementing different storing strategies

As I said, if we want to keep the `vue/no-mutating-props` rule, we should take different approach to the setter, like reassigning whole array (pick one):

```tsx
class ArrayProxyItem<T> {
  set value(newValue) {
    // newest approach, fast*
    this.array.value = this.array.value.with(this.idx, newValue);
    // older functional approach, slow
    this.array.value = this.array.value.map((item, i) => i == idx ? newValue : item)
		// copying, patching, aplying; ugly but fastest
    const arr = [...this.array.value];
		arr[this.idx] = newValue;
    this.array.value = arr;
  }
}
```

This will allow you to use a computed as an array, which setter emits `update:modelValue` event. You can do the same with `delete` method (pick one):

```tsx
class ArrayProxyItem<T> {
  delete() {
    // filter the item out; nice, but slower
    this.array.value = this.array.value.filter((_, idx) => this.idx != idx);
    // copy, remove with splice, apply; looking ugly, but fast
    const arr = [...this.array.value];
		arr[this.idx].splice(this.idx, 1);
    this.array.value = arr;
  }
}
```

The main difference is that without overwriting the array’s instance, `watch` won’t react on the change. Watching array is another wide problem. The `proxyArray` computed will react on any change here, because it reads the items.

While I wouldn’t recommend implementing both in the `ArrayProxyItem`, I would suggest implement both strategies with interfaces and use whatever version you need. Maybe add an option to `toArrayProxy` to select the updating strategy?

## Practical usage

The proxy implements two things: sorting and filtering. That means, having an array, you wrap it and display in different order, or hide some items:

```tsx
const search = ref("");
const todos: ref<TodoItem[]>([]);
const todosList = toArrayProxy({
  array: todos,
  filter: (item) => {
    // don't show subtasks here
    if (item.parent == null) return false;
    // if user typed something in search, filter the items
    if (search.value.length == 0) return true;
    return item.content.includes(search.value);
  },
  sort: (a, b) => {
    return a.priority - b.priority;
  }
});
```

That way we could use the new array to display, modify and remove items from the list.

If the list is returned from backend, it might be better to delete it by request and reload the whole list, but for optimization we could request the delete and just continue working without that item (this is the same thing we would achieve by reloading the array, but with reloading, we would lose the other unsaved changes, if there are any). If we wouldn’t sort, the items would remain in the same order, so we could use the index in original array for features like “add item directly after that one”.

We could still improve this by adding another functionalities, like listeners to change: triggers called when item is removed, moved etc.

# Conclusion

We’ve managed to write a wrapper for an array, so we can use a completely reordered and filtered array in `v-for` and still use `v-model` to mutate the original array with that. If we are not printing huge amounts of data, the performance. The given solution still brings a lot of places to improve performance and usability.

Using the class approach results in fewer dependencies to track compared to using nested `computed`s which creates another layer. Therefore, in the long term, this approach should be much more performant. Anyway, it makes many cases using arrays a lot easier, especially if we want to simply use `v-model` on items directly.