I’m trying to understand a specific issue. I have a composable in Vue 3 that needs to watch a reference, which gets updated in the view through a text field. How can I set up a watcher on this variable to trigger the watch?
export function useFieldValidationHelper3<T>(fieldValue: Ref<T>) {
const value = fieldValue;
watch(
value.value,
(newValue) => {
newValue.value = value;
console.log("Field value updated in the composable", newValue);
},
{ deep: true }
);
const errorMessage: Ref<string | undefined> = ref("");
const field = ref();
console.log(fieldValue)
// Method to manually update the value if needed
function updateValue(newValue: T) {
console.log("VALIDATE IS UPDATING THE HOUSE NUMBER", newValue);
field.value = newValue;
}
const getValue = computed(() => {
return value;
});
return {
fieldValue,
errorMessage,
updateValue,
getValue
};
}
The view
import { useFieldValidationHelper3 } from "@/composables/FieldValidationHelper3";
const testValue = ref({
houseNumber: '1'
});
const { fieldValue, errorMessage, updateValue, getValue } = useFieldValidationHelper3(testValue.value.houseNumber);
// This is not the preferred way
const updateText = () => {
updateValue(getValue.value+"1");
}
... Some more HTML
<v-text-field hide-details="auto" label="House Number" v-model="testValue.houseNumber" class="noBorders"></v-text-field>
{{ fieldValue, errorMessage, updateValue, getValue }}
<button @click="updateText">Test </button>
I’m confident that I’m nearing a solution.
Best, Ido
There are several problems.
A value is being instantly read in the composable, this defies the purpose of using a ref and it couldn't be made reactive this way in JavaScript.
It should be either:
watch(() => value.value, ...)
Or, since watch
specifically supports refs for this use:
watch(value, ...)
There's a problem with variable naming, because value
isn't a value but value reference. If a parameter is allowed to be both a value and a ref, it could be fieldValueOrRef
for clarity.
At the place where a value is consumed it can be:
const fieldValue = toValue(fieldValueOrRef);
In the case of a computed or watcher this should happen inside a function, but it doesn't serve a good purpose to set up a watcher for non-reactive value, so in this case a watcher should've been made optional depending on the provided value.
Instantly reading a value like useFieldValidationHelper3(testValue.value.houseNumber)
is the same as useFieldValidationHelper3(1)
, and again, it cannot be made reactive this way. In order to do this, the argument should be a ref (could be a computed in this case). But to the associated boilerplate, it can also be a getter function that is commonly used for computed
, etc to lazily read a value. This is the reason toValue
accepts both refs and getter functions, this allows to unify the usage in an efficient way. A getter function prevents useFieldValidationHelper3
from exposing its own model as single source of truth, but this shouldn't be a problem because a model already exists in the context where useFieldValidationHelper3
is used, this makes getValue
and updateValue
not useful.
So a composable with external model could be:
function useFieldValidationHelper3<T>(fieldGetterOrRef: Ref<T> | () => T) {
if (isRef(fieldGetterOrRef) or typeof fieldGetterOrRef === 'function') {
watch(() => toValue(fieldGetterOrRef), value => ...)
} else {
// composable doesn't do anything useful
}
...
With a possible use:
const { errorMessage } = useFieldValidationHelper3(() => testValue.value.houseNumber);
const updateText = () => {
testValue.value.houseNumber = 2
}
And a composable that accepts an initial value and has optional internal model could be:
function useFieldValidationHelper3<T>(fieldValueOrRef: MaybeRef<T>) {
let fieldRef: Ref<T>;
if (isRef(fieldValueOrRef)) {
// external model
if (isReadonly(fieldValueOrRef))
throw new Error()
fieldRef = fieldValueOrRef;
} else {
// internal model
fieldRef = ref(fieldValueOrRef);
}
watch(fieldRef, value => ...);
...
return { fieldRef, ... }
}
With a possible use:
const houseNumberRef = ref(1);
const testValue = ref({ houseNumber: houseNumberRef });
const { errorMessage, fieldRef } = seFieldValidationHelper3(houseNumberRef);
const updateText = () => {
fieldRef.value = 2 // or houseNumberRef
}
Either way, no need for separate getValue
and updateValue
, this is what reactive objects do. And fieldRef
that a composable returns is redundant when there's external model.