Vue 3 Composition API Migration
Overview
Migrating WikiLambda Vue components from Options API to Composition API using the setup() function pattern.
IMPORTANT: Only Vue components need migration. Pinia stores work with both APIs - no conversion needed.
The Pattern
Hybrid single <script> Tag with setup() Function that still uses module.exports
vue
<script>
const { computed, defineComponent, inject, ref } = require( 'vue' );
const useZObject = require( '../../composables/useZObject.js' );
const useMainStore = require( '../../store/index.js' );
module.exports = exports = defineComponent( {
name: 'my-component',
components: {
'child-component': ChildComponent
},
props: {
keyPath: {
type: String,
required: true
},
objectValue: {
type: Object,
required: true
}
},
emits: [ 'update-value' ],
setup( props, { emit } ) {
// 1. Inject i18n (provided by MediaWiki)
const i18n = inject( 'i18n' );
// 2. Use composables (destructure only what's needed)
const { getZObjectType, getZBooleanValue } = useZObject();
// 3. Access store (DO NOT destructure - use store.property)
const store = useMainStore();
// 4. Reactive state
const localState = ref( 'initial' );
// 5. Computed properties
const displayValue = computed( () => getZObjectType( props.objectValue ) );
// 6. Methods
function handleUpdate( value ) {
store.setValueByKeyPath( value ); // Use store.action()
emit( 'update-value', value );
}
// 7. MUST explicitly return everything the template uses
return {
// Only return what template actually uses
getZObjectType,
displayValue,
handleUpdate,
// Expose store getters/actions used in template
getLabelData: store.getLabelData
};
}
} );
</script>Key Rules
✅ DO:
- Use single <script> tag
- Use setup( props, { emit } ) inside defineComponent()
- Access props via props.propName
- Use emit() for events
- Use inject( 'i18n' ) for internationalization (MediaWiki provides i18n via app.provide)
- Access store via store.property (e.g., store.getLabelData, store.setValueByKeyPath())
- Use storeToRefs() ONLY for getters returned by reference in the return statement
- Explicitly return all values/methods template needs
- Destructure only needed methods from composables
❌ DON'T:
- Use two <script> tags
- Use spread operators in returns
- Use this (use props/emit instead)
- Use getCurrentInstance() for i18n (use inject( 'i18n' ) instead)
- Destructure from store (can cause reactivity issues and test complications)
- Return everything (only what template needs)
Pinia Store Access
When to Use storeToRefs()
Per Pinia docs, use storeToRefs() to destructure state and getters while maintaining reactivity:
setup( props, { emit } ) { const store = useMainStore(); // ✅ CORRECT: Use storeToRefs when destructuring getters/state const { isInitialized, getCurrentView } = storeToRefs( store ); // ✅ CORRECT: Actions can be destructured directly (they're just functions) const { setValueByKeyPath, fetchZids } = store; // ✅ CORRECT: Access store directly in computed/methods const displayValue = computed( () => store.getLabelData( someId ) ); function handleClick() { setValueByKeyPath( value ); // ✅ Destructured action store.fetchZids( zids ); // ✅ Or call directly } return { // Reactive refs from storeToRefs isInitialized, getCurrentView, // Other data displayValue, handleClick }; }
The Rules
- Destructure getters/state with storeToRefs() - Maintains reactivity
- Destructure actions directly from store - They're just functions, no reactivity needed
- Or access everything via store.property - Simpler, always works
Why? Pinia getters and state are reactive refs. Direct destructuring without storeToRefs() breaks reactivity. Actions are plain functions and can be destructured directly.
Composables Available
| Composable | Replaces | Methods Provided |
|---|---|---|
| useZObject() | zobjectMixin | getZObjectType, getZBooleanValue, etc. |
| useType() | typeMixin | typeToString, isValidZidFormat, etc. |
| useError(options) | errorMixin | fieldErrors, hasFieldErrors, clearFieldErrors |
| useEventLog() | eventLogMixin | logEvent |
| usePageTitle(options) | pageTitleMixin | updatePageTitle |
| useMetadata() | metadataMixin | metadata utils |
| useClipboard(options) | clipboardMixin | copy, getCopiedText |
Note: Some composables accept optional parameters:
- useError({ keyPath }) - optional keyPath for error tracking
- useZObject({ keyPath }) - optional keyPath for context
Migration Steps
- Remove mixin imports
- Import corresponding composables
- Add setup( props, { emit } ) function
- Convert data() to ref() or reactive()
- Convert computed to computed()
- Convert methods to functions
- Return all values/methods template uses
- Test the component
Migration Status
🏆 MIGRATION COMPLETE: 84 / 84 Files (100%) 🏆
Every Vue component and view has been successfully migrated to Composition API!
✅ Completed: Components with Mixins (40/40)
✅ Completed: Components without Mixins (40/40)
✅ Completed: View Files (4/4)
Composables Created (9/9)
Pinia Stores
NO MIGRATION NEEDED ✅
Stores work with both Options and Composition API. Access via const store = useMainStore();
Key Patterns Established
Store Access
// Get store instance const store = useMainStore(); // Use storeToRefs for getters you're returning by reference const { isInitialized, getCurrentView } = storeToRefs( store ); // Access store directly in computed/methods const value = computed( () => store.getLabelData( id ) ); store.setValueByKeyPath( data ); // Return with organized sections return { // Reactive store data isInitialized, getCurrentView, // Other data value, handleClick };
i18n Access
// Inject once const i18n = inject( 'i18n' ); // Return for template use return { i18n }; // Use in template {{ i18n( 'message-key' ).text() }}
Test Refactoring Status
🔄 IN PROGRESS: Test Behavior Migration
Goal: Refactor tests from implementation testing to behavior testing after Composition API migration.
Status: test files need refactoring
See: TEST_REFACTORING_TODOS.md for detailed refactoring plans and progress tracking
Migration Results
- ✅ 84 files converted (80 components + 4 views)
- ✅ 9 composables created to replace 8 mixins
- ✅ 0 linting errors
- ✅ All tests passing
- ✅ Full JSDoc documentation preserved
- ✅ Proper reactivity maintained with storeToRefs pattern
- 🔄 Test refactoring in progress
Test Refactoring TODOs - Behavior Testing Migration
This document outlines the test files that need refactoring from implementation testing to behavior testing after the Vue 3 Composition API migration.
🎯 Overview
During the migration from Options API to Composition API, many tests were updated but still use implementation testing patterns (accessing wrapper.vm properties, internal component state, etc.). This document prioritizes which tests need refactoring to focus on user behavior instead of implementation details.
📊 Current Status
- 10 files still using wrapper.vm (implementation testing) - 9 completed ✅
- 1 file still using $nextTick (need waitFor migration) - 5 completed ✅
- 72 files with excessive find usage (1009+ calls)
- 6 files need render wrapper functions (DRY improvement) - 16 completed ✅
- 44 files already have render wrappers ✅
- 12 files render wrapper pattern fixed (simplified stubs handling) ✅
🚀 Systematic Cleanup Tasks
1) 🔧 Behavior Testing Migration
Replace wrapper.vm access with user behavior testing:
2) ⏱️ Async Testing Migration
Replace $nextTick with waitFor() for better async handling:
3) 🧹 DRY Improvement
Create render wrapper functions to eliminate repetitive setup code:
📋 Quick Reference
Testing Best Practices
- Focus on user behavior over implementation details
- Use waitFor() for asynchronous assertions
- Prefer mount() over shallowMount() when testing user interactions
- Test component integration, not just unit functionality
- Ensure tests are maintainable and readable
Rules
- find vs get: Use get() when element must exist (test fails if not found), use find() when element might not exist
- wrapper.vm: Avoid accessing internal component state, test user-observable behavior instead
- $nextTick: Replace with waitFor() for better async handling
- Render wrappers: Create DRY helper functions to eliminate repetitive setup code
- Render wrapper pattern: Use simplified stubs handling - { stubs: { Component: false } } instead of complex global object spreading