Rzut okiem na Reactive API, bo później nie będziemy tego tłumaczyć tylko od razu przejdziemy do pisania komponentów z Laravelem i Inertią. Do dzieła.
Ok, ref:
const count = ref(0)
console.log(count.value) // 0
count.value = 1
console.log(count.value) // 1
Ref czyli poniekąd odpowiednik dynamicznych zmiennych z data(), które już robiliśmy, a poniekąd useState z Reacta, gdzie value i setValue to tak naprawdę jedna rzecz w dodatku zmienna.
Już to widzieliśmy:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
Ale ref może też robić za computed:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
Kolejny przykład:
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
Reactive – do reaktywnych obiektów:
const obj = reactive({ count: 0 })
obj.count++
Jeżeli trochę przespaliśmy frameworki frontendowe to możemy się zastanawiać po co to. Cóż, we frontnedzie po prostu typy złożone i jednocześnie reaktywne… z nimi inaczej się obchodzimy.
Wszystkie te in-place array methods tam po prostu nie działają. To jest, chcesz coś dodać to tablicy, literał nowej tablicy, nowy element i spread starej.
Chcesz coś usunąć z tablicy? Filter na starej, literał tablicy, spread na odfiltrowanej. Tam nie ma zmieniania typów złożonych (obiektów, tablic). Wynika to z konstrukcji samej reaktywności, używania Proxy, aby ją osiągnąć, po prostu nie można mieć typów złożonych i reaktywnych, którym zmiany będziesz in-place robił.
Dlatego Vue posiada interfejs do robienia reaktywnych obiektów/typów złożonych.
Ref unwrapping:
const count = ref(1)
const obj = reactive({ count })
// ref will be unwrapped
console.log(obj.count === count.value) // true
// it will update `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// it will also update `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
Readonly, czyli niemutowalność bez żadnych ceregieli typu ściągnij biblioteczkę tylko do tego albo baw się w Object.defineProperty (które zawsze robi jakieś dziwne akcje):
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// works for reactivity tracking
console.log(copy.count)
})
// mutating original will trigger watchers relying on the copy
original.count++
// mutating the copy will fail and result in a warning
copy.count++ // warning!
Przy okazji mamy tam watchEffect. No moim zdaniem Vue wygrywa z Reactem pod wieloma względami. Zobaczmy reaktywną zmienną:
const count = ref(1)
To samo w React:
let [count, setCount] = useState(1);
Teraz useEffect w React:
useEffect(() => {
console.log(copyCount);
}, [copyCount]);
A teraz watchEffect z Vue:
watchEffect(() => {
console.log(copy.count)
})
I poruszamy się tutaj po najłatwiejszych przykładach, w których nie mamy callbacków w useState, funkcji sprzątającej w useEffect i ogólnie w których React błyszczy, schody się później zaczynają.
Ok, zobaczmy jeszcze watchEffect:
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
count.value++
// -> logs 1
Ok, watcher na refa:
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
Robiliśmy już to, tylko prostacko używając Vue.createApp i tam mieliśmy watchery. Teraz widzimy, jak się pisze prawdziwe apki Vue, nie ma czegoś takiego jak jeden przydługi plik, w dodatku w formacie obiektu, gdzie źle postawiony przecinek albo niewłaściwe zagnieżdżenie rozwala nam projekt.
Ok, watcher reactive (gettera):
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
Watcher settera reactive:
const state = reactive({ count: 0 })
watch(
() => state,
(newValue, oldValue) => {
// newValue === oldValue
},
{ deep: true }
)
Rzut oka na nieszczęsny Modal.vue utworzony przez Laravel Breeze z opcją Vue:
<script setup>
import { computed, onMounted, onUnmounted, watch } from 'vue';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['close']);
watch(
() => props.show,
() => {
if (props.show) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = null;
}
}
);
const close = () => {
if (props.closeable) {
emit('close');
}
};
const closeOnEscape = (e) => {
if (e.key === 'Escape' && props.show) {
close();
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => {
document.removeEventListener('keydown', closeOnEscape);
document.body.style.overflow = null;
});
const maxWidthClass = computed(() => {
return {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[props.maxWidth];
});
</script>
<template>
//(...)
</template>
Jak widać komponenty też mają hooki cyklu życia, nie tylko App (w zasadzie można powiedzieć, że one mają przede wszystkim te hooki, bo po co nam unmounted albo beforeMount w kontekście App? Używaliśmy tego z synchronicznymi alertami, aby zdebugować działanie Vue i ogarnąć jak działa framework frontendowy, teraz już wiemy, jak to mówią…)
Ok, więcej Vue już wkrótce.