# Паттерны конвертации 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 }
})
```