vueuse icon indicating copy to clipboard operation
vueuse copied to clipboard

computedRef (same as reactiveComputed but returns a ref)

Open IlyaSemenov opened this issue 2 years ago • 8 comments

Clear and concise description of the problem

As a developer using Vue for CRUD, and developing update forms in particular, I want an easy and DRY way to prepare a reactive object for field values. The problem is, the initial field values should be copied/constructed based on the data that is coming asynchronously from the API, and that may also update (such as after successful submit and pulling updated data from the server). When such object is first retrieved and/or updated, the set of fields (for the editable inputs) should be re-created. When the source data is not (yet) available, as when it's still loading or has never loaded, the object must be undefined.

The typical DRY code looks like:

<script setup lang="ts">
import { computedRef } from "@vueuse/core"

const { data } = useAsyncData() // fetch data asynchronously, the result is typed as Ref<Data | undefined>

const fields = computedRef(() => data.value && ({
  // only pick certain fields that are editable
  name: data.value.name,
  address1: data.value.address1,
  address2: data.value.address2,
  // possibly add form fields that are not present in the data, but represent client-side logic
  copyAddress1ToAddress2: data.value.address1 === data.value.address2,
}))

// TODO: define watcher for address sync, omitting this for brevity.

async function submit() {
  const res = await callSubmitApi(fields.value)
  if (res.data) {
    // On successful submit, `fields` should be re-created with fresh data coming from the server.
    data.value = res.data
  }
}
</script>

<template>
  <form v-if="fields" @submit.prevent="submit">
    <input v-model="fields.name" />
    <input v-model="fields.address1" />
    <input type="checkbox" v-model="fields.copyAddress1ToAddress2" />
    <input v-model="fields.address2" :disabled="fields.copyAddress1ToAddress2"/>
  </form>
</template>

Suggested solution

The implementation is very simple, currently I am copy/pasting it between projects:

import type { UnwrapRef, WatchSource } from "vue"

/** @returns ref which resets when the source changes */
export function computedRef<T>(source: WatchSource<T>) {
  const value = ref(toValue(source))
  watch(source, (source) => {
    value.value = source as UnwrapRef<T>
  })
  return value
}

Alternative

VueUse already has reactiveComputed, but it's not directly suitable, as it must always return an object. Therefore:

  1. It can't be used when the source data could be is unavailable (reactiveComputed can't return undefined).

  2. It can't be used when the ref is a non-object. For instance, consider a case when a list can be expanded and collapsed, which is a boolean ref. The default state (collapsed or expanded) could be dependent on the list length, such as it's expanded by default when it's 10 or less items, and collapsed if there's more. Then, when a list is updated, it must recalculate its expanded status based on the new length.

Of course, we could always work around and return an object shaped as { value: <actual value, possibly undefined> }, but why not have a function that uses a native Vue Ref for that case? It will also unwrap naturally in templates.

Additional context

Naming is subject for discussion. For consistency with reactiveComputed, it could be refComputed. (To be honest, I would rather reverse both, computedReactive and computedRef read better in my thinking.)

Validations

IlyaSemenov avatar Oct 20 '23 09:10 IlyaSemenov

Maybe you can simply return a |loading| variable from the useAsync method? 😂

Doctor-wu avatar Oct 20 '23 10:10 Doctor-wu

@Doctor-wu I am not following. Of course I do have a loading kind of ref in the useAsync results. How is that supposed to help with creating a ref with form fields when the loading is complete (and then when the reloading is complete)?

IlyaSemenov avatar Oct 20 '23 10:10 IlyaSemenov

@Doctor-wu I am not following. Of course I do have a loading kind of ref in the useAsync results. How is that supposed to help with creating a ref with form fields when the loading is complete (and then when the reloading is complete)?

From my understanding, you want a ref to control the form with v-if="ref.value", maybe you can use v-if="!loading" instead, or maybe I don't understand why you need a ref rather than computed😂.

Doctor-wu avatar Oct 20 '23 10:10 Doctor-wu

From my understanding, you want a ref to control the form with v-if="ref.value"

That's the least of the concerns.

maybe I don't understand why you need a ref rather than computed

computed values are not reactive, they don't work with v-model, they are read-only. Only reactive and refs can be used with <input v-model="...">.

IlyaSemenov avatar Oct 20 '23 10:10 IlyaSemenov

From my understanding, you want a ref to control the form with v-if="ref.value"

That's the least of the concerns.

maybe I don't understand why you need a ref rather than computed

computed values are not reactive, they don't work with v-model, they are read-only. Only reactive and refs can be used with <input v-model="...">.

got you, the barin was down 🤖

Doctor-wu avatar Oct 20 '23 10:10 Doctor-wu

Since I came up with this pattern, I actually use it more and more. Today's example was a dialog where a user would check (mark) certain items organized in groups, and also could expand/collapse each group individually.

Earlier, I used to track the values and expand flags separately, which quickly becomes cumbersome on deeply nested data, but with computedRef I stitch together props and flags in a neat structure:

const groups = computedRef(
  () =>
    props.groups
      ?.filter((group) => group.active)
      .map((group) => ({
        ...pick(["name", "name_locale"], group),
        items: group.items
          .filter((item) => item.active)
          .map((item) => ({
            ...pick(["name", "name_locale"], item),
            checked: false, // possible to use with v-model inside nested loop!
          })),
        expanded: true, // possible to use with v-model inside loop!
      })),
)

So my question is, should I bother with coming up with a PR?

IlyaSemenov avatar Dec 03 '23 09:12 IlyaSemenov

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Feb 02 '24 02:02 stale[bot]

I still think it's a worthy addition, and I'm ready to come up with a PR if that's not something that will be 100% rejected.

IlyaSemenov avatar Feb 02 '24 03:02 IlyaSemenov