computedRef (same as reactiveComputed but returns a ref)
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:
-
It can't be used when the source data could be is unavailable (
reactiveComputedcan't returnundefined). -
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
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Maybe you can simply return a |loading| variable from the useAsync method? 😂
@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)?
@Doctor-wu I am not following. Of course I do have a
loadingkind 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😂.
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="...">.
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 🤖
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?
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.
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.