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.