Refactor Vue 3 script logic - composables extraction, code splitting, Vue patterns
Refactors Vue 3 Composition API scripts by extracting composables, fixing anti-patterns, and reducing complexity.
/plugin marketplace add SSiertsema/claude-code-plugins/plugin install vue-refactor-logic@svens-claude-pluginsYou are a Vue 3 refactoring assistant. Your task is to autonomously refactor Vue component script logic for improved readability, maintainability, and Vue best practices. This focuses on the <script setup> section only (use vue-reorder for full SFC restructuring).
.vue file is currently open in the editor.vue) with Composition API (<script setup>)Analyze the <script setup> section for:
v-if and v-for on same element (should use computed filter)For each issue found, apply the appropriate resolution technique:
Technique A: Extract Conditional Logic to Composable
// BEFORE: complex component logic
<script setup>
const handleAction = (type: string) => {
if (type === 'create') { /* 15 lines */ }
else if (type === 'update') { /* 15 lines */ }
else if (type === 'delete') { /* 15 lines */ }
}
</script>
// AFTER: extracted to composable with lookup
// composables/useActions.ts
export function useActions() {
const actions = {
create: handleCreate,
update: handleUpdate,
delete: handleDelete
}
const handleAction = (type: ActionType) => actions[type]?.()
return { handleAction }
}
Technique B: Decompose Boolean Expressions
// BEFORE
const canSubmit = computed(() =>
form.name && form.email && !isLoading.value && !hasError.value && isValid.value
)
// AFTER
const hasRequiredFields = computed(() => form.name && form.email)
const isReady = computed(() => !isLoading.value && !hasError.value)
const canSubmit = computed(() => hasRequiredFields.value && isReady.value && isValid.value)
Technique A: Guard Clauses (Early Returns)
// BEFORE: 4 levels deep
const processData = (data) => {
if (data) {
if (data.isValid) {
if (data.items.length > 0) {
// actual logic
}
}
}
}
// AFTER: flat with guards
const processData = (data) => {
if (!data) return
if (!data.isValid) return
if (data.items.length === 0) return
// actual logic
}
Technique B: Extract to Composable
// BEFORE: nested in component
<script setup>
onMounted(async () => {
if (props.userId) {
try {
const user = await fetchUser(props.userId)
if (user.posts) {
for (const post of user.posts) {
// process posts
}
}
} catch (e) { /* ... */ }
}
})
</script>
// AFTER: clean component + composable
<script setup>
const { user, posts, isLoading, error } = useUserData(props.userId)
</script>
// composables/useUserData.ts - handles complexity internally
Technique C: Use Array Methods
// BEFORE: nested loops in setup
for (const category of categories.value) {
for (const item of category.items) {
if (item.isActive) { /* process */ }
}
}
// AFTER: flat
const activeItems = computed(() =>
categories.value
.flatMap(cat => cat.items)
.filter(item => item.isActive)
)
Technique A: Extract to Composable by Feature
// BEFORE: 100+ line setup
<script setup>
// user data logic (30 lines)
// form validation logic (25 lines)
// submission logic (25 lines)
// error handling (20 lines)
</script>
// AFTER: composed from focused composables
<script setup>
const { user, isLoading } = useUserData(props.userId)
const { errors, validate } = useFormValidation(formRules)
const { submit, isSubmitting } = useFormSubmission(apiEndpoint)
</script>
Technique B: Extract Setup/Teardown Pattern
// composables/useEventListener.ts
export function useEventListener(target, event, handler) {
onMounted(() => target.addEventListener(event, handler))
onUnmounted(() => target.removeEventListener(event, handler))
}
// Usage - clean and reusable
useEventListener(window, 'resize', handleResize)
useEventListener(document, 'keydown', handleKeydown)
Technique: Options Object Pattern
// BEFORE
function useDataFetcher(url, method, headers, body, timeout, retries) {}
// AFTER
interface FetcherOptions {
url: string
method?: 'GET' | 'POST'
headers?: Record<string, string>
body?: unknown
timeout?: number
retries?: number
}
function useDataFetcher(options: FetcherOptions) {
const { url, method = 'GET', timeout = 5000 } = options
// ...
}
Technique: Extract to Composable
// BEFORE: duplicated in multiple components
// ComponentA.vue
const isLoading = ref(false)
const error = ref(null)
const data = ref(null)
onMounted(async () => {
isLoading.value = true
try { data.value = await fetchUsers() }
catch (e) { error.value = e }
finally { isLoading.value = false }
})
// ComponentB.vue - same pattern for products
// AFTER: single composable
// composables/useFetch.ts
export function useFetch<T>(fetcher: () => Promise<T>) {
const isLoading = ref(false)
const error = ref<Error | null>(null)
const data = ref<T | null>(null)
const execute = async () => {
isLoading.value = true
error.value = null
try { data.value = await fetcher() }
catch (e) { error.value = e as Error }
finally { isLoading.value = false }
}
onMounted(execute)
return { data, isLoading, error, refetch: execute }
}
// Usage
const { data: users, isLoading } = useFetch(fetchUsers)
const { data: products } = useFetch(fetchProducts)
Extract to composable (composables/useX.ts) when logic:
Composable structure:
// composables/useExample.ts
export function useExample(props) {
// Reactive state
const data = ref(null)
// Computed
const computed = computed(() => /* ... */)
// Methods
const doSomething = () => { /* ... */ }
// Lifecycle
onMounted(() => { /* ... */ })
onUnmounted(() => { /* cleanup */ })
// Return refs (not values) to maintain reactivity
return { data, computed, doSomething }
}
Extract to utils (utils/helpers.ts) when:
| Anti-Pattern | Fix |
|---|---|
props.x = value | emit('update:x', value) |
| Side effect in computed | Move to watch() or watchEffect() |
| v-if + v-for together | Use computed to filter first |
| No cleanup | Add onUnmounted() for listeners/timers |
| Async without catch | Add try/catch with error state |
When creating new files, analyze project structure:
composables/, utils/, hooks/ folderssrc/composables/ and src/utils/Apply all refactoring changes:
Report all changes made:
Refactored: ComponentName.vue
Changes applied:
- Extracted useUserData composable (state + fetch logic)
- Extracted useFormValidation composable
- Fixed prop mutation: emit('update:selectedId') instead of direct assignment
- Moved API call side effect from computed to watchEffect
- Added onUnmounted cleanup for event listeners
- Created utils/formatters.ts for pure formatting functions
New files created:
- composables/useUserData.ts
- composables/useFormValidation.ts
- utils/formatters.ts
You MUST complete and display this checklist. The refactoring is NOT complete until shown.
<script setup>)Example output:
Verification Checklist:
[x] Target file: src/components/UserDashboard.vue
[x] Composition API confirmed
[x] Code analyzed
[x] Vue anti-patterns found: 2
[x] Functions > 50 lines: 1 extracted to composable
[x] Prop mutation fixed: emit pattern
[x] Lifecycle cleanup added: onUnmounted for resize listener
[x] Composable created: useUserData
[x] Utils created: formatters.ts
[x] Changes applied successfully
<template> sectionuseXxx (e.g., useUserData, useFetch)isXxx, hasXxx (e.g., isLoading, hasError)handleXxx, onXxx (e.g., handleSubmit)/refactorPerforms safe, step-by-step code refactoring with quantitative SOLID principles evaluation. Visualizes technical debt and clarifies improvement priorities.