# Table of Contents
- Component Structure
- Reactive State
- Computed / Memoization
- Event Handlers & Methods
- Watchers / Effects
- Lifecycle Hooks
- Props & Communication
- Two-Way Binding
- Template Refs / DOM Refs
- Context / Provide-Inject
- Composables / Custom Hooks
- Conditional Rendering
- List Rendering
- Slots / Children & Render Props
- Styling
- Routing
- State Management
- 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++
const state = reactive({ name: 'Vue', score: 100 }) state.score += 10
|
# React
1 2 3 4 5 6 7
| import { useState } from 'react'
const [count, setCount] = useState(0) setCount(count + 1)
const [state, setState] = useState({ name: 'React', score: 100 }) setState(prev => ({ ...prev, score: prev.score + 10 }))
|
# 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)
const total = computed(() => price.value * qty.value) console.log(total.value)
|
# 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)
const total = useMemo(() => price * qty, [price, qty])
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('')
watch(query, (newVal, oldVal) => { console.log('query changed:', newVal) })
const stop = watchEffect(() => { console.log('query is now:', query.value) })
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('')
useEffect(() => { console.log('query changed:', query) }, [query])
useEffect(() => { const controller = new AbortController() fetch(`/search?q=${query}`, { signal: controller.signal }) return () => controller.abort() }, [query])
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'
useEffect(() => { console.log('mounted') return () => console.log('unmounted') }, [])
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
| function Child({ title, count = 0, onSubmit }) { return ( <button onClick={() => onSubmit('hello')}>{title}</button> ) }
<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
|
function MyInput({ value, onChange }) { return <input value={value} onChange={e => onChange(e.target.value)} /> }
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
| import { provide, ref } from 'vue'
const theme = ref('dark') provide('theme', theme)
import { inject } from 'vue'
const theme = inject('theme') const size = inject('size', 'md')
|
# 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'
const ThemeContext = createContext('light')
function App() { const [theme, setTheme] = useState('dark') return ( <ThemeContext.Provider value={theme}> <Child /> </ThemeContext.Provider> ) }
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
| 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 } }
const { count, increment } = useCounter(10)
|
# React — Custom Hook
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 } }
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
| function MyCard({ header, children, footer }) { return ( <div className="card"> {header} {children} {footer} </div> ) }
function MyCard({ children }) { const close = () => { } return <div className="card">{children(close)}</div> }
<MyCard header={<h2>Title</h2>} footer={<button>Close</button>} > <p>Body content</p> </MyCard>
<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
| 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
| import { createBrowserRouter, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter([ { path: '/', element: <Home /> }, { path: '/user/:id', element: <User /> }, ])
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
| 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
| 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 |