# Паттерны конвертации React → Vue 3 ## Паттерн 1 — Базовый компонент **React:** ```tsx interface Props { label: string; onClick: () => void } const Button: React.FC<Props> = ({ label, onClick }) => ( <button onClick={onClick}>{label}</button> ) ``` **Vue 3:** ```vue <script setup lang="ts"> const props = defineProps<{ label: string }>() const emit = defineEmits<{ click: [] }>() </script> <template> <button @click="emit('click')">{{ props.label }}</button> </template> ``` --- ## Паттерн 2 — Локальный стейт и эффекты Шпаргалка: - `useState` → `ref` / `reactive` - `useEffect(() => {}, [])` → `onMounted` - `useEffect(() => {}, [dep])` → `watch(dep, ...)` - `useEffect(() => { return cleanup })` → `onUnmounted` - `useMemo` → `computed` - `useCallback` → просто функция (не нужен в Vue) **React:** ```tsx const [isOpen, setIsOpen] = useState(false) useEffect(() => { document.title = isOpen ? 'Open' : 'Closed' return () => { document.title = '' } }, [isOpen]) ``` **Vue 3:** ```vue <script setup lang="ts"> import { ref, watch, onUnmounted } from 'vue' const isOpen = ref(false) watch(isOpen, (val) => { document.title = val ? 'Open' : 'Closed' }) onUnmounted(() => { document.title = '' }) </script> ``` --- ## Паттерн 3 — react-hook-form → VeeValidate v4 > 27+ файлов в проекте. Zod-схема не меняется. Шпаргалка: - `useForm({ resolver: zodResolver(schema) })` → `useForm({ validationSchema: toTypedSchema(schema) })` - `Controller` render-prop → `defineField` + `v-model` - `formState.errors` → `errors` - `formState.isSubmitting` → `isSubmitting` - `reset()` → `resetForm()` - `setValue` → `setFieldValue` **React:** ```tsx const { control, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema) }) ``` **Vue 3:** ```vue <script setup lang="ts"> import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' const { defineField, handleSubmit, errors } = useForm({ validationSchema: toTypedSchema(schema) }) const [email, emailAttrs] = defineField('email') const onSubmit = handleSubmit((data) => { /* ... */ }) </script> <template> <form @submit="onSubmit"> <input v-model="email" v-bind="emailAttrs" /> <span v-if="errors.email">{{ errors.email }}</span> </form> </template> ``` --- ## Паттерн 4 — createContext / useContext → provide / inject > Catalog модуль. Правило: внутри модуля → provide/inject, снаружи → Pinia. ```ts // catalog.injection-key.ts import type { InjectionKey, Ref } from 'vue' export interface CatalogState { filters: Ref<Record<string, unknown>> setFilters: (v: Record<string, unknown>) => void } export const CatalogKey: InjectionKey<CatalogState> = Symbol('catalog') ``` ```vue <!-- CatalogProvider.vue --> <script setup lang="ts"> import { ref, provide } from 'vue' import { CatalogKey } from './catalog.injection-key' const filters = ref({}) const setFilters = (v: Record<string, unknown>) => { filters.value = v } provide(CatalogKey, { filters, setFilters }) </script> <template><slot /></template> ``` ```ts // useCatalog.ts import { inject } from 'vue' import { CatalogKey } from './catalog.injection-key' export const useCatalog = () => { const catalog = inject(CatalogKey) if (!catalog) throw new Error('useCatalog must be used within CatalogProvider') return catalog } ``` --- ## Паттерн 5 — forwardRef → defineExpose > 3 файла: Dropdown, SearchModal, sidebar/Dropdown ```vue <script setup lang="ts"> import { ref } from 'vue' const rootEl = ref<HTMLDivElement>() defineExpose({ el: rootEl }) </script> <template> <div ref="rootEl"><!-- ... --></div> </template> ``` --- ## Паттерн 6 — Кастомный хук → Composable > Аргументы передавай как `Ref<T>`, не как примитивы. ```ts import { ref, watch, onUnmounted } from 'vue' import type { Ref } from 'vue' export const useAutoRefresh = (id: Ref<string>) => { const data = ref(null) let interval: ReturnType<typeof setInterval> const start = () => { interval = setInterval(async () => { data.value = await fetchData(id.value) }, 5000) } watch(id, () => { clearInterval(interval); start() }, { immediate: true }) onUnmounted(() => clearInterval(interval)) return { data } } ``` --- ## Паттерн 7 — ErrorBoundary (class) → onErrorCaptured ```vue <script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' const hasError = ref(false) onErrorCaptured((err) => { hasError.value = true console.error(err) return false }) </script> <template> <FallbackUI v-if="hasError" /> <slot v-else /> </template> ``` --- ## Паттерн 8 — Zustand vanilla → Pinia ```ts export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const setUser = (u: User) => { user.value = u } const logout = () => { user.value = null } return { user, setUser, logout } }) ```