# Table of Contents

  1. Component Structure
  2. Reactive State
  3. Computed / Memoization
  4. Event Handlers & Methods
  5. Watchers / Effects
  6. Lifecycle Hooks
  7. Props & Communication
  8. Two-Way Binding
  9. Template Refs / DOM Refs
  10. Context / Provide-Inject
  11. Composables / Custom Hooks
  12. Conditional Rendering
  13. List Rendering
  14. Slots / Children & Render Props
  15. Styling
  16. Routing
  17. State Management
  18. Quick Reference

# 1. Component Structure

Vue 3 React
File extension .vue (SFC) .jsx / .tsx
Template language HTML-like template JSX
Script style <script setup> Function component
Style scope <style scoped> CSS modules / CSS-in-JS

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="greeting">{{ message }}</div>
</template>

<script setup>
import { ref } from 'vue'
const message = ref('Hello, Vue 3!')
</script>

<style scoped>
.greeting { color: #42b883; }
</style>

# React

1
2
3
4
5
6
7
import { useState } from 'react'
import styles from './Greeting.module.css'

export default function Greeting() {
const [message] = useState('Hello, React!')
return <div className={styles.greeting}>{message}</div>
}

# 2. Reactive State

Vue 3 React
Primitive ref(value).value useState(value)[val, setVal]
Object reactive({}) useState({}) or useReducer
Read count.value count
Write count.value = 1 setCount(1)
Immutability Mutable (proxy) Immutable (must use setter)

# Vue 3

1
2
3
4
5
6
7
import { ref, reactive } from 'vue'

const count = ref(0)
count.value++ // mutate directly

const state = reactive({ name: 'Vue', score: 100 })
state.score += 10 // mutate directly

# React

1
2
3
4
5
6
7
import { useState } from 'react'

const [count, setCount] = useState(0)
setCount(count + 1) // must use setter

const [state, setState] = useState({ name: 'React', score: 100 })
setState(prev => ({ ...prev, score: prev.score + 10 })) // spread to keep immutability

# 3. Computed / Memoization

Vue 3 React
Derived value computed(() => ...) useMemo(() => ..., [deps])
Cached function useCallback(() => ..., [deps])
Auto dependency Yes (reactive tracking) No (manual deps array)

# Vue 3

1
2
3
4
5
6
7
8
import { ref, computed } from 'vue'

const price = ref(100)
const qty = ref(3)

// Auto-tracks price and qty
const total = computed(() => price.value * qty.value)
console.log(total.value) // 300

# React

1
2
3
4
5
6
7
8
9
10
import { useState, useMemo, useCallback } from 'react'

const [price, setPrice] = useState(100)
const [qty, setQty] = useState(3)

// Must list deps manually
const total = useMemo(() => price * qty, [price, qty])

// Cache a function reference
const handleAdd = useCallback(() => setQty(q => q + 1), [])

# 4. Event Handlers & Methods

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>

<template>
<button @click="increment">{{ count }}</button>
<button @click="count++">Inline</button>
<input @keyup.enter="submit" />
<form @submit.prevent="onSubmit" />
</template>

# React

1
2
3
4
5
6
7
8
9
10
11
const [count, setCount] = useState(0)
const increment = () => setCount(c => c + 1)

return (
<>
<button onClick={increment}>{count}</button>
<button onClick={() => setCount(c => c + 1)}>Inline</button>
<input onKeyUp={e => e.key === 'Enter' && submit()} />
<form onSubmit={e => { e.preventDefault(); onSubmit() }} />
</>
)

# 5. Watchers / Effects

Vue 3 React
Run on change watch(source, cb) useEffect(() => ..., [deps])
Run immediately watch(src, cb, { immediate: true }) useEffect(() => ..., [deps]) (always runs on mount)
Auto-track deps watchEffect(() => ...)
Cleanup Return nothing / onWatcherCleanup Return cleanup function
Deep watch watch(src, cb, { deep: true }) Manual deep comparison

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ref, watch, watchEffect } from 'vue'

const query = ref('')

// Explicit — runs when query changes
watch(query, (newVal, oldVal) => {
console.log('query changed:', newVal)
})

// Auto-track — runs when any accessed reactive value changes
const stop = watchEffect(() => {
console.log('query is now:', query.value)
})

// With cleanup
watch(query, async (val, _, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
await fetch(`/search?q=${val}`, { signal: controller.signal })
})

# React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react'

const [query, setQuery] = useState('')

// Runs when query changes (also runs on mount)
useEffect(() => {
console.log('query changed:', query)
}, [query])

// With cleanup
useEffect(() => {
const controller = new AbortController()
fetch(`/search?q=${query}`, { signal: controller.signal })
return () => controller.abort() // cleanup function
}, [query])

// Run once on mount (like onMounted)
useEffect(() => {
console.log('mounted')
return () => console.log('unmounted')
}, [])

# 6. Lifecycle Hooks

Vue 3 React ( useEffect equivalent) When
setup() body Top of function body Initialization
onBeforeMount() Before first render
onMounted() useEffect(() => ..., []) After first render
onBeforeUpdate() Before re-render
onUpdated() useEffect(() => ...) (no deps) After every render
onBeforeUnmount() Before destroy
onUnmounted() useEffect cleanup ( return () => ... ) After destroy

# Vue 3

1
2
3
4
5
import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => console.log('mounted'))
onUpdated(() => console.log('updated'))
onUnmounted(() => console.log('unmounted'))

# React

1
2
3
4
5
6
7
8
9
10
11
12
import { useEffect } from 'react'

// mounted
useEffect(() => {
console.log('mounted')
return () => console.log('unmounted') // unmounted
}, [])

// every update
useEffect(() => {
console.log('updated')
})

# 7. Props & Communication

Vue 3 React
Define props defineProps<T>() Function argument ({ prop })
Required prop { required: true } TypeScript type (no ? )
Default value withDefaults(...) Destructuring default = value
Child → Parent defineEmits + emit('event') Callback prop onEvent
v-model modelValue prop + update:modelValue emit Controlled component pattern

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Child.vue -->
<script setup>
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), { count: 0 })

const emit = defineEmits<{
submit: [value: string]
}>()
</script>

<template>
<button @click="emit('submit', 'hello')">{{ props.title }}</button>
</template>
1
2
<!-- Parent.vue -->
<Child title="Click me" :count="5" @submit="handleSubmit" />

# React

1
2
3
4
5
6
7
8
9
// Child.jsx
function Child({ title, count = 0, onSubmit }) {
return (
<button onClick={() => onSubmit('hello')}>{title}</button>
)
}

// Parent.jsx
<Child title="Click me" count={5} onSubmit={handleSubmit} />

# 8. Two-Way Binding

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Simple input binding -->
<input v-model="text" />

<!-- v-model on component -->
<!-- Parent -->
<MyInput v-model="name" />
<!-- Expands to: <MyInput :modelValue="name" @update:modelValue="name = $event" /> -->

<!-- Child (MyInput.vue) -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

# React

1
2
3
4
5
6
7
8
9
// React uses "controlled components" — no built-in two-way binding

function MyInput({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />
}

// Parent
const [name, setName] = useState('')
<MyInput value={name} onChange={setName} />

# 9. Template Refs / DOM Refs

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<input ref="inputEl" />
</template>

<script setup>
import { ref, onMounted } from 'vue'

const inputEl = ref(null)

onMounted(() => {
inputEl.value.focus()
})
</script>

# React

1
2
3
4
5
6
7
8
9
10
11
import { useRef, useEffect } from 'react'

function MyInput() {
const inputEl = useRef(null)

useEffect(() => {
inputEl.current.focus()
}, [])

return <input ref={inputEl} />
}

# 10. Context / Provide-Inject

# Vue 3 — Provide / Inject

1
2
3
4
5
6
7
8
9
10
11
// Ancestor
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme) // can be reactive

// Descendant (any depth)
import { inject } from 'vue'

const theme = inject('theme') // reactive ref
const size = inject('size', 'md') // with default

# React — Context API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createContext, useContext, useState } from 'react'

// Create context
const ThemeContext = createContext('light')

// Provider (ancestor)
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
)
}

// Consumer (any descendant)
function Child() {
const theme = useContext(ThemeContext)
return <div>{theme}</div>
}

# 11. Composables / Custom Hooks

Both patterns extract reusable stateful logic. The naming convention differs:

  • Vue: useXxx() composable functions
  • React: useXxx() custom hooks (React enforces the use prefix)

# Vue 3 — Composable

1
2
3
4
5
6
7
8
9
10
11
12
13
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initial = 0) {
const count = ref(initial)
const double = computed(() => count.value * 2)
const increment = () => count.value++
const reset = () => (count.value = initial)
return { count, double, increment, reset }
}

// Usage
const { count, increment } = useCounter(10)

# React — Custom Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
// hooks/useCounter.js
import { useState, useMemo } from 'react'

export function useCounter(initial = 0) {
const [count, setCount] = useState(initial)
const double = useMemo(() => count * 2, [count])
const increment = () => setCount(c => c + 1)
const reset = () => setCount(initial)
return { count, double, increment, reset }
}

// Usage
const { count, increment } = useCounter(10)

# 12. Conditional Rendering

# Vue 3

1
2
3
4
5
6
7
8
<template>
<div v-if="isLoggedIn">Welcome back!</div>
<div v-else-if="isGuest">Hello, guest.</div>
<div v-else>Please log in.</div>

<!-- v-show keeps element in DOM -->
<Spinner v-show="isLoading" />
</template>

# React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return (
<>
{isLoggedIn ? (
<div>Welcome back!</div>
) : isGuest ? (
<div>Hello, guest.</div>
) : (
<div>Please log in.</div>
)}

{/* Short-circuit for simple show/hide */}
{isLoading && <Spinner />}

{/* CSS equivalent of v-show */}
<Spinner style={{ display: isLoading ? 'block' : 'none' }} />
</>
)

# 13. List Rendering

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- Array -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>

<!-- With index -->
<li v-for="(item, i) in items" :key="i">
{{ i }}: {{ item.name }}
</li>

<!-- Object -->
<li v-for="(val, key) in user" :key="key">
{{ key }}: {{ val }}
</li>
</template>

# React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
return (
<ul>
{/* Array */}
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}

{/* With index */}
{items.map((item, i) => (
<li key={i}>{i}: {item.name}</li>
))}

{/* Object */}
{Object.entries(user).map(([key, val]) => (
<li key={key}>{key}: {val}</li>
))}
</ul>
)

# 14. Slots / Children & Render Props

Vue 3 React
Default content <slot /> {children}
Named slots <slot name="header" /> Separate props ( headerSlot )
Scoped slots <slot :data="val" /> Render props / children as function

# Vue 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- MyCard.vue -->
<template>
<div class="card">
<slot name="header" />
<slot />
<slot name="footer" :closeModal="close" />
</div>
</template>

<!-- Parent usage -->
<MyCard>
<template #header><h2>Title</h2></template>
<p>Body content</p>
<template #footer="{ closeModal }">
<button @click="closeModal">Close</button>
</template>
</MyCard>

# React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// MyCard.jsx — named "slots" via props
function MyCard({ header, children, footer }) {
return (
<div className="card">
{header}
{children}
{footer}
</div>
)
}

// Render prop pattern for scoped slot equivalent
function MyCard({ children }) {
const close = () => { /* ... */ }
return <div className="card">{children(close)}</div>
}

// Parent usage
<MyCard
header={<h2>Title</h2>}
footer={<button>Close</button>}
>
<p>Body content</p>
</MyCard>

// Render prop usage
<MyCard>{close => <button onClick={close}>Close</button>}</MyCard>

# 15. Styling

Vue 3 React
Scoped CSS <style scoped> CSS Modules ( .module.css )
Global CSS <style> (no scoped) Import directly
Dynamic class :class="{ active: isActive }" className={isActive ? 'active' : ''}
Dynamic style :style="{ color: color }" style={{ color }}
CSS-in-JS styled-components, Emotion

# Vue 3

1
2
3
4
5
6
7
8
9
<template>
<div :class="{ active: isActive, 'text-lg': isLarge }">Text</div>
<div :class="[baseClass, isActive && 'active']">Text</div>
<div :style="{ color, fontSize: size + 'px' }">Text</div>
</template>

<style scoped>
.active { background: #42b883; }
</style>

# React

1
2
3
4
5
6
7
8
9
10
11
12
import styles from './Component.module.css'

function Component({ isActive, isLarge, color, size }) {
return (
<>
<div className={`${styles.base} ${isActive ? styles.active : ''} ${isLarge ? styles.lg : ''}`}>
Text
</div>
<div style={{ color, fontSize: `${size}px` }}>Text</div>
</>
)
}

# 16. Routing

Vue Router React Router v6
Setup createRouter() createBrowserRouter()
Outlet <RouterView /> <Outlet />
Link <RouterLink to="..."> <Link to="...">
Navigate router.push(...) useNavigate()
Params route.params.id useParams().id
Query route.query.page useSearchParams()

# Vue 3 — Vue Router

1
2
3
4
5
6
7
8
9
10
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/user/:id', component: User, name: 'user' },
]
})
1
2
3
4
5
6
7
8
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()

console.log(route.params.id)
router.push({ name: 'user', params: { id: 1 } })
</script>

# React — React Router v6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.jsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom'

const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/user/:id', element: <User /> },
])

// Usage in component
import { useNavigate, useParams, Link } from 'react-router-dom'

function User() {
const { id } = useParams()
const navigate = useNavigate()

return (
<>
<p>User: {id}</p>
<button onClick={() => navigate('/')}>Home</button>
<Link to="/">Home Link</Link>
</>
)
}

# 17. State Management

Vue 3 — Pinia React — Zustand
Define store defineStore('id', () => {...}) create((set) => {...})
State ref() inside store Properties in initial object
Getters computed() inside store Derived selectors
Actions Functions inside store Functions calling set()
Use in component useCounterStore() useCounterStore(selector)

# Vue 3 — Pinia

1
2
3
4
5
6
7
8
9
10
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
return { count, double, increment }
})
1
2
3
4
5
6
7
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
const { count, double } = storeToRefs(store)
</script>

# React — Zustand

1
2
3
4
5
6
7
8
// stores/counter.js
import { create } from 'zustand'

export const useCounterStore = create((set, get) => ({
count: 0,
double: () => get().count * 2,
increment: () => set(state => ({ count: state.count + 1 })),
}))
1
2
3
4
5
function Counter() {
const count = useCounterStore(s => s.count)
const increment = useCounterStore(s => s.increment)
return <button onClick={increment}>{count}</button>
}

# 18. Quick Reference

Feature Vue 3 React
Reactive primitive ref(val) useState(val)
Reactive object reactive({}) useState({})
Computed / memoize computed(() => ...) useMemo(() => ..., [deps])
Cached function useCallback(fn, [deps])
Side effects watchEffect(() => ...) useEffect(() => ..., [deps])
Watch specific watch(src, cb) useEffect(() => ..., [dep])
On mount onMounted(() => ...) useEffect(() => ..., [])
On unmount onUnmounted(() => ...) useEffect(() => { return cleanup }, [])
DOM reference const el = ref(null) const el = useRef(null)
Props defineProps<T>() Function argument
Emit / callback defineEmits<T>() Callback prop
Shared state provide / inject createContext / useContext
Reusable logic Composable useXxx() Custom hook useXxx()
Template HTML template + directives JSX
Conditional v-if / v-show Ternary / &&
List v-for .map()
Two-way bind v-model Controlled component
Scoped styles <style scoped> CSS Modules
Global store Pinia Zustand / Redux
Router Vue Router React Router
Edited on