Урок 04

Composition API vs хуки

Сравнение

Оба — способ организации логики. Vue — Composition API, React — хуки.

Базовое состояние

Vue:

import { ref } from 'vue';

const count = ref(0);
count.value++;

React:

import { useState } from 'react';

const [count, setCount] = useState(0);
setCount(count + 1);

Эффекты

Vue — watch:

import { watch } from 'vue';

watch(count, (newVal, oldVal) => {
    console.log(`${oldVal} → ${newVal}`);
});

React — useEffect:

useEffect(() => {
    console.log(count);
}, [count]);

Vue не требует массив зависимостей.

Жизненный цикл

Vue:

import { onMounted, onUnmounted } from 'vue';

onMounted(() => console.log('Mounted'));

React:

useEffect(() => {
    console.log('Mounted');
    return () => console.log('Unmounted');
}, []);

Vue — отдельные функции. React — один useEffect.

Мемоизация

Vue — computed:

const doubled = computed(() => count.value * 2);

React — useMemo:

const doubled = useMemo(() => count * 2, [count]);

Vue автоматически трекает зависимости.

Ref vs useRef

Vue ref — реактивное значение (аналог React useState).

React useRef — мутируемый контейнер:

const inputRef = useRef<HTMLInputElement>(null);

Vue — то же:

const inputRef = ref<HTMLInputElement | null>(null);

Callback memoization

Vue — не нужно:

function handleClick() { ... }

Функции не пересоздаются.

React — useCallback:

const handleClick = useCallback(() => { ... }, [deps]);

Vue проще — не надо useCallback.

Кастомные хуки

Vue — composable:

// composables/useCounter.ts
export function useCounter(initial = 0) {
    const count = ref(initial);
    const increment = () => count.value++;
    return { count, increment };
}

React — кастомный хук:

// hooks/useCounter.ts
export function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount(c => c + 1), []);
    return { count, increment };
}

Правила хуков

React:

  • Только на верхнем уровне
  • Не в условиях
  • Не в циклах
  • Префикс use

Vue:

  • Можно в условиях (Composition API)
  • Не в циклах (технически можно, но осторожно)
  • Префикс use

Контекст / Provide

Vue:

provide('theme', 'dark');
const theme = inject('theme');

React:

const ThemeContext = createContext('light');

<ThemeContext.Provider value="dark">
    {children}
</ThemeContext.Provider>

const theme = useContext(ThemeContext);

Vue проще — без создания контекста.

Редуктор

Vue — useReducer-нет, но есть реактив:

const state = reactive({ count: 0 });
function dispatch(action) {
    if (action.type === 'inc') state.count++;
}

React:

const [state, dispatch] = useReducer(reducer, { count: 0 });

Vue — написать вручную. React — встроен.

Порядок и группировка

Vue composable — всё вместе:

export function useUser() {
    const user = ref(null);
    const loading = ref(false);

    async function load() { ... }

    onMounted(load);

    return { user, loading, load };
}

React хук — то же:

export function useUser() {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(false);

    const load = useCallback(async () => { ... }, []);

    useEffect(() => { load(); }, [load]);

    return { user, loading, load };
}

React длиннее из-за useCallback.

Что общего

  • Композиция вместо наследования
  • Префикс use
  • Переиспользование логики
  • Изоляция состояния

Плюсы Vue

  • Меньше boilerplate
  • Не надо useCallback/useMemo
  • Auto-tracking зависимостей
  • Composable можно в условиях
  • Проще порядок

Плюсы React

  • Один способ везде
  • Явный контроль
  • useReducer из коробки
  • Больше инструментов
  • Валидация правил через ESLint

Полный пример

Vue composable:

export function useForm<T extends object>(initial: T) {
    const values = reactive({ ...initial });
    const errors = ref<Record<string, string>>({});
    const touched = ref<Record<string, boolean>>({});

    const isValid = computed(() => Object.keys(errors.value).length === 0);

    function setField<K extends keyof T>(key: K, value: T[K]) {
        values[key] = value;
        touched.value[key as string] = true;
    }

    function reset() {
        Object.assign(values, initial);
        errors.value = {};
        touched.value = {};
    }

    return { values, errors, touched, isValid, setField, reset };
}

React хук:

export function useForm<T extends object>(initial: T) {
    const [values, setValues] = useState(initial);
    const [errors, setErrors] = useState<Record<string, string>>({});
    const [touched, setTouched] = useState<Record<string, boolean>>({});

    const isValid = useMemo(() => Object.keys(errors).length === 0, [errors]);

    const setField = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
        setValues(v => ({ ...v, [key]: value }));
        setTouched(t => ({ ...t, [key]: true }));
    }, []);

    const reset = useCallback(() => {
        setValues(initial);
        setErrors({});
        setTouched({});
    }, [initial]);

    return { values, errors, touched, isValid, setField, reset };
}

Итог

  • Composition API ≈ хуки по смыслу
  • Vue короче — меньше useCallback/useMemo
  • React гибче — useReducer, useEffect
  • Vue — composable в условиях можно
  • React — правила хуков строже

Дальше — Pinia vs Redux/Zustand.