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.